Java-REST-Web-服务安全指南-全-

Java REST Web 服务安全指南(全)

原文:zh.annas-archive.org/md5/28dd94223a475a77126c29f9db046845

译者:飞龙

协议:CC BY-NC-SA 4.0

前言

在计算机系统开发中使用 Web 服务的固有优势与创建对它们进行安全管理需求的优势相同。今天,我们可以这样说,没有任何一家公司能够在完全隔离的状态下工作,无需与其他人互动、共享和消费信息。此外,这是任何公司的最重要资产。因此,这些需求在代码行之间也是共同的。本书通过实际场景和可应用解决方案,引导你一步步学习,以便你能够轻松学习到解决最常见需求的解决方案和实现方法。

与基于 SOAP 的 Web 服务相比,RESTful Web 服务提供了几个优势。例如,在处理数据类型时,根据你使用的编程语言或创建它们的库,使用空值("")而不是 NULL 时可能会发现不一致性。此外,使用不同版本的库创建/消费 Web 服务时,可能会发现映射复杂对象和文件传输中的兼容性问题。在某些情况下,即使从.NET 应用程序中消费由 Java 创建的 Web 服务,最终也会在两者之间创建一个 Java 实现的中间服务。在 RESTful Web 服务中不会发生这种情况,因为在这种情况下,功能是通过 HTTP 方法调用暴露的。

为了保护信息,证券领域有许多有助于实现这一目标的功能。例如,了解如何通过认证和授权等一些问题来帮助实现任何选定的机制,其主要目标是使我们的应用程序更安全、更安全,这是至关重要的。选择每种不同的应用程序安全方式都与你想解决的问题相关;为此,我们展示了每种方式的用法场景。

许多时候,我们见过大型组织花费时间和精力创建自己的实现来处理安全,而不是使用已经解决了我们需要的标准的做法。通过我们想要与你分享的知识,我们希望避免这种重新发明轮子的过程。

本书涵盖内容

第一章,设置环境,帮助我们创建第一个功能应用程序,这与Hello World示例非常相似,但具有更多功能,并且非常接近现实世界。本章的主要目的是让我们熟悉我们将要使用的工具。

第二章, 确保 Web 服务的重要性,探讨了 Java 平台中所有可能的认证模型。为了更好地理解,我们将一步一步深入探讨如何利用每个可用的认证模型。我们将展示信息是如何暴露的,以及第三方如何拦截它,并且我们将使用 Wireshark,这是一个非常好的工具来解释这一点。

最后,在本章中,我们将回顾认证和授权之间的区别。这两个概念都非常重要,并且在安全术语的背景下绝对不能忽视。

第三章, 使用 RESTEasy 进行安全管理,展示了 RESTEasy 如何提供处理安全的机制,从相当基本的模型(粗粒度)到更复杂的模型(细粒度),在这个模型中你可以执行更彻底的控制,包括管理不仅配置文件,还包括程序文件。

第四章, RESTEasy 骨架密钥,帮助我们研究 OAuth 实现以及令牌携带实现和单点登录。所有这些都是为了限制资源共享的方式。一如既往,你将通过代码和真实示例进行实践。我们想向你展示通过这些技术共享应用程序之间的资源和信息是如何变成最有用和最强大的技术之一,允许客户端或用户只需使用一次凭证即可访问多个服务,限制第三方应用程序访问你的信息或数据,并通过令牌携带实现访问控制。你将学习如何应用这些技术和概念来构建安全且灵活的应用程序。

第五章, 数字签名和消息加密,通过一个简单的示例帮助我们理解数字签名的优势;你会注意到消息的接收者可以验证发送者的身份。此外,我们将模拟外部代理在传输过程中修改数据的情况,并看看数字签名如何帮助我们检测它,以避免处理损坏的数据。

最后,我们将解释 SMIME 用于正文加密及其工作原理,通过一个加密请求和响应的示例来帮助你更好地理解。

你需要这本书的什么

为了实现和测试本书中的所有示例,我们将使用许多免费工具,如下所示:

  • Eclipse IDE(或任何其他 Java IDE)

  • JBoss AS 7

  • Maven

  • Wireshark

  • SoapUI

这本书面向谁

本书面向开发者、软件分析师、架构师或与软件开发和 RESTful Web 服务工作的人士。本书要求您具备一些关于 Java 或其他语言中面向对象编程概念的先验知识。

在本书中,您不需要具备关于安全模型的知识,因为我们将在书中解释理论并在实际示例中应用它。

术语

在本书中,您将找到多种文本样式,用于区分不同类型的信息。以下是一些这些样式的示例及其含义的解释。

文本中的代码单词、数据库表名、文件夹名、文件名、文件扩展名、路径名、虚拟 URL、用户输入和 Twitter 昵称应如下所示:“我们将修改web.xml文件。”

代码块应如下设置:

private boolean isUserAllowed(final String username, final String password, final Set<String> rolesSet) {
    boolean isAllowed = false;
    if (rolesSet.contains(ADMIN)) {
      isAllowed = true;
    }
    return isAllowed;
  }
}

当我们希望引起您对代码块中特定部分的注意时,相关的行或项目将以粗体显示:

final List<String> authorizationList = headersMap.get(AUTHORIZATION_PROPERTY);

任何命令行输入或输出都应如下所示:

mvn clean install

新术语重要词汇将以粗体显示。您在屏幕上看到的单词,例如在菜单或对话框中,在文本中应如下所示:“从弹出窗口中选择SSL 设置选项卡。”

注意

警告或重要提示将以这样的框显示。

小贴士

小技巧和窍门将以这样的形式出现。

读者反馈

我们欢迎读者提供的建议或评论。请告诉我们您对这本书的看法——您喜欢什么或可能不喜欢什么。读者反馈对我们开发您真正从中受益的标题以及改进我们传授知识的方式非常重要。

要发送给我们一般性的反馈,只需发送一封电子邮件到<feedback@packtpub.com>,并在邮件的主题中提及书名。

如果您在某个主题上具有专业知识,并且您有兴趣撰写或为本书做出贡献,请参阅我们的作者指南www.packtpub.com/authors

客户支持

现在,您已经成为 Packt 书籍的骄傲拥有者,我们有一些事情可以帮助您从您的购买中获得最大收益。

下载示例代码

您可以从您在www.packtpub.com的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问www.packtpub.com/support并注册,以便将文件直接通过电子邮件发送给您。此外,我们强烈建议您从 GitHub 获取源代码,GitHub 地址为github.com/restful-java-web-services-security

错误清单

尽管我们已经尽一切努力确保我们内容的准确性,但错误仍然可能发生。如果您在我们的书中发现错误——可能是文本或代码中的错误——如果您能向我们报告这一点,我们将不胜感激。通过这样做,您可以节省其他读者的挫败感,并帮助我们改进本书的后续版本。如果您发现任何勘误,请通过访问www.packtpub.com/submit-errata,选择您的书,点击勘误提交表单链接,并输入您的勘误详情来报告它们。一旦您的勘误得到验证,您的提交将被接受,勘误将被上传到我们的网站,或添加到该标题的勘误部分下的现有勘误列表中。任何现有勘误都可以通过从www.packtpub.com/support中选择您的标题来查看。

盗版

在所有媒体中,互联网上对版权材料的盗版是一个持续存在的问题。在 Packt,我们非常重视我们版权和许可证的保护。如果您在互联网上发现我们作品的任何非法副本,无论形式如何,请立即向我们提供位置地址或网站名称,以便我们可以寻求补救措施。

如果您在书的任何方面遇到问题,请通过<copyright@packtpub.com>与我们联系,并提供涉嫌盗版材料的链接。

我们感谢您在保护我们作者以及为我们带来有价值内容方面的帮助。

询问

如果您在书的任何方面遇到问题,请通过<questions@packtpub.com>与我们联系,我们将尽力解决。

第一章. 环境设置

我们非常欢迎您加入我们旅程的第一章。让我们给您一个关于您在这里将实现什么的想法。阅读本章后,您将拥有设置用于与 RESTful Web 服务一起工作的开发环境所需的基本和启发性的知识。然后,您将熟悉与它相关的一个非常基础的项目开发。此外,到本章结束时,您将非常清楚地了解如何使用 RESTful Web 服务创建应用程序以及如何实现这一点。本章将以非常简单和全面的方式为您提供与这类 Web 服务一起工作的信息。

在本章中,我们将涵盖以下主题:

  • 安装开发环境

  • 创建我们的第一个 RESTful Web 服务应用程序

  • 测试 RESTful Web 服务

下载工具

首先,我们必须获取我们的工作工具,以便我们能够动手编写代码。这里指定的工具在全球范围内被广泛使用,但您可以选择您自己的工具。记住,“工具不能造就艺术家”。无论您使用 Windows、MAC OS X 还是 Linux,都有适用于每个操作系统的工具。

让我们简要解释每个工具的用途。我们将使用 Eclipse 作为我们的 IDE,JBoss AS 7.1.1.Final 作为我们的应用服务器,Maven 来自动化构建过程,以及 SoapUI 作为测试我们创建的 Web 服务功能的工具。此外,我们建议您安装最新版本的 JDK,即 JDK 1.7.x。为了帮助您,我们已经获取并包含了您需要使用的链接,以获取实现第一个示例所需的软件。每个链接都提供了关于每个工具的更多信息,如果您对它们不熟悉,这些信息可能会很有用。

下载链接

以下工具必须下载:

创建基础项目

为了使构建我们的示例项目的过程更加简单,我们将使用 Maven。这款出色的软件可以瞬间创建一个基础项目,我们的项目可以轻松编译和打包,无需依赖于特定的 IDE。

Maven 使用存档来创建特定类型的项目。存档是之前创建的项目模板;它们允许我们从 Java 桌面应用程序到多模块项目创建各种应用程序,其中 EAR 可以包含多个工件,如 JAR 和 WAR。其主要目标是尽可能快地让用户开始使用,通过提供一个演示 Maven 许多功能的示例项目。如果您想了解更多关于 Maven 的信息,可以通过访问maven.apache.org/来找到更多信息。

然而,我们这里描述的信息已经足够我们继续前进。我们将使用存档来创建一个基本项目;如果我们想更具体一些,我们将使用存档来创建一个 Java 网络应用程序。为此,我们将在终端中输入以下命令行:

mvn archetype:generate

当我们在终端中执行此命令行时,我们将获得 Maven 存储库中所有可用的存档。因此,让我们查找我们需要的存档来创建我们的网络应用程序;它的名字是webapp-javaee6,属于组org.codehaus.mojo.archetypes。我们还可以通过一个代表其 ID 的数字来搜索它;这个数字是557,如下面的截图所示。我们建议您通过名称进行搜索,因为数字可能会改变,因为以后可能会添加其他存档:

创建基础项目

将出现几个问题;我们必须为每个问题提供相应的信息。Maven 将使用这些信息来创建我们之前选择的存档,如下面的截图所示:

创建基础项目

如您可能已经注意到的,每个问题都要求您定义一个属性,每个属性的解释如下:

  • groupId:这个属性表示公司的域名反向顺序;这样我们就可以识别代码的所有者是哪个公司

  • artifactId:这个属性表示项目的名称

  • version:这个属性表示项目的版本

  • package:这个属性表示将要添加类的基包名

类名和包名一起构成了类的全名。这个全名允许以独特的方式识别类名。有时,当有多个具有相同名称的类时,包名有助于识别它属于哪个库。

下一步是将项目放入 Eclipse 的工作空间;为此,我们必须通过文件 | 导入 | Maven | 现有 Maven 项目来将我们的项目导入 Eclipse。

我们应该在 IDE 中看到项目,如下面的截图所示:

创建基础项目

在继续之前,让我们修复pom.xml文件中发生的问题。

以下代码中显示的错误与来自 Eclipse 和 Maven 集成的错误有关。为了修复这个问题,我们不得不在 <build> 标签之后添加 <pluginManagement> 标签。

pom.xml 文件应如下所示:

<project  
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.packtpub</groupId>
  <artifactId>resteasy-examples</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>war</packaging>

  . . .

  <build>
 <pluginManagement>
      <plugins>
        <plugin>
          . . .
        </plugin>
      </plugins>
 </pluginManagement>
  </build>

</project>

小贴士

下载示例代码

您可以从您在 www.packtpub.com 的账户下载您购买的所有 Packt 书籍的示例代码文件。如果您在其他地方购买了这本书,您可以访问 www.packtpub.com/support 并注册,以便直接将文件通过电子邮件发送给您。我们还强烈建议您从 GitHub 获取源代码,GitHub 地址为 github.com/restful-java-web-services-security

这将修复错误,现在我们只需要更新项目中的 Maven 配置,如下面的截图所示:

创建基本项目

在刷新项目后,错误应该会消失,因为我们更新 Maven 的配置实际上是在更新我们的项目依赖,例如缺少的库。通过这种方式,我们将它们包含到我们的项目中,错误就会消失。

src/main/webapp 路径内,让我们创建一个 WEB-INF 文件夹。

现在,在 WEB-INF 文件夹内,我们将创建一个名为 web.xml 的新文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 

  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
  http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
</web-app>

当您在保护您的应用程序时,这个文件非常有用;这次,我们将不进行任何配置来创建它。目前,/WEB-INF 文件夹和 web.xml 文件仅定义了 Web 应用程序的结构。

首个功能示例

现在我们已经设置好了我们的开发环境,是时候动手编写第一个 RESTful Web 服务了。由于我们使用的是 JBoss,让我们使用 JAX-RS 的 RESTEasy 实现。我们将开发一个非常简单的示例;让我们假设您想实现一个保存和搜索人员信息的服务的功能。

首先,我们创建一个简单的 Person 领域类,该类使用 JAXB 注解。JAXB 在 XML 和 Java 之间序列化/反序列化对象。在这个例子中,我们将这些实例存储在内存缓存中,而不是数据库中。在 JEE 中,这通常代表关系数据库中的一个表,每个实体实例对应于该表中的一行,如下面的代码所示:

package com.packtpub.resteasy.entities;

import javax.xml.bind.annotation.XmlAccessType;
import javax.xml.bind.annotation.XmlAccessorType;
import javax.xml.bind.annotation.XmlAttribute;
import javax.xml.bind.annotation.XmlElement;
import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name = "person")
@XmlAccessorType(XmlAccessType.FIELD)
public class Person {

  @XmlAttribute
  protected int id;

  @XmlElement
  protected String name;

  @XmlElement
  protected String lastname;

  public int getId() {
    return id;
  }

  public void setId(int id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public String getLastname() {
    return lastname;
  }

  public void setLastname(String lastname) {
    this.lastname = lastname;
  }

}

接下来,我们在 com.packtpub.resteasy.services 包中创建一个新的类 PersonService。这个类将有两个方法;一个用于注册新人员,另一个用于通过 ID 搜索人员。这个类将使用内存映射缓存来存储人员信息。

该服务将具有以下实现:

package com.packtpub.resteasy.services;

import java.net.URI;
import java.util.HashMap;
import java.util.Map;

import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.WebApplicationException;
import javax.ws.rs.core.Response;

import com.packtpub.resteasy.entities.Person;

@Path("/person")
public class PersonService {
  private Map<Integer, Person> dataInMemory;
  public PersonService() {
    dataInMemory = new HashMap<Integer, Person>();
  }

  @POST
  @Consumes("application/xml")
  public Response savePerson(Person person) {
    int id = dataInMemory.size() + 1;
    person.setId(id);
    dataInMemory.put(id, person);
    return Response.created(URI.create("/person/" + id)).build();
  }

  @GET
  @Path("{id}")
  @Produces("application/xml")
  public Person findById(@PathParam("id") int id) {
    Person person = dataInMemory.get(id);
    if (person == null) {
      throw new WebApplicationException(Response.Status.NOT_FOUND);
    }
    return person;
  }
}

@Path注解定义了 URL 中可用的路径,该路径位于此类中编写的功能内部。被@Post注解的方法表示它应该执行 HTTP POST 请求。此外,它还注解了@Consumes并使用application/xml值;这意味着 POST 请求将以包含要保存的人的信息的 XML 格式的字符串执行。另一方面,要从 ID 查找一个人,您必须执行 HTTP GET 请求。URL 必须以与在方法上@Path注解中指示的方式相同的方式指示 ID。@Produces注解表示我们将以 XML 格式获取响应。最后,请注意,参数 ID,如@Path注解中所示,是使用@PathParam注解的方法的参数。

最后,我们编写一个将扩展Application类并将我们刚刚创建的服务设置为单例的类。这样,信息就不会在每次请求中丢失,我们将在内存中保持它如下所示:

package com.packtpub.resteasy.services;

import java.util.HashSet;
import java.util.Set;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/services")
public class MyRestEasyApplication extends Application {

  private Set<Object> services;

  public MyRestEasyApplication() {
    services = new HashSet<Object>();
    services.add(new PersonService());
  }

  @Override
  public Set<Object> getSingletons() {
    return services;
  }
}

注意,由于我们已经使用 JAXB 映射了我们的实体,我们的方法以 XML 格式消费和生成信息。

为了在 JBoss 中部署我们的应用程序,我们应在pom.xml文件中添加一个依赖项。这个依赖项必须引用 JBoss 插件。我们必须更改pom.xml中生成的工件名称。默认值是这个artifactId文件,后面跟着版本;例如,resteasy-examples-1.0-snapshot.war。我们将将其设置为仅使用artifactId文件;在这种情况下,resteasy-examples.war。所有这些配置都必须在pom.xml中包含、修改和实现,如下面的 XML 代码片段所示:

  <build>
 <finalName>${artifactId}</finalName>
    <pluginManagement>
      <plugins>
        <plugin>
          <groupId>org.jboss.as.plugins</groupId>
          <artifactId>jboss-as-maven-plugin</artifactId>
          <version>7.5.Final</version>
          <configuration>
            <jbossHome>/pathtojboss/jboss-as-7.1.1.Final</jbossHome>
          </configuration>
        </plugin>
        ...
        </plugin>
      </plugins>
    </pluginManagement>
  </build>

您应该更改jbossHome属性的值,以指定您的 JBoss 安装路径。之后,我们将使用命令行终端;进入项目的目录,并输入mvn jboss-as:run。如果在命令执行后对代码进行了任何更改,那么您应该使用以下命令来查看更改:

mvn jboss-as:redeploy

运行和重新部署是这个插件的目标。如果您想了解更多关于此插件的目标,您可以访问docs.jboss.org/jbossas/7/plugins/maven/latest/)。这将重新编译所有项目类;然后将其打包以创建.war文件。最后,修改将部署到服务器上。如果一切正常,我们应该在终端中看到一条消息,表明部署已成功完成,如下面的截图所示:

第一个功能示例

本章的源代码可在 GitHub 的以下位置找到:

github.com/restful-java-web-services-security/source-code/tree/master/chapter01

测试示例 Web 服务

在这个时刻,我们将测试我们刚刚创建的功能。我们将使用 SoapUI 作为我们的测试工具;请确保您使用的是最新版本,或者至少是等于或大于 4.6.x 的版本,因为这个版本提供了更多测试 RESTful Web 服务的功能。让我们先执行以下步骤:

  1. 从主菜单开始,让我们通过导航到 文件 | 新建 REST 项目 来创建一个新的 REST 项目,如下面的截图所示:测试示例 Web 服务

  2. 设置我们服务的 URI,如下所示:测试示例 Web 服务

  3. 然后,让我们从工作区使用 POST 方法创建一个新的个人。在 媒体类型 字段中,选择 application/xml,并使用包含 XML 信息的字符串进行请求,如下面的文本所示:

    <person><name>Rene</name><lastname>Enriquez</lastname></person>
    
  4. 当我们点击 播放 按钮,我们应该获得一个显示创建的资源 URI(超链接 "http://localhost:8080/resteasy-examples/services/person/1")的答案,如下面的截图所示:测试示例 Web 服务

  5. 如果我们在 SoapUI 的 资源 文本框中更改 URI 并使用 GET 方法,它将显示我们刚刚输入的数据,如下面的截图所示:测试示例 Web 服务

恭喜!我们已经开发出了第一个具有两个功能的 RESTful Web 服务。第一个功能是保留人们的信息在内存中,第二个是通过 ID 获取人们的信息。

注意

如果您重新启动 JBoss 或重新部署应用程序,所有数据都将丢失。在搜索人们的信息之前,您必须首先保存数据。

摘要

在本章中,我们创建了我们的第一个功能应用程序——类似于一个 hello world 示例,但功能更接近现实世界。

本章中我们涵盖的基本部分是熟悉我们将要使用的工具。在后面的章节中,我们将假设这些概念已经清楚。例如,当使用 SoapUI 时,我们将逐步前进,因为这是一个将有助于测试我们将要开发的功能的工具。这样,我们将避免编写 Web 服务客户端代码的任务。

现在,我们已经准备好回顾下一章,其中包含 Java 提供的一些安全模型。我们将了解每一个,并学习如何实现它们。

第二章. 保护网络服务的重要性

看你;你已经到达了第二章;恭喜!这一章非常重要,因为它与软件中隐含的一个概念有关,那就是安全。这一点非常重要,因为软件被公司和像我们这样的人使用。有时,我们通过软件分享非常重要的机密信息,这就是为什么这个话题对每个人来说都如此重要。

在本章中,我们将向您介绍与计算机系统安全管理相关的基本方面。

我们将探索和实施不同的安全机制及其可用场景。

此外,你还将学习如何使用协议分析器。这将使我们能够展示攻击是如何进行的,并确定当攻击达到目标时,即在我们的信息中,这种攻击的影响。你还将能够想象出更多在 Web 服务中实施安全性的选项。

因为一切都需要实践,你将通过一个简单的代码示例来学习身份验证和授权之间的区别。准备好一个有趣且实用的主题。

本章我们将涵盖以下内容:

  • 理解安全管理的重要性

  • 探索和实施不同的安全机制

  • 使用协议分析器截获请求

  • 理解身份验证和授权之间的区别

安全的重要性

安全管理是设计应用程序时需要考虑的主要方面之一。

无论怎样,组织的功能和信息都不能没有任何限制地暴露给所有用户。考虑一下人力资源管理系统的情况,它可以让你查询员工的工资,例如:如果公司经理需要知道他们员工之一的工资,这并不是什么重要的事情。然而,在同样的背景下,想象一下,如果其中一名员工想知道他们同事的工资;如果对这个信息的访问是完全开放的,这可能会在薪酬不同的员工之间产生问题。

一个更加关键的例子可能是银行 XYZ 在客户或第三方通过 ATM 向其账户存款时,增加银行余额的情况。IT 经理设想这种功能可能会很常见,并决定将其作为一项网络服务来实现。目前,这项功能仅限于使用该网络服务的应用程序登录的银行用户。假设 IT 经理对未来的愿景成真,现在这项功能需要从 ATM 实现;迅速提出这一需求表明这种功能已经实现,并且可以通过调用网络服务来使用。到目前为止,可能没有安全漏洞,因为 ATM 很可能有一个控制系统来控制访问,因此操作系统对网络服务功能的访问也是间接控制的。

现在,假设公司 ABC 希望有一种类似的功能,通过向其员工的银行账户增加一个x金额来认可他们对公司的某种贡献。网络服务的功能会发生什么变化?你认为你还能再次信任处理其自身安全方案的应用程序来控制对其功能的访问吗?即使我们信任这个机制,如果请求被嗅探器截获怎么办?那么,任何知道如何执行请求的人都可以增加余额。当这些问题得到回答时,它们以一种相当逻辑的方式抛出。公开这些场景现在听起来相当合理,因此验证用户有权访问此功能的网络服务应该是可以信赖的,并且在这种情况下,应该被委托管理安全系统。无论是来自组织本身还是来自外部机构,都必须存在安全控制,以便公开像我们刚才概述的敏感功能。

当通过网络服务共享现有信息或功能时,众所周知,我们并不依赖于编程语言、架构或系统平台来交互。这给了我们灵活性,并使我们免于重写现有功能。进一步来说,我们应该理解这些特性对数据机密性有影响,因为我们将要与实体或系统共享信息和/或功能。这样,我们可以实现业务目标,并肯定防止入侵者读取我们的信息;或者更糟糕的是,未经授权的第三方能够访问我们服务公开的功能。因此,对这些功能的访问必须严格分析,并且我们公开的服务必须得到正确保障。

安全管理选项

Java 提供了一些安全管理选项。目前,我们将解释其中的一些,并演示如何实现它们。所有认证方法实际上都是基于从客户端到服务器的凭证传递。有几种方法可以执行此操作,它们包括:

  • 基本认证

  • 摘要认证

  • 客户端证书认证

  • 使用 API 密钥

使用 Java 构建的应用程序中的安全管理,包括具有 RESTful Web 服务的应用程序,始终依赖于 JAAS。

Java 认证和授权服务JAAS)是 Java 平台企业版的一部分的框架。因此,它是处理 Java 应用程序安全性的默认标准;它允许你实现授权,并允许对应用程序进行认证控制,以保护属于应用程序的资源。如果你想了解更多关于 JAAS 的信息,你可以查看以下链接:

docs.oracle.com/javase/7/docs/technotes/guides/security/jaas/tutorials/GeneralAcnOnly.html

如果你不想使用 JAAS,当然始终可以创建自己的实现来处理安全,但这会很困难。所以,我们为什么不节省一些时间,精力和平静,通过实现这项有用的技术呢?建议尽可能使用标准实现。在我们的开发练习中,我们将使用 JAAS 进行前三种认证方法。

授权和认证

当你使用这些术语时,很容易混淆,但在有安全系统方法的情况下,它们有不同的含义。为了澄清这些术语,我们将在本节中解释它们。

认证

简而言之,这个术语指的是你是谁。这是通过用户的用户名密码来识别用户的过程。当我们使用这个概念时,我们试图确保用户的身份,并验证用户声称的身份。此外,它与用户拥有的访问权限无关。

安全研究已经指定了一个应该验证的因素列表,以实现积极的认证。这个列表包含三个元素,其中使用其中两个是非常常见的,但最好我们使用所有这些。以下元素如下:

  • 知识因素:这个元素意味着用户知道的东西,例如,密码,通行短语或个人识别码(PIN)。另一个例子是挑战响应,用户必须回答一个问题,软件令牌或作为软件令牌的手机的电话。

  • 拥有因素:这是用户拥有的东西,例如,一个手环(在物理认证的情况下),身份证,安全令牌,或者内置硬件令牌的手机。

  • 内在因素:这是用户的某事,例如指纹或视网膜图案、DNA 序列、签名、面部、声音、独特的生物电信号或其他生物识别标识符。

授权

简而言之,这个术语指的是你能做什么。它是指授予用户执行或拥有某物的权限的过程。当我们谈论软件时,我们有一个系统管理员负责定义用户可以访问的系统以及使用权限(例如访问哪些文件目录、访问期限、分配的存储空间数量等等)。

授权通常被视为系统管理员设置权限的初始设置以及当用户获取访问权限时检查已设置的权限值。

访问控制

认证和授权的一个非常常见的用途是访问控制。一个仅应由授权用户使用的计算机系统必须尝试检测并拒绝未经授权的用户。通过持续进行认证过程以建立用户身份并具有一定程度的信心,同时授予该身份指定的权限来控制访问。让我们举一些在不同场景中涉及认证的访问控制示例,如下所示:

  • 当承包商首次到达房屋进行工作时要求出示照片 ID

  • 实施验证码作为验证用户是人类而不是计算机程序的一种方式

  • 当使用在手机等网络设备上获得的一次性密码OTP)作为认证密码/PIN

  • 一个使用盲凭证以认证到另一个程序的计算机程序

  • 当你用护照进入一个国家

  • 当你登录到计算机

  • 当一项服务使用确认电子邮件来验证电子邮件地址的所有权

  • 使用互联网银行系统

  • 当你从 ATM 机取款

有时,访问的便利性会与访问检查的严格性相权衡。例如,一笔小额交易通常不需要经过认证的人的签名作为交易授权的证明。

然而,安全专家认为,不可能绝对确定用户的身份。只能应用一套测试,如果通过,则已事先宣布为确认身份的最低标准。问题在于如何确定哪些测试足够;这取决于公司来确定这一套。

传输层安全性

在本节中,我们强调 TLS 的一些主要特性:

  • 它的前身是安全套接字层SSL

  • 它是一个加密协议

  • 它提供互联网上的安全通信

  • 它通过 X.509 证书(非对称加密)验证对方

  • 它允许客户端-服务器应用程序通过网络进行通信,并防止窃听和篡改

  • TLS 通常在传输层协议之上实现。

  • 它封装了特定于应用程序的协议,如 HTTP、FTP、SMTP、NNTP 和 XMPP。

  • TLS 的使用应委托给其他部门,尤其是在执行凭证、更新、删除以及任何类型的价值交易时。

  • 在现代硬件上,TLS 的开销非常低,只是略微增加了延迟,但这为最终用户提供了更多的安全性。

通过提供用户凭证进行基本身份验证

可能,基本身份验证是所有类型应用程序中最常用的技术之一。在用户获得应用程序功能之前,会被要求输入用户名和密码。这两个信息都会被验证,以确认凭证是否正确(它们属于应用程序用户)。我们 99% 确定您至少执行过这种技术一次,可能是通过定制机制,或者如果您使用过 JEE 平台,可能通过 JAAS。这种控制被称为 基本身份验证

这种安全实现的 主要问题是凭证以明文方式从客户端传播到服务器。这种方式下,任何嗅探器都可以读取通过网络发送的数据包。我们将通过一个名为 Wireshark 的工具来考虑一个示例;它是一个协议分析器,将展示这个问题。对于安装,我们可以访问链接 www.wireshark.org/download.html

安装相当简单(一路点击 下一步)。因此,我们不会展示这些步骤的截图。

现在,我们将修改来自 第一章,设置环境 的项目,其中用户尝试调用 Web 服务的任何功能。用户将被要求输入用户名和密码;一旦这些信息得到验证,用户将能够访问 Web 服务的功能。

为了有一个可工作的示例,让我们启动我们的应用程序服务器 JBoss AS 7;然后,转到 bin 目录并执行文件 add-user.bat(UNIX 用户的 .sh 文件)。最后,我们将创建一个新用户,如下所示:

通过提供用户凭证进行基本身份验证

这里最重要的是,您应该在第一个问题中选择 应用程序用户 并为其分配一个 管理员 角色。这将与 web.xml 文件中定义的信息相匹配,这将在我们实现应用程序内的安全措施时进行解释。结果,我们将在 JBOSS_HOME/standalone/configuration/application - users.properties 文件中有一个新用户。

JBoss 已经设置了一个默认的安全域,称为other;此域使用我们之前提到的文件中存储的信息进行身份验证。现在,我们将配置应用程序在resteasy-examples项目的WEB-INF文件夹内使用此安全域。让我们创建一个名为jboss-web.xml的文件,其内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
  <security-domain>other</security-domain>
</jboss-web>

好的,让我们配置web.xml文件以聚合安全约束。在下面的代码块中,您将看到需要加粗的内容:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 

  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
  http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
 <!-- Roles -->
 <security-role>
 <description>Any rol </description>
 <role-name>*</role-name>
 </security-role>

 <!-- Resource / Role Mapping -->
 <security-constraint>
 <display-name>Area secured</display-name>
 <web-resource-collection>
 <web-resource-name>protected_resources</web-resource-name>
 <url-pattern>/services/*</url-pattern>
 <http-method>GET</http-method>
 <http-method>POST</http-method>
 </web-resource-collection>
 <auth-constraint>
 <description>User with any role</description>
 <role-name>*</role-name>
 </auth-constraint>
 </security-constraint>

 <login-config>
 <auth-method>BASIC</auth-method>
 </login-config>
</web-app>

从终端,让我们转到resteasy-examples项目的家目录并执行mvn jboss-as:redeploy。现在,我们将像在第一章中那样测试我们的 Web 服务,使用 SOAP UI。我们将使用POST方法向 URLhttp://localhost:8080/resteasy-examples/services/person/发送以下 XML:

<person><name>Rene</name><lastname>Enriquez</lastname></person>

我们获得以下响应:

通过提供用户凭证进行基本身份验证

SOAP UI 显示我们 HTTP 401 错误,这意味着请求未被授权。这是因为我们没有向服务器提供凭证就执行了请求。为了做到这一点,我们必须点击位于 SOAP UI 左下角的()按钮,并输入我们刚刚创建的用户凭证,如下面的截图所示:

通过提供用户凭证进行基本身份验证

现在是时候启用我们的流量分析器了。让我们启动 Wireshark 并将其设置为分析环回地址内的流量。从菜单导航到捕获 | 接口

选择lo0选项,如下面的截图所示,然后点击开始按钮。这样,所有通过地址 127.0.0.1 或其等效的 localhost 的流量都将被拦截以供我们分析。

此外,在Filter字段中,我们只需输入http以拦截 HTTP 请求和响应,如下面的截图所示:

通过提供用户凭证进行基本身份验证

看看下面的截图:

通过提供用户凭证进行基本身份验证

完成这些操作后,我们将从 SOAP UI 执行请求操作。再次,SOAP UI 显示 HTTP 201 消息;这次,请求被成功处理。您可以在 Wireshark 中看到以下信息列:

  • :此列以独特的方式标识请求或响应

  • 时间:此列标识执行操作的时间

  • :此列标识请求/响应的起始地址

  • 目标:此列标识执行 HTTP 请求/响应的目标 IP 地址

  • 协议:此列标识执行请求/响应的协议

  • 长度:此列标识请求/响应的长度

  • 信息:此列标识与请求/响应相关的信息

现在,是时候在 Wireshark 上观察信息流量了,如下所示:

通过提供用户凭据进行基本认证

注意 Wireshark 如何显示我们正在使用 HTTP 协议和 XML 字符串(协议)对目标地址127.0.0.1(目的地)执行 POST(信息)操作。此外,您还可以读取用户名和密码。因此,这种方法对于安全实现来说并不非常安全,因为任何人都可以访问这些信息并执行钓鱼攻击。

您可以在此 URL 找到本章的源代码:

github.com/restful-java-web-services-security/source-code/tree/master/chapter02/basic-authentication

摘要访问认证

这种认证方法使用散列函数在将密码发送到服务器之前加密用户输入的密码。这显然比基本认证方法更安全,在基本认证方法中,用户的密码以明文形式传输,容易被拦截者读取。为了克服这些缺点,摘要 MD5 认证对用户名、应用安全域和密码的值组合应用一个函数。结果,我们得到一个几乎无法被入侵者解读的加密字符串。

为了更好地理解这个过程,我们将向您展示一个从维基百科提取的简单解释。

带有解释的示例

以下示例最初在 RFC 2617 中给出,此处扩展以显示每个请求和响应预期的完整文本。请注意,此处仅涵盖auth(认证)保护代码的质量——在撰写本文时,只有 Opera 和 Konqueror 网络浏览器已知支持auth-int(具有完整性保护的认证)。尽管规范提到了 HTTP 版本 1.1,但该方案可以成功添加到版本 1.0 服务器,如下所示。

这种典型的事务包括以下步骤:

客户端请求需要认证的页面,但没有提供用户名和密码。通常,这是因为用户只是输入了地址或跟随了一个链接到该页面。

服务器以 401“未经授权”的响应代码响应,提供认证域和一个随机生成的、一次性使用的值,称为nonce

在此阶段,浏览器将向用户展示认证域(通常是正在访问的计算机或系统的描述)并提示输入用户名和密码。用户可能会选择在此处取消。

一旦提供了用户名和密码,客户端将重新发送相同的请求,但添加一个包含响应代码的认证头。

在此示例中,服务器接受认证并返回页面。如果用户名无效和/或密码不正确,服务器可能会返回 401 响应代码,客户端将再次提示用户。

注意

客户端可能已经拥有所需的用户名和密码,无需再次提示用户,例如,如果它们之前已被网络浏览器存储。

如果你想了解更多关于此机制的信息,你可以通过以下链接访问维基百科上的完整文章 en.wikipedia.org/wiki/Digest_access_authentication

你还可以阅读规范 RFC 2617,它可在 www.ietf.org/rfc/rfc2617.txt 找到。

现在,让我们在我们的示例中测试此机制。

为了启动,我们必须确保环境变量 JAVA_HOME 已经设置并添加到 PATH 变量中。因此,你可以在终端中键入以下命令来确认:

java -version

这将显示以下截图中的信息:

带有解释的示例

此命令显示了我们的电脑上安装的 Java 版本。如果你获得的是错误信息而不是之前的输出,你应该创建环境变量 JAVA_HOME,将其添加到 PATH 变量中,并重复验证。

现在,为了执行我们之前解释的内容,我们需要为我们的示例用户生成一个密码。我们必须使用我们之前讨论的参数——用户名、域和密码来生成密码。让我们从终端进入 JBOSS_HOME/modules/org/picketbox/main/ 目录,并键入以下命令:

java -cp picketbox-4.0.7.Final.jar org.jboss.security.auth.callback.RFC2617Digest username MyRealmName password

我们将获得以下结果:

RFC2617 A1 hash: 8355c2bc1aab3025c8522bd53639c168

通过这个过程,我们获得加密的密码,并将其用于我们的密码存储文件(JBOSS_HOME/standalone/configuration/application-users.properties 文件)。我们必须替换文件中的密码,它将被用于用户 username。我们必须替换它,因为旧密码不包含应用程序的域名称信息。作为替代,你可以使用 add-user.sh 文件创建新用户;你只需在请求时提供域信息即可。

为了使我们的应用工作,我们只需要在 web.xml 文件中做一点修改。我们必须修改 auth-method 标签,将值 FORM 更改为 DIGEST,并按以下方式设置应用程序域名称:

<login-config>

  <auth-method>DIGEST</auth-method>

  <realm-name>MyRealmName</realm-name>  
</login-config>

现在,让我们在 JBoss 中创建一个新的安全域,以便我们可以管理 DIGEST 认证机制。在 JBOSS_HOME/standalone/configuration/standalone.xml 文件的 <security-domains> 部分,让我们添加以下条目:

<security-domain name="domainDigest" cache-type="default"> <authentication>
    <login-module code="UsersRoles" flag="required"> <module-option name="usersProperties" value="${jboss.server.config.dir}/application-users.properties"/> <module-option name="rolesProperties" value="${jboss.server.config.dir}/application-roles.properties"/> <module-option name="hashAlgorithm" value="MD5"/> <module-option name="hashEncoding" value="RFC2617"/>
      <module-option name="hashUserPassword" value="false"/>
      <module-option name="hashStorePassword" value="true"/>
      <module-option name="passwordIsA1Hash" value="true"/> 
      <module-option name="storeDigestCallback" value="org.jboss.security.auth.callback.RFC2617Digest"/> </login-module>
  </authentication>
</security-domain>

最后,在应用中,请将文件 jboss-web.xml 中的安全域名进行更改,如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
  <security-domain>java:/jaas/domainDigest</security-domain>
</jboss-web>

我们将在web.xml文件中将认证方法从BASIC更改为DIGEST。同时,我们将输入安全域的名称。所有这些更改都必须在以下方式中应用于login-config标签:

<login-config>
  <auth-method>DIGEST</auth-method>
  <realm-name>MyRealmName</realm-name
</login-config>

现在,重新启动应用程序服务器,并在 JBoss 上重新部署应用程序。为此,请在终端命令行中执行以下命令:

mvn jboss-as:redeploy

让我们启用通过 Wireshark 捕获流量,并再次使用 SOAP UI 测试 Web 服务。首先,我们应该将字段Authentication Type从全局 HTTP 设置更改为SPNEGO/Kerberos。一个非常实用的技巧是告诉 SOAP UI 不要使用基本认证方法。一旦我们执行请求,Wireshark 将告诉我们以下截图中的消息:

带有解释的示例

如截图所示,让我们首先确认在认证方法中执行了之前描述的所有步骤。让我们使用 Wireshark 的No字段进行跟踪:

在第 5 步,执行请求。

在第 7 步,服务器返回一个带有生成的nonce值的错误消息代码 HTTP 401。nonce值有助于防止重放攻击。

在第 9 步,再次执行请求。这次,包含了用于认证所需的信息,并且所有这些信息都按照我们之前描述的方式加密。

最后,在第 11 步,我们获得了表示请求已成功执行的响应。

如你所注意到的,这是一种更安全的认证方法,主要在不需要通过 TLS/SSL 加密的全传输安全开销时使用。

你可以在以下 URL 找到本章的源代码:

github.com/restful-java-web-services-security/source-code/tree/master/chapter02/digest-authentication

证书认证

这是一个通过证书在服务器和客户端之间建立信任协议的机制。它们必须由一个为确保提供的用于认证的证书是合法的机构签署,这被称为 CA。

让我们想象一个使用这种安全机制的程序。当客户端尝试访问受保护资源时,它不是提供用户名或密码,而是向服务器展示证书。这是包含用于认证的用户信息的证书;换句话说,除了唯一的私钥和公钥对之外,还有凭证。服务器通过 CA 确定用户是否合法。然后,它验证用户是否有权访问资源。此外,你应该知道,这种认证机制必须使用 HTTPS 作为通信协议,因为我们没有安全的通道,任何人都可以窃取客户端的身份。

现在,我们将展示如何在我们的示例中完成这项操作。

在我们的示例中,我们将自己变成 CA;它们通常是像 VERISIGN 这样的公司。然而,由于我们想为您节省费用,我们将这样做。我们首先需要为 CA(即我们自己)生成一个密钥,并将为应用程序服务器和用户签名证书。由于本书的目的是解释这种方法是如何工作的,而不是如何生成证书,因此我们不会包括生成证书所需的所有步骤,但我们将它们包含在以下 GitHub 链接中:

github.com/restful-java-web-services-security/source-code/tree/master/chapter02/client-cert-authentication

好的,让我们开始。首先,将 server.keystoreserver.truststore 文件复制到文件夹目录 JBOSS_HOME/standalone/configuration/。您可以使用以下链接从 GitHub 下载这些文件:

github.com/restful-java-web-services-security/source-code/tree/master/chapter02/client-cert-authentication/certificates

现在,正如我们之前提到的,这种安全机制要求我们的应用程序服务器使用 HTTPS 作为通信协议。因此,我们必须启用 HTTPS。让我们在 standalone.xml 文件中添加一个连接器;查找以下行:

<connector name="http"

添加以下代码块:

<connector name="https" protocol="HTTP/1.1" scheme="https" socket-binding="https" secure="true">
  <ssl password="changeit" 
certificate-key-file="${jboss.server.config.dir}/server.keystore" 
verify-client="want" 
ca-certificate-file="${jboss.server.config.dir}/server.truststore"/>

</connector>

接下来,我们添加安全域,如下所示:

<security-domain name="RequireCertificateDomain">
                    <authentication>
    <login-module code="CertificateRoles" flag="required">
                            <module-option name="securityDomain" value="RequireCertificateDomain"/>
                            <module-option name="verifier" value="org.jboss.security.auth.certs.AnyCertVerifier"/>
                            <module-option name="usersProperties" value="${jboss.server.config.dir}/my-users.properties"/>
                            <module-option name="rolesProperties" value="${jboss.server.config.dir}/my-roles.properties"/>
                        </login-module>
  </authentication>
  <jsse keystore-password="changeit" keystore-url="file:${jboss.server.config.dir}/server.keystore" 
                        truststore-password="changeit" truststore-url="file:${jboss.server.config.dir}/server.truststore"/>
                </security-domain>

如您所见,我们需要两个文件:my-users.propertiesmy-roles.properties;这两个文件都是空的,位于 JBOSS_HOME/standalone/configuration 路径。

我们将在 web.xml 文件中按照以下方式添加 <user-data-constraint> 标签:

<security-constraint>
...<user-data-constraint>

  <transport-guarantee>CONFIDENTIAL</transport-guarantee>
  </user-data-constraint>
</security-constraint>

然后,将身份验证方法更改为 CLIENT-CERT,如下所示:

  <login-config>
    <auth-method>CLIENT-CERT</auth-method>
  </login-config>

最后,按照以下方式更改 jboss-web.xml 文件中的安全域:

<?xml version="1.0" encoding="UTF-8"?>
<jboss-web>
  <security-domain>RequireCertificateDomain</security-domain>
</jboss-web>

现在,重新启动应用程序服务器,并使用 Maven 以下命令重新部署应用程序:

mvn jboss-as:redeploy

为了测试这种身份验证方法,我们首先必须在 SOAP UI 中进行一些配置。首先,让我们转到安装目录,找到文件 vmoptions.txt,并添加以下行:

-Dsun.security.ssl.allowUnsafeRenegotiation=true

现在,我们将更改 SOAP UI 的 SSL 设置。为此,您必须从主菜单导航到 文件 | 首选项

从弹出窗口中,选择 SSL 设置 选项卡并输入以下截图所示的值:

通过证书进行身份验证

KeyStore 是您应该已将 .pfx 文件复制到的位置。请注意 KeyStore 密码changeit 并检查 需要客户端身份验证 选项。

现在,我们将测试我们刚才所做的修改;因此,让我们启用流量分析器并再次使用 SOAP UI 执行请求。Wireshark 将显示以下截图中的信息:

通过证书进行认证

如您所见,所有信息都已加密,无法被解读。因此,如果数据包在网络中传输并被截获,信息就不会受到攻击。

您可以在以下 URL 找到本节的源代码:

github.com/restful-java-web-services-security/source-code/tree/master/chapter02/client-cert-authentication/resteasy-examples

API 密钥

随着云计算的出现,想象将许多云中可用的应用程序集成的应用程序并不困难。现在,很容易看到应用程序是如何与 Flickr、Facebook、Twitter、Tumblr 等互动的。

为了启用这些集成,已经开发了一种新的认证机制,使用 API 密钥。这种方法主要用于我们需要从另一个应用程序进行认证,但我们不想访问另一个应用程序中托管的私有用户数据时。相反,如果您想访问这些信息,您必须使用 OAuth。如果您对此感兴趣,请不要担心,我们将在本书的后面部分研究这项神奇的技术。

我们想了解 API 密钥是如何工作的,所以让我们以 Flickr 为例。这里重要的是要理解 API 密钥是如何工作的,因为同样的概念可以应用于像 Google、Facebook 这样的公司。对于那些不熟悉 Flickr 的人来说,它是一个云应用程序,我们可以在这里存储我们的照片、图像、屏幕截图或类似文件。

要开始使用这种认证模型,我们首先获取一个 API 密钥;在我们的 Flickr 示例中,您可以使用以下链接来完成此操作:

www.flickr.com/services/developer/api/

当我们请求我们的 API 密钥时,我们需要输入将要创建的应用程序的名称以及我们将使用该 API 密钥的方式。一旦我们输入所需的信息并提交,Flickr 将向我们提供几个值;它们是一个秘密和一个密钥。这两个值在下面的屏幕截图中显示:

API 密钥

我们创建的每个应用程序都是 Flickr App Garden 的一部分。App Garden 不过是所有 Flickr 成员创建的所有应用程序的集合。

请记住,在创建 API 密钥时,我们是有意识地接受提供者的某些使用条款。这些条款清楚地说明了我们可以做什么以及不能做什么;例如,Flickr 表示:

a. 您必须:

请遵守 Flickr 社区指南www.flickr.com/guidelines.gne,Flickr 的使用条款www.flickr.com/terms.gne,以及 Yahoo!服务条款docs.yahoo.com/info/terms/

b. 您不得:

在所有旨在复制或尝试取代 Flickr.com 基本用户体验的应用程序中使用 Flickr API

因此,通过要求用户接受使用条款,API 密钥提供者防止了其 API 的滥用。因此,如果有人开始不尊重协议,提供者将收回 API 密钥。Flickr 有一套大量的方法,我们可以在我们的应用程序中使用;我们将尝试其中之一来展示它们是如何工作的:

flickr.photos.getRecent方法列出了 Flickr 上最近发布的所有照片,我们可以按照以下方式调用它:

https://www.flickr.com/services/rest?method=flickr.photos.getRecent&;&api+key=[your_api_key_from_flicker]

让我们使用我们刚才生成的密钥,并使用浏览器执行以下请求:

API 密钥

首先,注意信息是如何通过安全通道(HTTPS)传输的。然后,在收到请求后,Flickr 通过读取属于用户的 API 密钥与密钥对应的秘密密钥中的信息来验证用户。一旦这些验证成功,服务器将响应发送给客户端。因此,我们获得了包含 Flickr 上最近发布的所有照片的响应。正如你所注意到的,通过这种方式,你可以轻松地使用提供者的 API 创建应用程序。此外,提供者将允许你进行身份验证、访问公共信息,并负责跟踪使用 API 密钥所进行的 API 调用量或数量,以确保使用符合协议。

摘要

在本章中,我们探讨了所有可能的身份验证模型。我们将在下一章使用它们,并将它们应用到我们刚刚创建的网络服务功能中。

即使你在任何示例中遇到困难,你也可以继续到下一章。为了更好地理解,我们将一步一步、更深入地探讨我们如何利用每个可用的身份验证模型。

正如你所意识到的那样,选择正确的安全管理非常重要,否则信息会暴露,并且很容易被第三方拦截和使用。

最后,在本章中,我们回顾了身份验证和授权之间的区别。这两个概念都非常重要,在安全术语的背景下绝对不能忽视。

现在,我们将邀请你加入我们,继续确保我们的网络服务安全。

第三章。使用 RESTEasy 进行安全管理

欢迎来到第三章。我们希望您正在与我们一起享受并学习。在本章中,您将更深入地参与安全管理。您还将与一些更高级的安全概念一起工作。

使用 RESTful Web 服务构建的应用程序中的安全管理可能比我们在上一章中审查的更细粒度。如果我们围绕认证和授权主题思考,我们描述了前者;授权被搁置了。这是因为我们想在本章中缓慢且非常详细地处理它。

本章涵盖的主题包括:

  • 在应用程序中实现与认证和授权相关的安全限制

  • 实现细粒度安全

  • 使用注解来获得对资源访问控制的更多粒度

细粒度和粗粒度安全

我们可以管理的有两个级别的安全:细粒度粗粒度

当我们在安全性的上下文中提到粗粒度一词时,我们指的是通常在应用程序中处理在较高层次的安全系统。例如,在第二章《确保 Web 服务的重要性》中,任何角色的用户都可以使用服务,这是一个完美的粗粒度示例,因为当安全限制允许用户访问而无需担心角色或更具体的认证用户特征时,就会使用粗粒度选项。这意味着为了让系统允许访问功能,我们只需验证用户身份;换句话说,它验证了用户。然而,在现实生活中,仅仅有一个认证的应用程序用户是不够的。还必须确保用户有权使用某些功能。我们可以通过细粒度控制来实现这一点。验证用户分配的权限以访问功能意味着使用授权控制。

为了以实际的方式展示这些概念,我们将使用上一章中创建的应用程序。您可以在以下 URL 下访问源代码,位于基本认证部分:

github.com/restful-java-web-services-security/source-code/tree/master/chapter02/basic-authentication

让我们开始吧;假设我们只想让具有administrator角色的用户能够使用我们应用程序中的功能。首先要做的事情是修改web.xml文件并添加以下约束。注意这些更改是如何以粗体显示的:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 

  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
  http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

  <security-role>
 <description>Application roles</description>
 <role-name>administrator</role-name>
  </security-role>
  <security-constraint>
    <display-name>Area secured</display-name>
    <web-resource-collection>
      <web-resource-name>protected_resources</web-resource-name>
      <url-pattern>/services/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
 <description>User with administrator role</description>
 <role-name>administrator</role-name>
    </auth-constraint>
  </security-constraint>
  <login-config>
    <auth-method>BASIC</auth-method>
  </login-config>
</web-app>

现在,让我们尝试使用我们刚刚创建的用户(username)发出请求。当您收到403 Forbidden错误时,您可能会感到惊讶。

注意,如果您尝试使用无效凭据发出请求,您将收到错误 HTTP/1.1 401 未授权。错误非常明确;访问未授权。这意味着我们发送了无效凭据,因此用户无法进行身份验证。我们刚刚收到的错误是 HTTP/1.1 403 禁止访问,这表明用户已成功登录,但没有权限使用他们所需的功能。这将在以下屏幕截图中演示:

细粒度和粗粒度安全性

现在,让我们使用 JBOSS_HOME/standalone/bin/adduser.sh 文件创建一个新的用户,并赋予其 administrator 角色。按照以下屏幕截图所示输入所需信息:

细粒度和粗粒度安全性

当我们在 SoapUI 中更改凭据时,请求的结果是成功的,如下面的屏幕截图所示:

细粒度和粗粒度安全性

如您所见,我们使用了一个额外的控制,其中我们仅限制了具有分配给他们的 administrator 角色的已认证用户;他们能够使用 Web 服务功能。在管理现实世界应用程序的安全性时,使用这类控制非常常见。由于我们已经实现了更详细的控制级别,平台为我们提供了实施更细粒度控制的机会,例如我们现在将要看到的。

保护 HTTP 方法

JAAS 的一项好处是我们甚至可以在 HTTP 方法的级别上控制。因此,我们可以实施安全控制,仅允许具有特定角色的用户使用某些方法,以方便我们;例如,一个角色用于保存信息,另一个用于删除信息,其他角色用于读取信息,等等。

要实现这类控制,我们有必要了解应用程序中 HTTP 方法的功能。在我们的例子中,我们已经知道为了保存信息,应用程序始终使用 HTTP POST 方法。同样,当我们想要读取信息时,应用程序使用 HTTP GET 方法。因此,我们将修改我们的示例,以便只有具有 administrator 角色的用户能够使用 savePerson (HTTP POST) 方法。同时,只有具有 reader 角色的用户能够使用 findById (HTTP GET) 方法来读取信息。

以此为目标,我们将按如下方式修改我们的 web.xml 文件:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
  http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
  <!-- Roles -->
  <security-role>
    <description>Role for save information</description>
    <role-name>administrator</role-name>
  </security-role>
  <security-role>
    <description>Role for read information</description>
    <role-name>reader</role-name>
  </security-role>

  <!-- Resource / Role Mapping -->
  <security-constraint>
    <display-name>Administrator area</display-name>
    <web-resource-collection>
  <web-resource-name>protected_resources</web-resource-name>
      <url-pattern>/services/*</url-pattern>
      <http-method>POST</http-method>
    </web-resource-collection>
    <auth-constraint>
    <description>User with administrator role</description>
      <role-name>administrator</role-name>
    </auth-constraint>
  </security-constraint>
  <security-constraint>
    <display-name>Reader area</display-name>
    <web-resource-collection>
  <web-resource-name>protected_resources</web-resource-name>
      <url-pattern>/services/*</url-pattern>
      <http-method>GET</http-method>
    </web-resource-collection>
    <auth-constraint>
      <description>User with reader role</description>
      <role-name>reader</role-name>
    </auth-constraint>
  </security-constraint>

  <login-config>
    <auth-method>BASIC</auth-method>
  </login-config>
</web-app>

在我们继续之前,我们必须使用 JBOSS_HOME/standalone/bin/adduser.sh 脚本创建一个新的用户(readeruser)并赋予其 reader 角色。

现在,让我们使用 SoapUI 测试角色及其权限。

HTTP 方法 – POST

我们将使用没有所需权限的角色来测试 POST 方法。您将看到权限错误信息。

角色:Reader

当使用此角色时,此方法不允许。这将在以下屏幕截图中演示:

HTTP 方法 – POST

角色:管理员

使用这个角色,你可以成功执行方法。这在下述屏幕截图中有演示:

HTTP 方法 – POST

HTTP 方法 – GET

现在,我们将使用具有使用 GET 方法所需权限的用户。使用这个角色执行应该是成功的。

角色:阅读者

现在,使用这个角色执行是成功的。这在下述屏幕截图中有演示:

HTTP 方法 – GET

角色:管理员

管理员角色没有访问这个方法的权限。这在下述屏幕截图中有演示:

HTTP 方法 – GET

可以将相同的角色考虑用于 URL 模式。在我们的例子中,我们在/services/*模式上应用了限制。然而,你可以在更深的级别应用它,例如/services/person/*。我们的意思是,如果我们有另一个在 URL/services/other-service/下公开的服务,我们可以设置它,使得一个角色可以访问路径/services/person/*下的服务,并在路径/services/other-service/*下有不同的访问级别。这个例子相当简单,并被作为基本示例提供给读者。

在应用所有更改后,我们在web.xml文件中列出的所有方法上设置了安全。然而,我们必须问自己一个问题;那些未被包括的方法会发生什么?

OWASP(开放 Web 应用程序安全项目),一个致力于发现和修复软件中安全漏洞的非营利组织,已经就这个问题写了一篇论文,其名称如下:

通过 HTTP 动词篡改绕过 Web 身份验证和授权:如何无意中允许攻击者完全访问你的 Web 应用程序。

如果你想查看完整的文档,你可以通过访问以下链接来完成:

dl.packetstormsecurity.net/papers/web/Bypassing_VBAAC_with_HTTP_Verb_Tampering.pdf

OWASP 在上述文档中所描述的是简单的。它表明,如果我们不采取某些预防措施,JEE 会在web.xml配置文件中暴露潜在的安全漏洞,因为文件中未列出的所有方法都可以无限制地使用。这意味着一个未在应用程序中认证的用户可以调用任何其他 HTTP 方法。

OWASP 在早先的文章中陈述了以下内容:

不幸的是,几乎所有这种机制的实现都在一个意外和不安全的战争中运行。它们不是拒绝规则中未指定的方法,而是允许任何未列出的方法。讽刺的是,通过在规则中列出具体方法,开发者实际上允许比他们意图更多的访问。

为了更好地理解这一点,让我们用一个类比来关注。

假设您有一个用于编写书籍的 Web 应用程序,该应用程序处理两个角色——一个用于能够编写书籍页面的作者,另一个用于只能阅读书籍并添加带注释的笔记的审阅者。现在,假设一个用户不小心得到了您应用程序的 URL。这个用户没有任何凭证可以提供,显然,用户甚至不应该能够访问应用程序。然而,OWASP 展示的问题实际上并不是做看似明显的事情,而是实际上允许具有足够权限执行任何操作(如删除)的未认证用户访问应用程序。

让我们通过一个例子来看看这个不便之处,然后我们将实施 OWASP 的建议来解决它。

让我们在 PersonService 类中创建一个新的方法;这次我们将使用 web.xml 文件中未列出的一种方法。最常用的方法之一是 HTTP DELETE;它的功能是使用其 ID 从内存中删除一个条目。这将把记录的 ID 作为参数放在 URL 中,因此请求的 URL 将看起来像以下这样:

http://localhost:8080/resteasy-examples/services/person/[ID]

方法实现应该看起来像以下这样:

@DELETE
@Path("{id}")
public Response delete(@PathParam("id") int id) {
  Person person = dataInMemory.get(id);
if (person == null) {
  // There is no person with this ID
throw new WebApplicationException(Response.Status.NOT_FOUND);
  }
  dataInMemory.remove(id);
  return Response.status(Status.GONE).build();
}

为了测试该方法,我们首先必须通过 SoapUI 创建几个注册项,同样使用 HTTP POST 方法和一个如下所示的字符串:

<person><name>Rene</name><lastname>Enriquez</lastname></person>

现在,在 SoapUI 中选择 DELETE 方法,移除我们用于身份验证的凭证信息,并使用其中一个项目 ID 执行请求,如下面的截图所示:

HTTP 方法 – GET

如您所见,该条目已被移除,服务器返回了消息 HTTP/1.1 410 Gone。这表明资源已不再可用。正如您所注意到的,当我们没有指定该方法默认应该受到保护时,它会被标记为可用。在我们的例子中,任何不需要进行身份验证的用户都可以删除我们的应用程序资源。

为了克服这个缺点,OWASP 建议在 web.xml 文件中添加另一个安全约束。这个新的安全约束不应该在其内部列出任何 HTTP 方法,这意味着拒绝所有 HTTP 方法的访问,如下面的代码所示:

<security-constraint>
  <display-name>For any user</display-name>
  <web-resource-collection>
  <web-resource-name>protected_resources</web-resource-name>
    <url-pattern>/services/*</url-pattern>
  </web-resource-collection>
  <auth-constraint>
    <description>User with any role</description>
    <role-name>*</role-name>
  </auth-constraint>
</security-constraint> 

此外,我们还需要添加一个新的角色,以便在应用程序中确定已认证的用户,如下面的代码所示:

<security-role>
    <description>Any role</description>
    <role-name>*</role-name>
  </security-role>

现在,我们从 SoapUI 运行请求,我们可以看到错误消息 HTTP/1.1 401 Unauthorized。这表明您无法执行请求,因为用户尚未认证,这反过来意味着未认证的用户不能使用 DELETE 或任何其他方法。

通过注解实现细粒度安全实现

The web.xml file, the file that allows all security settings, is not the only way in which you can achieve fine-grained security implementation; the platform also offers the possibility of using annotations for security checks. To do this, there are three options that can be chosen depending on your needs, listed as follows:

  • @RolesAllowed

  • @DenyAll

  • @PermitAll

The @RolesAllowed annotation

The @RolesAllowed annotation can be applied at the method or class level. With this annotation, you can define a set of roles that are allowed to use the annotated resource. As a parameter annotation, let's write all allowed roles. For this example, we will modify our web.xml file as follows:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 

xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
  http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
  <!-- Roles -->
 <context-param>
 <param-name>resteasy.role.based.security</param-name>
 <param-value>true</param-value>
 </context-param>
  <security-role>
    <description>Any role</description>
    <role-name>*</role-name>
  </security-role>
  <!-- Resource / Role Mapping -->
  <security-constraint>
  <display-name>Area for authenticated users</display-name>
    <web-resource-collection>
  <web-resource-name>protected_resources</web-resource-name>
      <url-pattern>/services/*</url-pattern>
    </web-resource-collection>
    <auth-constraint>
      <description>User with any role</description>
      <role-name>*</role-name>
    </auth-constraint>
  </security-constraint>
  <login-config>
    <auth-method>BASIC</auth-method>
  </login-config>
</web-app>

PersonService 类中,让我们在每个我们希望能够执行方法的角色上使用注解,如下所示:

  @RolesAllowed({ "reader", "administrator" })
  @POST
  @Consumes("application/xml")
  public Response savePerson(Person person) {...

  @RolesAllowed({ "administrator" })
  @GET
  @Path("{id}")
  @Produces("application/xml")
  public Person findById(@PathParam("id") int id) {...

现在是时候通过 SoapUI 进行测试了。

The savePerson method

现在,我们将使用管理员角色测试 PersonService 类的 savePerson 方法,如下截图所示:

The savePerson method

执行成功,如前一个截图所示。原因是我们在 @RolesAllowed 注解中包括了这两个角色。此外,我们还将使用 reader 角色进行测试,以确保执行成功,如下截图所示:

The savePerson method

如您所见,当我们使用 @RolesAllowed 注解时,我们授予了特定角色的权限。对于此方法,我们使用了 administratorreader

The findById method

我们现在将使用管理员角色测试 findById 方法,如下截图所示:

The findById method

截图显示执行成功,因为 @RolesAllowed 注解包括了管理员。由于我们没有包括 reader 角色,下一次执行不应被授权。现在让我们立即进行测试,如下截图所示:

The findById method

再次强调,我们使用了 @RolesAllowed 注解在方法级别上授予权限,但这次我们只指定了一个角色,administrator

本章的所有源代码都可以在以下网址找到:

github.com/restful-java-web-services-security/source-code/tree/master/chapter03

The @DenyAll annotation

The @DenyAll annotation allows us to define operations that cannot be invoked regardless of whether the user is authenticated or the roles are related to the user. The specification defines this annotation as follows:

指定不允许任何安全角色调用指定的方法(即方法应从 J2EE 容器中排除执行)。

The @PermitAll annotation

当我们使用@PermitAll注解时,我们告诉容器,被注解的资源(一个方法或类的所有方法)可以被任何已登录到应用程序的用户调用。这意味着只需要用户进行身份验证;不需要分配任何特定角色。

从这三个注解中,无疑最常用的是第一个(@RolesAllowed);其他注解不常使用,因为@PermitAll可以很容易地在web.xml文件中替换,而@DenyAll只能在少数场景中使用。

精细粒度安全性的程序实现

除了提供我们已看到的选项用于安全管理之外,RESTEasy 还以编程方式提供了一种额外的访问控制机制。

在 Web 服务的操作中,你可以向方法添加一个额外的参数。这允许访问安全上下文,而不会改变客户端调用方法或方法执行的动作。参数必须以以下方式包含:

@GET...
@Consumes("text/xml")
public returnType methodName(@Context SecurityContext secContext, …) {...

假设在我们这个例子中,在savePerson方法中,我们想要访问这个功能。我们需要的唯一更改如下代码片段所示。

之前,该方法只使用一个参数,如下所示代码所示:

@POST
@Consumes("application/xml")
public Response savePerson(Person person) {
  int id = dataInMemory.size() + 1;
  person.setId(id);
  dataInMemory.put(id, person);
  return Response.created(URI.create("/person/" + id)).build();
}

现在,该方法有另一个参数,如下所示代码所示:

@POST
@Consumes("application/xml")
public Response savePerson(@Context SecurityContext secContext, Person person) {
  int id = dataInMemory.size() + 1;
  person.setId(id);
  dataInMemory.put(id, person);
  return Response.created(URI.create("/person/" + id)).build();
}

接口javax.ws.rs.core.SecurityContext提供了以下三个有趣的功能:

  • isUserInRole()

  • getUserPrincipal()

  • isSecure()

方法isUserInRole()的功能与注解@RolesAllowed类似;其目标是执行检查以确定登录用户是否属于指定的角色,如下所示:

@POST
@Consumes("application/xml")
public Response savePerson(@Context SecurityContext secContext, Person person) {
  boolean isInDesiredRole = 	secContext.isUserInRole ("NameOfDesiredRole");
  int id = dataInMemory.size() + 1;
  person.setId(id);
  dataInMemory.put(id, person);
  return Response.created(URI.create("/person/" + id)).build();
}

getUserPrincipal()方法获取应用程序中的主要用户,换句话说,就是登录用户。你可以通过此用户获取代表它的信息,如用户名;这在需要生成审计跟踪的场景中总是很有用。

最后,方法isSecure()确定调用是否是通过安全的通信方式进行的,例如你是否正在使用 HTTPS。

如你所知,HTTP 和 HTTPS 是交换信息的协议;前者通常用于分享不敏感信息时,后者通常用于信息敏感且我们需要安全通道时。

让我们想象一下 ABC 银行的门户网站,特别是显示有关服务和与银行业务相关的信息的主页,这些信息可以通过 HTTP 进行管理。我们无法使用 HTTP 协议管理与账户或货币转账相关的网页;这是因为信息没有得到保护。通过 HTTPS 协议,我们可以加密信息;当信息被像 Wireshark 这样的流量分析器截获时,它无法被解释。

您可以通过将更改应用到项目中以启用 HTTPS 来测试此功能,正如我们在第二章确保 Web 服务安全的重要性中向您展示的那样。

当您使用 HTTP 调用此方法时,结果将是错误的,但使用 HTTPS 调用相同方法时,它将是正确的。

我们刚才分析的三种方法在我们想要实现细粒度安全检查时非常有用。例如,当我们想要实现审计时,我们可以确定是否使用安全的传输协议(如 HTTPS)执行了某个操作;此外,我们还可以发现有关执行操作的用户的信息。

摘要

在实现应用程序安全时,我们的需求可能相当多样化。在本章中,我们看到了 JAX-RS 如何提供处理安全的机制,从相当基本的模型(粗粒度)到更复杂的模型(细粒度),在细粒度模型中,您可以执行更彻底的控制,包括程序性控制和通过配置文件的控制。

当然,始终建议将这些检查保留在配置文件,如web.xml中。由于您将控制集中在一个地方,这有助于维护。当在源代码级别处理安全时,这种情况不会发生,因为当有许多类是项目的一部分时,如果需要对当前功能进行某种形式的修改,任务会变得复杂。

现在,您应该为下一章做好准备,我们将讨论 OAuth。这是一个非常激动人心的主题,因为这个协议在互联网应用程序中被广泛接受和使用。全球互联网的摇滚明星公司,如 Google、Twitter 和 Facebook 等,都取得了巨大的成功。

第四章:RESTEasy Skeleton Key

欢迎来到第四章!我们希望你在享受这本书,更重要的是,在学习和理解我们所传达和教授的内容。现在是时候前进并沉浸在新的一章中。

一旦阅读了本章,你将具备设计、实现和聚合额外的安全级别到你的 RESTEasy 应用程序中的知识,这一切都使用 OAuth 和 RESTEasy Skeleton Key 以及这些技术的某些特定要求,例如设置 OAuth 服务器。你将通过实际和描述性的应用程序示例来学习,就像我们在前面的章节中所做的那样;我们不会只停留在理论上,而是会实现应用程序并解释实现 OAuth 的具体方法和类。

在本章中,你将了解以下主题:

  • OAuth 和 RESTEasy

  • 安全管理的 SSO 配置

  • 访问令牌

  • 自定义过滤器

  • 测试用的 Web 服务客户端

如你可能已经体验过的,如果你在一个或几个社交网络上拥有账户,许多这些社交网络允许你在它们之间共享信息或在所有它们上发布内容。这是一个迹象表明应用程序需要共享信息,并且也使用其他应用程序中的资源。在这个例子中,它可以是你的账户或你的联系名单。这涉及到敏感信息,因此需要保护。此外,对资源的有限权限意味着第三方应用程序只能读取你的联系名单。这为应用程序中一个非常重要、吸引人和有用的功能打开了大门,即代表用户使用资源的容量。当然,你可能想知道后者如何授权使用?好吧,本章将向你展示。那么,让我们开始吧!

OAuth 协议

这是一个开放协议,允许你从一个站点(服务提供者)安全地授权你的私有资源到另一个站点(消费者),而不必分享你的身份。

一个实际的例子是当你授权一个网站或应用程序使用你手机或社交网络中的联系名单。

OAuth 和 RESTEasy Skeleton Key

在本节中,我们将回顾与 OAuth 作为认证框架、RESTEasy Skeleton Key 以及它们如何协同工作相关的一些概念。你将检查这些技术的某些功能,并通过一些实际示例的代码来亲自动手。

什么是 RESTEasy Skeleton Key?

RESTEasy Skeleton Key 为浏览器和 JAX-RS 客户端提供了一种统一的方式来确保安全。这允许在应用程序和服务网络中安全且可扩展地执行和转发请求,而无需在每次出现请求时与中央认证服务器交互。

OAuth 2.0 认证框架

这使得第三方应用程序或服务能够代表资源所有者访问 HTTP 资源。它还防止第三方应用程序或服务与所有者的凭证取得联系。这是通过通过浏览器颁发访问令牌和使用直接授权来实现的。

简要解释了这两个概念之后,现在是时候描述它们是如何相互关联的了。RESTEasy 骨架密钥是一个 OAuth 2.0 实现,它使用 JBoss AS 7 安全基础设施来保护 Web 应用程序和 RESTful 服务。

这意味着你可以将 Web 应用程序转换为 OAuth 2.0 访问令牌提供者,你还可以将 JBoss AS 7 安全域转换为中央认证和授权服务器,在那里应用程序和服务可以相互交互。

下面的图表以更好的方式描述了此过程:

OAuth 2.0 认证框架

主要功能

我们希望帮助你理解这些技术并阐明它们的使用目的;这就是为什么我们将命名它们的一些主要功能。通过 OAuth 2.0 和 RESTEasy 骨架密钥,你可以执行以下功能:

  • 将基于 servlet 表单认证的 Web 应用程序转换为 OAuth 2.0 提供者。

  • 在中央认证服务器上提供分布式单点登录(SSO),以便一次登录即可安全地访问域中配置的任何基于浏览器的应用程序。

  • 只用一个链接即可从所有配置了单点登录(SSO)的分布式应用程序中注销。

  • 使用访问令牌使 Web 应用程序与远程 RESTful 服务交互。

  • 使用 OAuth 2.0 对签名访问令牌,并在以后使用这些令牌访问域中配置的任何服务。令牌具有身份和角色映射,并且由于令牌是数字签名的,因此无需在每次请求时都过载中央认证服务器。

你可以在docs.jboss.org/resteasy/docs/3.0-beta-2/userguide/html/oauth2.html找到更多关于这些主题的信息。

我们将讨论最重要的部分,但这可能对你有所帮助。

OAuth2 实现

我们刚刚回顾了本章将要处理的一些主要概念,但这还远远不够。我们必须实现一个描述性示例,以便我们能够完全理解这些主题。

在 JBoss 中更新 RESTEasy 模块

为了不干扰您的 JBoss 配置或其他任何东西,我们将使用另一个全新的 JBoss 实例。我们必须更新一些与 RESTEasy 相关的模块。我们可以非常容易地做到这一点。让我们访问链接 resteasy.jboss.org/;在您的右侧,您将找到一个标题为 Useful Links 的面板,其中有一个下载链接。点击它以访问另一个页面,该页面包含许多下载链接。在这个示例中,我们使用 3.0.7.Final 版本。下载此版本以继续前进。

下载并解压后,您将找到一个名为 resteasy-jboss-modules-3.0.7.Final 的另一个 .zip 文件;此文件包含一些 JAR 文件,这些文件将更新您的 JBoss 模块。因此,解压它,将所有文件夹复制到 JBOSS_HOME/modules/ 中,并替换所有冲突项。最后一步:我们必须更新 JAR 文件的版本并修改 JBoss 中的模块 XML,以便将 org.apache.httpcomponents 设置为使用 httpclient-4.2.1.jarhttpcore-4.2.1.jarhttpmime-4.2.1.jar,因为当前最新版本是 4.3.4,这也同样可以正常工作。因此,复制这些 JAR 文件并在 JBOSS_HOME/modules/org/apache 文件夹中的 module.xml 文件中更新版本。现在,我们已经更新了我们的模块以支持 RESTEasy。

在 JBoss 中设置配置

为了使我们的 JBoss 为我们的示例做好准备的下一步,我们必须前往 github.com/restful-java-web-services-security/source-code/tree/master/chapter04 并下载 chapter04 示例 zip 文件。解压后,您将找到一个名为 configuration 的文件夹。这个文件夹包含设置我们的 JBoss 配置所需的文件。因此,复制这些文件并替换位于 JBOSS_HOME/standalone/configuration 的 JBoss 中的配置文件夹。

实现 OAuth 客户端

为了开发这个示例,我们研究了一个非常有用的示例并将其应用于一个新的项目。这个示例由几个项目组成;每个项目将生成一个 WAR 文件。这个示例的目的是演示 OAuth 的工作原理,并解释您如何在技术层面上实现这项技术。因此,我们将模拟几个事物来创建一个可以应用此实现的环境。完整的代码可以从以下链接下载:

github.com/restful-java-web-services-security/source-code/tree/master/chapter04/oauth2-as7-example

oauth-client 项目

首先,我们将创建 oauth-client webapp 项目。您可以使用我们在前几章中使用的 Maven 命令,或者您可以使用 Eclipse IDE 来执行此操作。

然后,让我们添加一些依赖项以实现我们的客户端。这些依赖项适用于所有项目。转到pom.xml文件,并确保在<dependencies>标签内添加以下依赖项:

       <dependency>
            <groupId>org.jboss.spec.javax.servlet</groupId>
            <artifactId>jboss-servlet-api_3.0_spec</artifactId>
            <version>1.0.1.Final</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-client</artifactId>
            <version>3.0.6.Final</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>skeleton-key-core</artifactId>
            <version>3.0.6.Final</version>
            <scope>provided</scope>
        </dependency>

让我们从创建包com.packtpub.resteasy.example.oauth开始。然后,创建类public class Loader implements ServletContextListener,它实现了ServletContextListener接口,因为我们将会加载一个密钥存储库并初始化上下文。

让我们在我们的类private ServletOAuthClient oauthClient中添加一个字段,它将代表我们的 OAuth 客户端对象。

然后,让我们创建以下代码片段所示的方法:

private static KeyStore loadKeyStore(String filename, String password) throws Exception 
{
KeyStore keyStore = KeyStore.getInstance(KeyStore.getDefaultType());
File keyStoreFile = new File(filename);
FileInputStream keyStoreStream = new FileInputStream(keyStoreFile);
    keyStore.load(keyStoreStream, password.toCharArray());
    keyStoreStream.close();
    return keyStore; 
}

此方法接收两个参数,文件名和密码,并创建KeyStore对象。它还从接收到的文件名创建一个FileInputStream对象,以便它可以使用它来加载KeyStore对象,并且它使用接收到的密码作为字符数组。

然后,由于我们的类实现了ServletContextListener接口,我们必须覆盖一些方法。第一个要覆盖的方法是contextInitialized。让我们这样做:

@Override
 public void contextInitialized(ServletContextEvent sce) {
  String truststoreKSPath = "${jboss.server.config.dir}/client-truststore.ts";
  String truststoreKSPassword = "changeit";
  truststoreKSPath = EnvUtil.replace(truststoreKSPath);
  try {
   KeyStore truststoreKS = loadKeyStore(truststoreKSPath, 
     truststoreKSPassword);
   oauthClient = new ServletOAuthClient();
   oauthClient.setTruststore(truststoreKS);
   oauthClient.setClientId("third-party");
   oauthClient.setPassword("changeit");
   oauthClient.setAuthUrl("https://localhost:8443/oauth-server/login.jsp");
   oauthClient.setCodeUrl("https://localhost:8443/oauth-server/
     j_oauth_resolve_access_code");
   oauthClient.start();
   sce.getServletContext().setAttribute(ServletOAuthClient.class.getName(), oauthClient);
  } catch (Exception e) {
   throw new RuntimeException(e);
  }

 }

通过这个方法,我们将完成几件事情。正如你所看到的,我们设置了两个内部变量;一个是设置到我们的client-truststore.ts文件路径,另一个是设置密码。确保将文件粘贴到我们在变量中指定的路径(JBOSS_HOME/standalone/configuration)。

然后,我们使用在变量中指定的路径和密码加载KeyStore对象,通过这种方式获取另一个KeyStore对象。

现在,是时候实例化和设置我们的 OAuth 客户端对象的属性了。在之前的代码中,我们设置了以下属性:trustStoreclientIdpasswordauthUrlcodeUrl

最后,我们创建客户端以从代码中获取访问令牌。为了完成这个任务,我们使用start()方法。同时,我们使用我们刚刚创建的 OAuth 客户端对象设置属性 servlet OAuth 客户端。

为了完成我们的 OAuth 客户端,我们需要覆盖一个名为public void contextDestroyed(ServletContextEvent sce)的第二个方法,如下面的代码所示:

@Override
  public void contextDestroyed(ServletContextEvent sce) {
    oauthClient.stop();
  }

当 servlet 上下文即将关闭、我们的应用程序正在重新部署等情况发生时,此方法将执行。该方法关闭客户端实例及其所有相关资源。

我们为我们的示例实现了 OAuth 客户端。我们需要另一个资源。这次,我们将创建一个类,作为我们的光盘存储库的数据库客户端。所以,让我们称它为CompactDiscsDatabaseClient,并且我们将获取以下两个方法:

  • public static void redirect(HttpServletRequest request, HttpServletResponse response)

  • public static List<String> getCompactDiscs(HttpServletRequest request)

因此,让我们开始实现第一个方法。这个方法解释如下:

public static void redirect(HttpServletRequest request, HttpServletResponse response) {
ServletOAuthClient oAuthClient = (ServletOAuthClient) request.getServletContext().getAttribute(ServletOAuthClient.class.getName());
    try {
oAuthClient.redirectRelative("discList.jsp", request, response);
    } catch (IOException e) {
      throw new RuntimeException(e);
    }
  }

在前面的方法中,我们从请求中获取的ServletContext对象中获取ServletOAuthClient对象;servlet OAuth 客户端作为名为ServletOAuthClient的属性存在于 servlet 上下文中。记住,在我们创建的第一个类中,我们在 servlet 上下文中设置了此属性。

最后,我们通过通过redirectRelative (String relativePath, HttpServletRequest request, HttpServletResponse response)方法将浏览器重定向到认证服务器来启动获取访问令牌的过程。

现在,让我们继续使用下一个方法来加载光盘。以下代码表示该方法:

public static List<String> getCompactDiscs(HttpServletRequest request) {

ServletOAuthClient oAuthClient = (ServletOAuthClient) request.getServletContext().getAttribute(
        ServletOAuthClient.class.getName());

ResteasyClient rsClient = new 
ResteasyClientBuilder().trustStore(oAuthClient.getTruststore()).hostnameVerification(ResteasyClientBuilder.HostnameVerificationPolicy.ANY).build();

String urlDiscs = "https://localhost:8443/store/discs";
  try {
String bearerToken = "Bearer" + oAuthClient.getBearerToken(request);

Response response = rsClient.target(urlDiscs).request().header(HttpHeaders.AUTHORIZATION, bearerToken)
          .get();
    return response.readEntity(new GenericType<List<String>>() {
	      });
    } finally {
      rsClient.close();
    }
}

让我们检查一下这里的情况。在前面的getCompactDiscs()方法中,我们创建了一个ServletOAuthClient对象,该对象负责通过将浏览器重定向到认证服务器来启动获取访问令牌的过程。再次,我们从请求的ServletContext对象中获取属性。然后,我们使用一个新的ResteasyClientBuilder()实例创建一个ResteasyClient对象;这个类是创建客户端和允许 SSL 配置的抽象。

我们随后使用trustStore()方法设置客户端信任存储库。这个调用将返回一个KeyStore对象并设置客户端信任存储库。之后,我们调用hostnameVerification()方法,该方法设置用于验证主机名的 SSL 策略。最后,通过使用build()方法,我们构建一个新的客户端实例,该实例包含在此客户端构建器中先前指定的整个配置。这将返回一个ResteasyClient实例。

让我们继续创建一个内部变量,该变量将保存我们将设置为我们的目标资源的资源 URL。同时,我们还将创建另一个内部变量来保存作为字符串的 bearer 令牌。这个字符串将由单词Bearer后跟 servlet OAuth 客户端和请求中的 bearer 令牌组成。

现在,为了创建响应,我们将使用我们刚刚创建的 servlet OAuth 客户端。让我们使用变量urlDiscs作为参数,并通过target()方法创建一个新的网络资源目标。之后,使用request()方法,我们设置对刚刚设置的目标网络资源的请求。

最后,我们通过调用header()方法添加一个头,该方法将接收两个参数:第一个参数代表头的名称,第二个参数是头的值。之后,我们调用当前请求的HTTP GET方法。

为了澄清,HttpHeaders.AUTHORIZATION常量代表当用户想要通过添加一个授权请求头字段与服务器进行身份验证时的特定情况。它是通过在请求中添加一个授权请求头字段来完成的。另一方面,授权字段值由包含请求资源的领域内用户身份验证信息的凭据组成。

一旦创建响应对象,我们使用 readEntity() 方法读取消息实体输入流作为指定 Java 类型的实例。这样,我们就可以用我们的光盘示例列表填充列表,以便在网页中展示。这意味着我们已经访问了资源。

如果您想了解更多关于我们在描述的代码块中使用的代码,这里有一些链接作为参考。您可以查看它们,扩展您的知识,并获取更多关于 RestEasyClientRestEasyClientBuilder 的详细信息。

光盘存储项目

我们将要创建的下一个项目是 discstore 项目;创建项目的步骤与上一个项目相同,您可以使用 Maven 命令或 Eclipse IDE。

在此项目中,我们将创建一个类,该类将创建光盘列表。这个类相当简单,它使用了一些在前几章中已经讨论过的注解。这个类的名称将是 CompactDiscService,它将只有一个带有多个注解的方法。让我们从代码开始,我们将在代码块之后添加一个简短的描述:

@Path("discs")
public class CompactDiscService {
  @GET
 @Produces("application/json")
  public List<String> getCompactDiscs() {
    ArrayList<String> compactDiscList = new ArrayList<String>();
    compactDiscList.add("The Ramones");
    compactDiscList.add("The Clash");
    compactDiscList.add("Nirvana");
    return compactDiscList;
  }
}

如您所见,getCompactDiscs() 方法负责创建一个字符串列表,其中每个项目都将代表一张光盘,这是我们将添加三个项目的示例。

@Produces 注解用于指定 MIME 媒体类型,如果应用于方法级别,则这些注解将覆盖在类级别上应用的任何 @Produces 注解。如您所知,@GET 注解将代表 HTTP 方法 GET。同时,@Path 注解将帮助我们设置类为资源,其名称将为 discs

所有后端功能都已实现;我们现在需要开发一些其他资源,以便我们的示例能够运行。记住我们在上面的类中指定了一些网页吗?嗯,这正是我们现在要实现的。

oauth-server 项目

如前所述,为了创建此项目,您可以使用 Maven 命令或 Eclipse IDE。

为了使此应用程序运行,我们必须创建包含以下内容的 jboss-web.xml 文件:

<jboss-web>
    <security-domain>java:/jaas/commerce</security-domain>
    <valve>
        <class-name>org.jboss.resteasy.skeleton.key.as7.OAuthAuthenticationServerValve</class-name>
    </valve>
</jboss-web>

最后一件事情:我们需要创建一个 JSON 文件,目的是将我们的证书和安全配置保存在这个服务器上。我们将它命名为resteasy-oauth。正如你所见,这个文件并没有什么大不了的;它只是一组属性和值。通过这个文件,我们指定了 KeyStores 和密码、信任存储库路径等等。这个文件将位于本项目的WEBINF文件夹中。

{
   "realm" : "commerce",
   "admin-role" : "admin",
   "login-role" : "login",
   "oauth-client-role" : "oauth",
   "wildcard-role" : "*",
   "realm-keystore" : "${jboss.server.config.dir}/realm.jks",
   "realm-key-alias" : "commerce",
   "realm-keystore-password" : "changeit",
   "realm-private-key-password" : "changeit",
   "truststore" : "${jboss.server.config.dir}/client-truststore.ts",
   "truststore-password" : "changeit",
   "resources" : [
      "https://localhost:8443/oauth-client",
      "https://localhost:8443/discstore/"
   ]
}

webapp/WEB-INF/ jboss-deployment-structure.xml

我们必须在所有项目中配置这个文件,因为我们从 JBoss AS 实例更新了一些模块。在这个文件中,我们必须指定我们的应用程序与 JBoss 某些模块的依赖关系。然后,我们需要使用<dependencies>标签内的<module>标签明确设置它们,如下所示:

<jboss-deployment-structure>
    <deployment>
        <!-- This allows you to define additional dependencies, it is the same as using the Dependencies: manifest attribute -->
        <dependencies>
            <module name="org.jboss.resteasy.resteasy-jaxrs" services="import"/>
            <module name="org.jboss.resteasy.resteasy-jackson-provider" services="import"/>
            <module name="org.jboss.resteasy.skeleton-key" />
        </dependencies>
    </deployment>
</jboss-deployment-structure>

运行应用程序

我们已经解释了每个项目的关键部分,因此为了运行和测试应用程序,你可以从github.com/restful-java-web-services-security/source-code/tree/master/chapter04下载本章的示例文件夹。下载 ZIP 文件后,解压它,你将发现有一个名为OAuthExample的文件夹。在这个文件夹中,有我们的三个项目。你可以将它们复制并粘贴到你的工作空间中,并使用 Eclipse 导入这些项目。

我们已经提供了你所需的密钥存储库、证书和信任存储库文件,这些文件位于你设置 JBoss configuration时粘贴的configuration文件夹内。为了确保应用程序正确运行,你可能需要根据configuration文件夹内名为keystoreCommands.txt文件中的说明更新这些文件。

为了启动我们的应用程序,我们必须部署它。所以,打开一个终端。让我们进入JBOSS_HOME/bin并以独立模式启动 JBoss;这意味着如果你在 Windows 上,执行standalone.bat,如果你在 Linux 上,执行./standalone.sh。然后,打开一个终端并进入工作空间中我们的应用程序文件夹。我们必须执行以下命令:mvn clean install,然后对创建的三个项目(discstoreoauth-clientoauth-server)中的每一个执行mvn jboss-as:deploy

我们在discstore项目中创建了一个特殊类。这个类包含一个void main方法,我们通过这个类来测试我们的应用程序。我们将其命名为OAuthClientTest。这个类的代码如下:

public class OauthClientTest {

  public static void main(String[] args) throws Exception {

    String truststorePath = "C:/Users/Andres/jboss/2do_jboss/jboss-as-7.1.1.Final/standalone/configuration/client-truststore.ts";
    String truststorePassword = "changeit";
    truststorePath = EnvUtil.replace(truststorePath);

    KeyStore truststore = loadKeyStore(truststorePath, truststorePassword);

         ResteasyClient client = new ResteasyClientBuilder()
                .disableTrustManager().trustStore(truststore).build();

    Form form = new Form().param("grant_type", "client_credentials");
    ResteasyWebTarget target = client.target("https://localhost:8443/oauth-server/j_oauth_token_grant");
    target.register(new BasicAuthentication("andres", "andres"));

    AccessTokenResponse tokenResponse = target.request().post(Entity.form(form), AccessTokenResponse.class);
    Response response = client.target("https://localhost:8443/discstore/discs")
        .request()
        .header(HttpHeaders.AUTHORIZATION,
            "Bearer " + tokenResponse.getToken()).get();
    try {
      String xml = response.readEntity(String.class);
      System.out.println(xml);
    } finally {
      client.close();
    }

  }

我们将首先解释前面的代码,我们有两个变量,truststorePathtruststorePassword。第一个变量引用的是我们位于 JBoss 配置文件夹中的 client-truststore.ts 文件路径。你应该更改这个变量的值以便使这个测试工作,所以请放置你配置文件夹的路径。之后,使用我们已解释过的方法 loadKeyStore (),我们使用前面的变量来加载 KeyStore,并将这个值赋给一个名为 truststoreKeyStore 对象。从 truststore 中,我们创建了一个名为 clientRestEasyClient 对象。

现在,我们将以编程方式获取访问令牌,这样我们就可以通过使用 HTTPS 调用来从 auth-server 获取访问令牌。然后我们必须使用基本认证来识别我们的用户;结果,我们将为该用户获取一个签名访问令牌。

因此,我们向 auth-server 的上下文根执行一个简单的 POST 请求,并在目标 URL 的末尾加上 j_oauth_token_grant,因为当我们使用该 URL 并执行带有基本认证的 POST 请求时,我们将为特定用户获取一个访问令牌。

之后,我们获得了访问令牌,它是一个简单的字符串。为了调用受令牌认证保护的服务,我们必须构建一个由你的 HTTPS 请求的授权头、字符串 Bearer 以及最终的访问令牌字符串组成的字符串。这将返回响应对象,因此我们可以读取它并像在测试中那样打印它。在控制台中,你会看到如下截图所示的 CD 列表:

运行应用程序

安全管理的 SSO 配置

SSO 是一种认证机制。它允许用户通过一次输入凭证即可访问多个系统或应用程序。我们认为你今天可能更经常体验到这一点,因为我们生活在一个社交网络时代,这些服务中的大多数都允许我们使用彼此的凭证来访问多个服务。

在讨论了一些 SSO 的概念之后,让我们尝试实现这个机制。为了实现这一点,我们将使用 JBoss 7 应用程序服务器和我们的早期项目 secure-demo

作为对这个实现的简要介绍,我们想告诉你,我们将与两个文件一起工作;一个文件属于 JBoss,另一个属于我们的应用程序。

属于 JBoss 的文件是 standalone.xml。我们将向这个文件添加一些行。在下面的代码行中,让我们在 virtual-server 定义中添加 SSO 元素:

<subsystem  default-virtual-server="default-host" native="false">
            <connector name="http" protocol="HTTP/1.1" scheme="http" socket-binding="http"/>
            <virtual-server name="default-host" enable-welcome-root="true">
                <alias name="localhost"/>
                <sso domain="localhost" reauthenticate="false"/>
            </virtual-server>
</subsystem>

reauthenticate 属性允许我们确定每个请求是否需要重新认证到 securityReal。默认值是 false

下一个我们必须编辑的文件是在我们的应用程序中,其名称为jboss-web.xml。此外,我们还需要向此文件添加一些代码行。这些代码行将声明管理 SSO 的 valve。换句话说,每个请求都将通过这个 valve,如下面的代码所示:

<jboss-web>
    <security-domain>java:/jaas/other </security-domain>
          <valve>
        <class-name>org.apache.catalina.authenticator.SingleSignOn</class-name>
    </valve>
</jboss-web>

就算你忘记了或者删除了它,我们在前面的章节中设置了一个安全域。以下代码块必须在standalone.xml文件中存在:

<security-domain name="other" cache-type="default">
    <authentication>
      <login-module code="Remoting" flag="optional">
<module-option name="password-stacking"  value="useFirstPass"/>
      </login-module>
      <login-module code="RealmUsersRoles" flag="required">
<module-option name="usersProperties" value="${jboss.server.config.dir}/application-users.properties"/>
<module-option name="rolesProperties" value="${jboss.server.config.dir}/application-roles.properties"/>
<module-option name="realm" value="ApplicationRealm"/>
<module-option name="password-stacking" value="useFirstPass"/>
      </login-module>
     </authentication>
</security-domain>

由于我们正在使用secure-demo示例,因此我们只需修改以下内容来配置 SSO。

为了测试这个机制,我们需要另一个应用程序。我们必须在我们的secure-demo示例中复制我们刚刚所做的配置。

当我们在其中一个中输入凭据时,由于我们已经应用了 SSO,我们不再需要在其他中输入凭据。我们将在这两个应用程序中进行认证。

OAuth 令牌通过基本认证

现在,让我们探索并实现一个使用令牌的简短示例。为了构建这个示例,我们将创建一个类。这个类,就像之前的示例一样,将模拟数据库客户端。它将具有相同的方法getCompactDiscs(),但我们将在这个示例中修改内部函数。此外,这次它不会接收任何参数。

好的,让我们开始吧!首先,在类中创建两个静态字符串字段。第一个字段将保存 auth-server 中认证的 URL。另一个字段将显示光盘列表的 URL;你可以从之前的示例中重用相同的网页。然后,你应该有如下变量:

private static String urlAuth = "https://localhost:8443/auth-server /j_oauth_token_grant";
private static String urlDiscs = "https://localhost:8443/discstore/discs";

然后,让我们创建我们的方法来获取光盘列表。以下代码片段显示了该方法的确切执行方式:

public static List<String> getCompactDiscs() {
  ResteasyClient rsClient = new ResteasyClientBuilder().disableTrustManager().build();
    Form form = new Form().param("grant_type", "client_credentials");
  ResteasyWebTarget resourceTarget = rsClient.target(urlAuth);
    resourceTarget.register(new BasicAuthentication("andres", "andres"));
  AccessTokenResponse accessToken = resourceTarget.request().post(Entity.form(form), AccessTokenResponse.class);
    try {
      String bearerToken = "Bearer " + accessToken.getToken();
      Response response = rsClient.target(urlDiscs).request().header(HttpHeaders.AUTHORIZATION, bearerToken).get();
      return response.readEntity(new GenericType<List<String>>() {
      });
    } finally {
      rsClient.close();
    }
  }

是时候检查我们刚刚所做的工作了。作为第一步,我们创建了一个ResteasyClient对象。如果你注意到了,我们使用了一些东西来禁用信任管理和主机名验证。这次调用的结果是关闭了服务器证书验证,允许 MITM(中间人)攻击。因此,请谨慎使用此功能。

然后,我们创建一个form对象并传入一些参数。这些参数通过param()方法传入,分别代表参数名称和参数值。这意味着我们指定了应用程序请求的授权类型,它将是client_credentials

然后,就像我们在之前的示例中所做的那样,让我们创建一个 RESTEasy 网络目标,该目标将针对我们之前设置的 URL 显示光盘列表。记住,这个 URL 是在我们之前创建的静态字段中设置的。这个网络目标将是我们将访问的resourceTarget对象。

当我们使用register()方法并传入一个BasicAuthentication对象时,我们注册了一个自定义 JAX-RS 组件的实例,该实例将在可配置上下文中实例化和使用。

接下来,我们通过向我们的目标执行请求来创建AccessTokenResponse类。然后,在同一行中,我们执行一个 POST 请求,以便同步发送我们想要获取的当前请求的实体和响应类型。Entity.form()方法从我们之前创建的form对象中创建application/x-www-form-urlencoded实体。现在,这将返回一个AccessTokenResponse对象;我们使用这个对象通过在令牌开头添加单词Bearer来构建承载令牌。

最后,让我们通过向urlDiscs变量中设置的 URL 执行请求来创建响应对象。我们应该使用ResteasyClient对象来定位这个资源,然后执行请求并使用变量bearerToken中设置的bearer令牌设置HttpHeaders.AUTHORIZATION头字段。这样,我们就获得了对目标资源的访问权限;在这种情况下,我们可以看到信息。

由于我们继续使用相同的应用程序业务,我们可以重用前一个示例中的网页。确保将index.htmldiscsList.jsp网页包含到你的示例中,路径与前一个示例相同。我们还将使用jboss-deployment-structure.xml文件中设置的配置,因为我们正在使用相同的模块依赖项。

我们的web.xml文件应该比前一个示例简单,所以可能看起来像以下这样:

<?xml version="1.0" encoding="UTF-8"?>
<web-app 

      xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd"
      version="3.0">
    <security-constraint>
        <web-resource-collection>
            <url-pattern>/*</url-pattern>
        </web-resource-collection>
        <user-data-constraint>
            <transport-guarantee>CONFIDENTIAL</transport-guarantee>
        </user-data-constraint>
    </security-constraint>
</web-app>

运行应用程序

你可以从github.com/restful-java-web-services-security/source-code/tree/master/chapter04下载完整的代码和配置。解压文件,你将找到一个名为token-grant的文件夹。你必须使用相同的命令部署这个项目。作为一个要求,你必须部署oauth-serveroauth-clientdiscstore项目。

是时候运行我们的应用程序了。让我们执行前一个示例中做的步骤,OAuth 示例。之后,我们必须打开我们喜欢的浏览器并输入 URL https://localhost:8443/token-grant/。这将带我们到以下网页:

运行应用程序

好吧,正如你所注意到的,我们重用了相同的网页,只是为了这些示例的目的。然而,有一点小小的不同;当调用不同的网页时,你可以查看我们刚才解释的核心。这将执行一个令牌,我们将通过这个令牌对想要访问的数据进行请求。结果,我们将在网页中读取我们的光盘列表,如下面的截图所示:

运行应用程序

最终结果是能够在网页中展示光盘列表的能力。然而,不要忘记发生了什么;我们只是使用请求、使用我们的凭证进行的基本身份验证和表单,获得了访问令牌响应。有了访问令牌响应,我们可以创建响应并使用相应的授权来展示数据。

自定义过滤器

简要介绍,JAX-RS 2.0 有两个不同的拦截概念:过滤器(filters)和拦截器(interceptors)。

拦截器是拦截 EJB 方法调用的组件。它们可以用来审计和记录 EJB 何时被访问。这是一个不会在本书中包含的主题,但如果你对它感到好奇并想了解更多,我们提供以下链接作为参考,以便你可以查找:

过滤器主要用于修改或处理传入和传出的请求或响应头。它们可以在请求和响应处理之前和之后执行。

此外,JAX-RS 2.0 为我们提供了两种过滤器类别:服务器端过滤器(server-side filters)和客户端过滤器(client-side filters)。以下图表展示了这一概念的更好分类:

自定义过滤器

服务器端过滤器

当我们在服务器端时,我们对这些过滤器有另一种分类;容器请求过滤器在 JAX-RS 资源方法被调用之前执行。此外,我们还有容器响应过滤器;你可能已经猜到了,它们在 JAX-RS 资源方法被调用之后执行。然而,这还没有结束;容器请求过滤器还有另一种分类:预匹配和后匹配。

你可以通过@PreMatching注解指定一个预匹配的容器请求过滤器,这意味着该过滤器将在 JAX-RS 资源方法与传入的 HTTP 请求匹配之前执行。

容器请求过滤器可以通过执行abortWith (Response)方法来终止请求。如果过滤器实现了自定义身份验证协议,它可能想要终止。

一旦资源类方法被执行,JAX-RS 将运行所有容器响应过滤器。这些过滤器允许你在响应被序列化和发送到客户端之前修改输出的响应。

客户端过滤器

正如我们已经告诉你的,客户端也有过滤器,并且与服务器端过滤器类似,它们也有两种类型的过滤器:客户端请求过滤器(client request filters)和客户端响应过滤器(client response filters)。客户端请求过滤器在 HTTP 请求通过网络发送到服务器之前执行。另一方面,客户端响应过滤器在从服务器收到响应之后、响应体被组装之前运行。

客户端请求过滤器也能够中止请求,并在不通过服务器发送任何数据的情况下提供响应。客户端响应过滤器能够在将响应对象返回给应用程序代码之前修改响应对象。

过滤器的示例用法

在查看了一些关于这个主题的必要理论之后,是时候动手实践了。现在,我们将实现一个示例,以支持我们新的理论知识。所以,让我们开始吧!

我们将实现一个拦截器,该拦截器将根据请求中发送的用户名和密码验证用户的访问权限。您可以从以下链接下载此示例的完整代码:

github.com/restful-java-web-services-security/source-code/tree/master/chapter04

我们有光盘存储的主题。因此,以下类将代表我们的服务,并将具有按名称查找光盘和更新光盘信息的函数。这里使用的注解已经在前面章节中研究过,所以您可能会发现以下代码容易理解:

 @Path("/compactDisc-service")
public class CompactDiscService {
  @PermitAll
 @GET
 @Path("/compactDiscs/{name}")
  public Response getCompactDiscByName(@PathParam("name") String name, @Context Request request) {
    Response.ResponseBuilder rb = Response.ok(CompactDiscDatabase.getCompactDiscByName(name));
    return rb.build();
  }

  @RolesAllowed("ADMIN")
 @PUT
 @Path("/compactDiscs/{name}")
  public Response updatePriceByDiscName(@PathParam("name") String name) {
    // Update the User resource
    CompactDiscDatabase.updateCompactDisc(name, 10.5);
    return Response.status(200).build();
  }
}

如您所见,我们只创建了两个方法,一个用于按名称检索光盘,另一个用于更新光盘的价格。注解让我们知道,getCompactDiscByName()方法对所有用户都是可访问和可执行的;同时,updatePriceByDiscName()方法只能由具有ADMIN角色的用户访问和执行。

如果您注意到了前面的代码,我们使用了CompactDiscDatabase类,该类模拟了一个数据库。我们在前面的示例中也使用了相同的技巧。由于它工作得非常好,我们再次使用它。这个类没有特殊的代码。您可以从以下代码中了解这一点:

public class CompactDiscDatabase {
  public static HashMap<String, CompactDisc> compactDiscs = new HashMap<String, CompactDisc>();

  static {
    CompactDisc ramonesCD = new CompactDisc();
    ramonesCD.setDiscName("Ramones Anthology");
    ramonesCD.setBandName("The Ramones");
    ramonesCD.setPrice(15.0);

    Calendar calendar = Calendar.getInstance();
    calendar.set(1980, 10, 22);
    Date realeaseDate = calendar.getTime();
    ramonesCD.setReleaseDate(realeaseDate);
    compactDiscs.put("Ramones Anthology", ramonesCD);

  }

  public static CompactDisc getCompactDiscByName(String name) {
    return compactDiscs.get(name);
  }

  public static void updateCompactDisc(String name, double newPrice) {
    CompactDisc cd = compactDiscs.get(name);
    cd.setPrice(newPrice);
  }
}

这里没有复杂的东西;我们只是创建了一个映射并放入了一个条目。这个条目是一个光盘对象,正如您所看到的。我们有两个静态方法来模拟查询——一个 SELECT 语句和一个 UPDATE 语句。

现在,让我们检查以下代码中的CompactDisc类:

@XmlAccessorType(XmlAccessType.NONE)
@XmlRootElement(name = "compactDisc")
public class CompactDisc implements Serializable {
  private static final long serialVersionUID = 1L;

  @XmlElement(name = "discName")
  private String discName;

  @XmlElement(name = "bandName")
  private String bandName;

  @XmlElement(name = "releaseDate")
  private Date releaseDate;

  @XmlElement(name = "price")
  private double price;
//getters and setters
}

在这个类中,我们只设置了代表通用光盘属性的字段。使用@XmlElement注解将属性映射到从属性名称派生的 XML 元素。

现在,是时候实现过滤器了。在这段简短的介绍之后,我们将向您展示代码,解释我们所做的工作,并解释实现中使用的某些技术概念。准备好了吗?我们开始了!

由于这个类的代码有点长,我们将将其拆分,并在每个代码块之后包含一个简短的描述,如下所示:

@Provider
public class SecurityFilter implements javax.ws.rs.container.ContainerRequestFilter {

  private static final String ADMIN = "ADMIN";
  private static final String RESOURCE_METHOD_INVOKER = "org.jboss.resteasy.core.ResourceMethodInvoker";
  private static final String AUTHORIZATION_PROPERTY = "Authorization";
  private static final String AUTHENTICATION_SCHEME = "Basic";
  private static final ServerResponse ACCESS_DENIED = new ServerResponse("Access denied for this resource", 401,
      new Headers<Object>());
  private static final ServerResponse ACCESS_FORBIDDEN = new ServerResponse("Nobody can access this resource", 403,
      new Headers<Object>());

让我们来看看这段代码。第一步,为了实现一个过滤器,我们需要添加注解@Provider。当我们把这个注解放在类级别时,我们就把这个类设置为一个过滤器。我们的类名是SecurityFilter,正如你所见,它实现了ContainerRequestFilter接口。如果你还记得,这个过滤器将在服务器端执行,并在资源方法被调用之前执行。

在我们类的主体开始处,我们设置了一些我们稍后会用到的常量。AUTHORIZATION_PROPERTY常量代表一个属性的名称,就像RESOURCE_METHOD_INVOKER常量一样。AUTHENTICATION_SCHEME常量代表一个字符串。ACCESS_DENIEDACCESS_FORBIDDEN常量代表两个不同的服务器响应对象,以便在请求被拒绝或用户权限不足时通知用户请求的结果。

由于我们实现了ContainerRequestFilter接口,我们必须重写filter()方法。我们的过滤逻辑将放在这个方法中,基于执行请求的用户来过滤请求。

让我们开始。作为第一步,我们使用常量RESOURCE_METHOD_INVOKER获取请求的方法。之后,我们将有一个ResourceMethodInvoker对象,然后是Method对象,如下面的代码所示:

@Override
public void filter(ContainerRequestContext requestContext) {
    ResourceMethodInvoker methodInvoker = (ResourceMethodInvoker) requestContext
        .getProperty(RESOURCE_METHOD_INVOKER);
    Method method = methodInvoker.getMethod();

接下来,我们将对method进行一些简单的验证。我们将检查方法是否被注解为@PermitAll。如果不是,方法将继续,然后我们检查它是否被注解为@DenyAll。如果方法被注解为DenyAll,那么我们将终止请求,包括常量ACCESS_FORBIDDEN,如下面的代码所示:

// Access allowed for all
    if (!method.isAnnotationPresent(PermitAll.class)) {
      // Access denied for all
      if (method.isAnnotationPresent(DenyAll.class)) {
        requestContext.abortWith(ACCESS_FORBIDDEN);
        return;
      }

现在,我们必须获取用户名和密码。我们首先获取请求的头部并将其放入一个映射中。然后,我们使用常量AUTHORIZATION_PROPERTY作为键来获取授权字符串列表。这个列表将告诉我们用户是否有足够的权限。因此,我们检查列表是否为空或为 null;如果进入if()块,我们将终止请求,包括常量ACCESS_DENIED,如下面的代码所示:

      final MultivaluedMap<String, String> headersMap = requestContext.getHeaders();

      final List<String> authorizationList = headersMap.get(AUTHORIZATION_PROPERTY);

      if (authorizationList == null || authorizationList.isEmpty()) {
        requestContext.abortWith(ACCESS_DENIED);
        return;
      }

这个列表的第一个元素包含编码的用户名和密码作为字符串。因此,我们执行替换操作,消除常量AUTHENTICATION_SCHEME中包含的字符串。然后,我们使用Base64.decodeBase64解码器对其进行解码,并通过StringTokenizer获取分隔的用户名和密码。让我们看看下面的代码:

 final String encodedUserPassword = authorizationList.get(0).replaceFirst(AUTHENTICATION_SCHEME + " ", "");

      String usernameAndPassword = new String(Base64.decodeBase64(encodedUserPassword));

      // Split username and password tokens
      final StringTokenizer tokenizer = new StringTokenizer(usernameAndPassword, ":");
      final String userName = tokenizer.nextToken();
      final String password = tokenizer.nextToken();

现在是评估和检查用户是否有足够权限的时候了。首先,让我们检查method是否有@RolesAllowed注解;如果有,我们使用method对象获取允许的角色集合。最后,我们检查常量ADMIN是否包含在这个列表中。如果不是,请求将被终止,并且再次包含ACCESS_DENIED,如下面的代码所示:

      // Verify user access
 if (method.isAnnotationPresent(RolesAllowed.class)) {
 RolesAllowed rolesAnnotation = method.getAnnotation(RolesAllowed.class);
        Set<String> rolesSet = new HashSet<String>(Arrays.asList(rolesAnnotation.value()));

        // Is user valid?
        if (!isUserAllowed(userName, password, rolesSet)) {
        requestContext.abortWith(ACCESS_DENIED);
          return;
        }
      }
    }
  }

  private boolean isUserAllowed(final String username, final String password, final Set<String> rolesSet) {
    boolean isAllowed = false;

    if (rolesSet.contains(ADMIN)) {
      isAllowed = true;
    }
    return isAllowed;
  }
}

概述

在本章中,我们研究和实现了分享和保护我们信息最有用和必要的技巧。如今,应用程序之间相互交互的应用已经大幅增加,因为它们想要满足客户、用户等的需求,在此过程中既不妥协数据的安全性,也不妥协数据的完整性。

在本章中,我们研究了多种技术,以确保、限制和授权第三方应用程序使用我们的资源,从关于 OAuth 2.0 认证、单点登录、过滤器和令牌的简要但描述性的概念开始。

通过一个实际示例和真实代码,你见证了如何授权第三方应用程序访问特定资源,以便共享信息并保持对其的控制。此外,我们还检查并使用特定代码实现了近年来最常用的技术之一,尤其是在社交网络领域,即单点登录(Single Sign-On)。现在,你可以将这些概念和技术付诸实践,以构建相互交互的应用程序,选择你想要共享的资源,选择你想要用作单点登录的应用程序,并根据用户和角色过滤某些资源的使用。

第五章:数字签名和消息加密

由于许多系统相互作用以实现其业务目标,我们常常感到有必要与别人公开的服务进行交互。此外,当安全需求扮演重要角色时,我们必须验证我们接收到的信息是否来自我们期望的人,并且它没有在传输过程中被修改。正是在这里,数字签名将发挥重要作用,帮助我们满足这一需求。

此外,我们有时可能需要加密消息体,以防止它被不受欢迎的人拦截后阅读。正是在这里,我们可以利用安全/多用途互联网邮件扩展(Secure/Multipurpose Internet Mail Extensions),或称 S/MIME 标准,这在电子邮件领域被广泛用于公钥(en.wikipedia.org/wiki/Public_key)、加密(en.wikipedia.org/wiki/Encryption)和签名(en.wikipedia.org/wiki/Digital_signature)MIME 数据(en.wikipedia.org/wiki/MIME),并且它还提供了适应 HTTP 协议的能力,使我们能够在 RESTful Web 服务中使用它。

在本章中,我们将学习以下内容:

  • 签名消息

  • 验证签名

  • 使用 S/MIME 加密消息体

数字签名

现在,数字签名是一种广泛使用的机制。它们主要用于签署数字文档和发行电子发票等。

使用它们的优点包括以下内容:

  • 它们允许接收者获得签名者的身份。

  • 它们提供了一种能力,可以验证发送的信息自签发以来未被修改。

为了通过 RESTful Web 服务电子签名我们将交换的信息,我们将使用名为域密钥识别邮件(DomainKeys Identified Mail,DKIM)的认证机制,它允许我们使用 DOSETA 规范规定的规则装饰消息的头部。这种认证机制主要用于电子邮件身份验证;然而,它也可以在其他协议如 HTTP 上工作,正因为如此,我们可以将其集成到 RESTful Web 服务中。因此,我们将注入用于签名的元数据到我们的消息中,这些签名可以被希望消费它们的人验证。

在这个时候,我们将构建一个示例,展示如何签名一条消息,然后剖析它的每个部分来理解其操作。

如果您愿意,您可以使用以下 GitHub 链接下载源代码:

github.com/restful-java-web-services-security/source-code/tree/master/chapter05/signatures

否则,我们将在以下页面中解释它。让我们先创建一个新的项目。打开终端并输入以下内容:

mvn archetype:generate -DgroupId=com.packtpub -DartifactId=signatures -DarchetypeArtifactId=webapp-javaee6 -DarchetypeGroupId=org.codehaus.mojo.archetypes

当它要求你输入版本时,将默认值 1.0-SNAPSHOT 更改为 1.0

现在,我们将生成允许我们加密消息的密钥,并将它们放置在我们的应用程序的类路径中。为此,我们首先将项目导入到 Eclipse IDE 中,然后在项目中创建一个文件夹,我们将要生成的密钥放置在这个文件夹中。在 Eclipse 中,右键单击名为 signatures 的新项目,并选择 新建 | 源文件夹

文件夹名称 字段中,我们将输入 src/main/resources,然后点击 完成 按钮。

现在,让我们从命令行进入这个目录并执行以下指令:

keytool -genkeypair -alias demo._domainKey.packtpub.com -keyalg RSA -keysize 1024 -keystore demo.jks

现在,我们应该为 KeyStore 和我们将用于签名消息的密钥输入一个密码。当它要求你输入密码时,输入 changeit,这是我们在这本书的示例中一直使用的相同密码。然后,我们输入如下截图所示的信息:

数字签名

现在,我们将实现一些源代码来签名一个消息。我们首先需要将所需的依赖项添加到 pom.xml 文件中。

首先,添加从其中获取工件的自定义 JBoss 存储库,如下代码所示:

<repositories>
  <repository>
    <id>jboss</id>
    <url>http://repository.jboss.org/maven2</url>
  </repository>
</repositories>

现在,让我们添加所有我们需要签名消息的依赖项,如下所示:

  <dependencies>
    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-jaxrs</artifactId>
      <version>3.0.6.Final</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-crypto</artifactId>
      <version>3.0.6.Final</version>
    </dependency>
  </dependencies>

为了避免类路径中重复的类,我们应该删除以下依赖项:

    <dependency>
      <groupId>javax</groupId>
      <artifactId>javaee-web-api</artifactId>
      <version>6.0</version>
      <scope>provided</scope>
    </dependency>

更新 RESTEasy JAR 文件

由于我们使用的是 3.0.6.Final 版本来编译项目,因此有必要更新 JBoss 中的现有版本。因此,我们将前往 URL sourceforge.net/projects/resteasy/files/Resteasy%20JAX-RS/ 并下载我们刚刚描述的版本。

当我们解压 .zip 文件时,我们会找到一个名为 resteasy-jboss-modules-3.0.6.Final.zip 的文件。让我们也解压这个文件,然后将所有内容粘贴到我们的目录 JBOSS_HOME/modules 中。由于 RESTEasy 模块有依赖项,我们还需要更新它们。因此,在更新 RESTEasy 模块后,我们应该更新模块 org.apache.httpcomponents。让我们前往目录 JBOSS_HOME/modules/org/apache/httpcomponents 并更新以下工件:

  • httpclient-4.1.2.jar 更新为 httpclient-4.2.1.jar

  • httpcore-4.1.4.jar 更新为 httpcore-4.2.1.jar

此外,我们修改 module.xml 文件,因为 JAR 文件的名字不同,如下所示:

<?xml version="1.0" encoding="UTF-8"?>

<!--
 ...
  -->

<module  name="org.apache.httpcomponents">
    <properties>
        <property name="jboss.api" value="private"/>
    </properties>

    <resources>
 <resource-root path="httpclient-4.2.1.jar"/>
 <resource-root path="httpcore-4.2.1.jar"/>
        <resource-root path="httpmime-4.1.2.jar"/>
        <!-- Insert resources here -->
    </resources>

    <dependencies>
        <module name="javax.api"/>
        <module name="org.apache.commons.codec"/>
        <module name="org.apache.commons.logging"/>
        <module name="org.apache.james.mime4j"/>
    </dependencies>
</module>

应用数字签名

现在我们已经拥有了编译我们项目所需的一切,我们将创建一个非常简单的操作并应用签名。为了实现这一点,让我们在源代码包 com.packtpub.resteasy.services 中创建一个名为 SignedService 的类,如下截图所示:

应用数字签名

要签名消息,我们从 KeyStore 中取一个密钥并使用它。我们可以通过它们的别名和它们所属的域以独特的方式识别密钥。例如,对于密钥demo._domainKey.packtpub.com,别名是demo,它所属的域是密钥packtpub.com。鉴于我们可以在 KeyStore 中找到多个密钥,RESTEasy 提供了使用注解@Signed选择我们想要的密钥的能力。

让我们将以下代码中突出显示的方法添加到类中,并观察注解是如何工作的:

  @POST
  @Produces("text/plain")
 @Signed(selector = "demo", domain = "packtpub.com")
  public String sign(String input) {
    System.out.println("Aplyng signature " + input);
    return "signed " + input;
  }

下图以更好的方式展示了如何选择密钥来签名消息:

应用数字签名

现在,我们将定义在签名资源下的路径将可用,因此让我们按照以下方式注释类:

import javax.ws.rs.Consumes;
import javax.ws.rs.POST;
import javax.ws.rs.Path;

import org.jboss.resteasy.annotations.security.doseta.Signed;

@Path("/signed")
public class SignedService {
...

为了使应用程序正常工作,我们将提供信息,以便它可以应用适当的签名。

首先,在src/main/webapp文件夹中,我们将创建一个包含空web.xml文件的WEB-INF文件夹。

让我们从web.xml文件开始,它应该看起来如下:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 

  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
  http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">

  <display-name>signatures</display-name>

</web-app>

现在,我们首先要做的是告诉我们的应用程序我们想要签名的资源是什么,即包含我们正在签名的方法的类。为此,让我们使用相应的完整类名配置参数resteasy.resources,如下所示:

<context-param>
<param-name>resteasy.resources</param-name>
<param-value>com.packtpub.resteasy.services.SignedResource</param-value>
</context-param>

接下来,我们将通知我们的应用程序应用签名的密钥的位置(我们之前创建的.jks文件)。为此,我们有两个上下文参数可用,resteasy.doseta.keystore.classpathresteasy.keystore.filename。让我们使用第一个参数,以便我们的文件看起来如下:

<context-param>
<param-name>resteasy.doseta.keystore.classpath</param-name>
<param-value>demo.jks</param-value>
</context-param>

如您所记得,在创建密钥时,我们被要求为 KeyStore 提供密码。我们将使用参数resteasy.doseta.keystore.password告诉我们的应用程序这是什么。让我们添加以下内容:

  <context-param>
    <param-name>resteasy.doseta.keystore.password</param-name>
    <param-value>changeit</param-value>
  </context-param>

要创建 KeyStore,从中我们将提取允许我们签名消息的密钥,我们必须添加以下参数:

<context-param>
  <param-name>resteasy.context.objects</param-name>
  <param-value>org.jboss.resteasy.security.doseta.KeyRepository : org.jboss.resteasy.security.doseta.ConfiguredDosetaKeyRepository</param-value>
</context-param>

最后,我们应该添加 RESTEasy servlet,如下所示:

  <servlet>
    <servlet-name>Resteasy</servlet-name>
    <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>Resteasy</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>

在这里,我们展示了在添加所有必需信息后web.xml文件应该如何看起来:

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="3.0" 

  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee 
  http://java.sun.com/xml/ns/javaee/web-app_3_0.xsd">
<web-app>
  <display-name>signatures</display-name>
  <context-param>
    <param-name>resteasy.resources</param-name>
    <param-value>com.packtpub.resteasy.services.SignedService</param-value>
  </context-param>
  <context-param>
    <param-name>resteasy.doseta.keystore.classpath</param-name>
    <param-value>demo.jks</param-value>
  </context-param>
  <context-param>
    <param-name>resteasy.doseta.keystore.password</param-name>
    <param-value>changeit</param-value>
  </context-param>
  <context-param>
    <param-name>resteasy.context.objects</param-name>
    <param-value>org.jboss.resteasy.security.doseta.KeyRepository : org.jboss.resteasy.security.doseta.ConfiguredDosetaKeyRepository</param-value>
  </context-param>
  <servlet>
    <servlet-name>Resteasy</servlet-name>
    <servlet-class>org.jboss.resteasy.plugins.server.servlet.HttpServletDispatcher</servlet-class>
  </servlet>
  <servlet-mapping>
    <servlet-name>Resteasy</servlet-name>
    <url-pattern>/*</url-pattern>
  </servlet-mapping>
</web-app>

现在,让我们通过执行以下命令生成 WAR 文件:

mvn install

然后,我们将生成的工件复制到 JBoss 部署目录。

测试功能

现在,打开 SoapUI,测试是否如以下截图所示,web 服务按预期运行:

测试功能

如您在响应中看到的,我们获得了用于签名消息的DKIM-Signature头。此头的完整内容如下:

DKIM-Signature: d=packtpub.com;s=demo;v=1;a=rsa-sha256;c=simple/simple;bh=lc+ECoAqpQCB4ItWLUomBv34m3F9G0pkIBAI8Z/yWcQ=;b=AlJY6iiCtdCnHrJa+Of9aRgBXeIp7V7cEG7eyUp0CRbD9wjFodbQGRQjhfwDgd1WIBzVLIWelTdI85BlGl3ACNcMLBjPv2iBBjo+78e/9HcYs81YNlPRAAj6jzymA/+jkmpTVcthWaEEyoPJJBAI5FvP33zH7etfkFaGX+bwer0=

从整个字符串中,对我们来说重要的是以下内容:

  • d=: 这是域,在实现方法时指示的值。

  • a=:这是 RESTEasy 用来签名消息的算法。在这种情况下,我们使用 RSA,因为它是目前框架唯一支持的算法。

其他参数并不重要,它们只是为了一个签名的消息。

现在,为了验证签名的真实性,我们将创建一个类,我们将从这个类中进行验证。

我们将使用 JUnit;因此,首先在 pom.xml 文件中添加相应的依赖项,如下面的代码片段所示:

    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.8.2</version>
      <scope>test</scope>
    </dependency>

现在,让我们创建一个新的源文件夹名为 scr/test/java,并在其中创建一个名为 com.packtpub.resteasy.services.test 的包。在包内部,让我们使用以下内容创建一个名为 SignedServiceTest 的类:

import javax.ws.rs.client.Entity; 
import javax.ws.rs.client.Invocation; 
import javax.ws.rs.client.WebTarget; 
import javax.ws.rs.core.Response;  
import junit.framework.Assert;  
import org.jboss.resteasy.client.jaxrs.ResteasyClient; 
import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder; 
import org.jboss.resteasy.security.doseta.DosetaKeyRepository; 
import org.jboss.resteasy.security.doseta.Verification; 
import org.jboss.resteasy.security.doseta.Verifier; 
import org.junit.Test;

public class SignedServiceTest {

  @Test
  public void testVerification() {
    // Keys repository
    DosetaKeyRepository repository = new DosetaKeyRepository();
    repository.setKeyStorePath("demo.jks");
    repository.setKeyStorePassword("changeit");
    repository.start();
    // Building the client
  ResteasyClient client = new ResteasyClientBuilder().build();
    Verifier verifier = new Verifier();
    Verification verification = verifier.addNew();
    verification.setRepository(repository);
    WebTarget target = client
             .target(
      "http://localhost:8080/signatures-1.0/signed");
    Invocation.Builder request = target.request();
    request.property(Verifier.class.getName(), verifier);
    // Invocation to RESTful web service
    Response response = request.post(Entity.text("Rene"));
    // Status 200 OK
    Assert.assertEquals(200, response.getStatus());
    System.out.println(response.readEntity(String.class));
    response.close();
    client.close();
  }
}

如果一切顺利,我们将看到测试结果是一个绿色的条形,如下面的截图所示:

测试功能

使用注解验证签名

验证资源是否签名的更简单方法是使用注解。这种解决方案主要适用于你必须满足的签名流。

例如,想象一下,Packt Publishing 公司的员工可以通过一个系统申请增加他们的电脑的 RAM。为了将这些请求视为有效,它们必须由提出请求的人签名。我们的意思是,我们只需要请求被签名,才能被认为是有效的,如下面的图所示:

使用注解验证签名

对于这个例子,我们将向我们的 SignedService 类添加两种方法;第一个方法将允许我们发送请求,如下所示:

  @POST
  @Path("ram")
  @Signed(selector = "demo", domain = "packtpub.com")
  @Consumes("text/plain")
  public String requestRam(int numberOfGB) {
    return numberOfGB + "-GB";
  }

为了满足业务需求,我们将使用 @Verify 注解,在其中我们可以对签名添加限制。目前,我们只需要验证请求是否已签名。

以下是一个方法,展示了老板用来批准或拒绝为员工 PC 增加内存的复杂逻辑:

@Verify
@POST
@Path("verifier")
@Produces("text/plain")
public String processRequestRam (String input) {
  int numberOfGbRequested = Integer.valueOf(input.split("-")[0]);
  if (numberOfGbRequested > 4) {
    return "deny";
  } else {
    return "accepted";
  }
}

现在,让我们在 JBoss 上部署应用程序,并使用 SoapUI 进行测试。正如我们提到的,为了处理请求,请求必须经过签名。因此,首先对 processRequestRam 方法发送一个不带签名的请求,如下面的截图所示:

使用注解验证签名

对于应用程序能够被处理来说,最重要的是它来自公司域,在这种情况下,是 packtpub.com。之后,老板对申请进行严格的分析,并做出判断,以确定申请是否被批准或拒绝。

对于这个例子,我们将删除我们之前创建的方法,并向我们的 SignedService 类添加两种方法;第一个方法将允许我们发送请求,如下所示:

@POST
@Signed(selector = "demo", domain = "packtpub.com")
@Consumes("text/plain")
public Response requestRAM(int numberOfGB) {
  return Response.seeOther(
    URI.create("/signed/" + "GB:" + numberOfGB)).build();
}

输出清楚地显示了错误。请求无法处理,因为没有包含验证签名的信息的 DKIM-Signature 标头。这意味着这些头信息不存在,因为它们之前没有被签名。

为了成功处理请求,我们将调用一个签名的 Web 服务。我们将添加带有签名信息的头,并再次调用processRequestRam方法。

让我们先调用requestRam操作,如下截图所示:

使用注释验证签名

由于这个回调,我们将获得以下值:

DKIM-Signature: d=packtpub.com;s=demo;v=1;a=rsa-sha256;c=simple/simple;bh=uA6n2udZlWdx+ouwCEeeyM6Q48KH0EWa2MnfBwMP+vM=;b=T0drw9QWud7rs1w//5384hs8GCatJKzmljIhgiTrHWdVx/IhCVl915yycchN+hQ+ljUaS6bPtLYo/ZNspcv2LtAe/tKTPpng4RWlr52k0TqnV3XX2KvJ7kBOpEU2Rg6f6lBOJT5v+o0iV05ObagfzKDfQ9o09WpZjQKcBG+/xvE=

RESPONSE: 8 GB

让我们继续前进!现在,我们将使用这些值发出请求。从 SoapUI 中,让我们调用processRequestRam操作,并关注请求编辑器的左下角;那里有一个名为Header的选项。让我们选择这个选项,并点击+符号。现在,我们必须输入DKIM-Signature头,并放置相应的值。同时,别忘了发送请求参数8-GB,这是requestRam操作的响应,如以下截图所示:

使用注释验证签名

如我们所见,请求已成功处理,但老板拒绝了内存增加的请求。现在,我们指出数字签名允许我们验证信息在签名后未被篡改。假设恶意软件拦截了响应,并且不是8-GB,而是提供了12-GB的值。让我们根据数字签名的理论在 SoapUI 中发出这个请求。这个请求应该是不合法的;然而,我们必须检查:

使用注释验证签名

错误信息清楚地表明消息体已被篡改,因此请求未处理,我们收到HTTP 401 Unauthorized消息。这证实了之前关于已签名消息完整性的陈述。

然而,RESTEasy 不仅允许我们验证消息已被签名,我们还可以验证签名人是否属于特定域名。在我们的例子中,只有当公司属于packtpub.com域名时,才会被认为是有效的。为了执行此类控制,我们将进行以下更改:

@Verify(identifierName = "d", identifierValue = "packtpub.com")
@POST
@Path("verifier")
@Produces("text/plain")
public String processRequestRam(String input) {
  int numberOfGbRequested = Integer.valueOf(input.split("-")[0]);
  if (numberOfGbRequested > 4) {
    return "deny";
  } else {
    return "accepted";
  }
}

让我们在 JBoss 中部署应用程序,并再次从 SoapUI 中执行请求:

使用注释验证签名

现在,让我们强制一个故障。我们将假设只有从itpacktpub.com域名签名的有效消息。因此,让我们应用以下更改:

@Verify(identifierName = "d", identifierValue = "itpacktpub.com")
@POST
@Path("verifier")
@Produces("text/plain")
public String processRequestRam(String input) {
  int numberOfGbRequested = Integer.valueOf(input.split("-")[0]);
  if (numberOfGbRequested > 4) {
    return "deny";
  } else {
    return "accepted";
  }
}

让我们在 JBoss 中再次部署应用程序,并从 SoapUI 中执行请求:

使用注释验证签名

如我们所预期,这次请求失败了。显然,这是因为签名无法验证,因为消息是用packtpub.com域名签名的,而不是我们在processRequestRam操作中设置的itpacktpub.com域名。

突然,你可能会想知道为什么识别出的名称的值是d。正如我们之前提到的,字母d代表域。RESTEasy 文档对每个参数的说明稍微详细一些。在这里,我们向您展示文档中关于 JBoss 相关主题的一个示例:

这里是一个 DKIM-Signature 头部示例的外观:

DKIM-Signature: v=1;

a=rsa-sha256;

d=example.com;

s=burke;

c=simple/simple;

h=Content-Type;

x=0023423111111;

bh=2342322111;

b=M232234=

如你所见,它是一组以分号分隔的名称值对。虽然了解头部的结构并不那么重要,但以下是每个参数的解释:

v: 协议版本。始终为 1。

a: 用于哈希和签名的算法。目前 RESTEasy 只支持 RSA 签名和 SHA256 哈希算法。

d: 签名者的域。这用于识别签名者以及发现用于验证签名的公钥。

s: 域选择器。也用于识别签名者和发现公钥。

c: 规范化算法。目前只支持 simple/simple。基本上,这允许你在计算哈希之前转换消息体。

h: 签名计算中包含的头部列表,以分号分隔。

x: 签名何时过期。这是一个自纪元以来的时间秒的数值长值。允许签名者控制已签名消息的签名何时过期。

t: 签名的时间戳。自纪元以来的时间秒的数值长值。允许验证者控制签名何时过期。

bh: 消息体的 Base 64 编码哈希值。

b: Base 64 编码的签名。

现在我们有了这些信息,很明显,如果你想检查签名者,而不是使用字母d,我们必须使用字母s,而不是packtpub.com,我们将使用demo。一旦应用这些更改,我们的代码应该看起来像以下这样:

@Verify(identifierName = "s", identifierValue = "demo")
@POST
@Path("verifier")
@Produces("text/plain")
public String processRequestRam(String input) {
  int numberOfGbRequested = Integer.valueOf(input.split("-")[0]);
  if (numberOfGbRequested > 4) {
    return "deny";
  } else {
    return "accepted";
  }
}


此外,如果你想验证签名者的名称和域,你必须进行一些小的更改。这次,我们将使用@Verifications注解;这个注解接收一个@Verify注解数组作为参数,这允许我们执行我们之前描述的操作。在这种情况下,我们应该使用@Verify注解添加两个控制,我们的代码应该看起来像以下这样:

@Verifications({ 
@Verify(identifierName = "s", identifierValue = "demo"),
@Verify(identifierName = "d", identifierValue = "packtpub.com") })
@POST
@Path("verifier")
@Produces("text/plain")
public String processRequestRam(String input) {
  int numberOfGbRequested = Integer.valueOf(input.split("-")[0]);
  if (numberOfGbRequested > 4) {
    return "deny";
  } else {
    return "accepted";
  }
}

一旦我们应用了这些更改,我们就可以使用 SoapUI 进行请求。我们应该得到一个成功的执行结果,如下面的截图所示:

使用注释验证签名

消息体加密

在上一章中,我们看到了如何使用 HTTPS 加密完整的 HTTP 消息。现在,我们将解释如何仅加密消息体以及每个过程的区别。我们首先构建一个简单的示例,然后,随着我们对实现进行相应的测试,我们将了解它是如何工作的。

为了不破坏我们之前的工程,我们将构建一个新的。为此,我们将在终端中执行以下命令:

mvn archetype:generate -DgroupId=com.packtpub -DartifactId=encryption -DarchetypeArtifactId=webapp-javaee6 -DarchetypeGroupId=org.codehaus.mojo.archetypes

如本章前面所见,当你被要求输入版本号时,将 1.0-SNAPSHOT 的默认值更改为 1.0

当然,如果你想的话,你可以从以下 URL 下载所有源代码:

github.com/restful-java-web-services-security/source-code/tree/master/chapter05/encryption

现在,让我们将项目导入到 Eclipse 中,删除 pom.xml 文件中的现有默认依赖项,并添加对 resteasy-jaxrsresteasy-crypto 艺术品的依赖。

dependencies 部分应该看起来像以下这样:

  <dependencies>
    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-jaxrs</artifactId>
      <version>3.0.6.Final</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-crypto</artifactId>
      <version>3.0.6.Final</version>
    </dependency>
  </dependencies>

现在,让我们在包 com.packtpub 内创建一个名为 EncryptedService 的类。在这个类中,我们将创建一个非常简单的操作,如下所示:

package com.packtpub;

import javax.ws.rs.GET;
import javax.ws.rs.Path;

@Path("/encrypted")
public class EncryptedService {

  @GET
  public String gretting() {
    return "Hello world";
  }
}

为了注册我们应用程序的服务,让我们创建一个名为 EncryptedApplication 的类,如下所示:

package com.packtpub;

import java.util.HashSet;
import java.util.Set;

import javax.ws.rs.ApplicationPath;
import javax.ws.rs.core.Application;

@ApplicationPath("/services")
public class EncryptedApplication extends Application {

  private Set<Object> resources = new HashSet<Object>();

  public EncryptedApplication() throws Exception {
    resources.add(new EncryptedService());
  }

  @Override
  public Set<Object> getSingletons() {
    return resources;
  }
}

测试功能

之后,我们的应用程序应该就绪了。所以,让我们从 SoapUI 执行一个测试,使用 Wireshark 观察流量,如下面的截图所示:

测试功能

Wireshark 显示以下内容:

测试功能

如我们所见,流量分析器显示了所有信息是如何直接传输的,以及它被解释得有多容易。现在,让我们在 JBoss 上启用 HTTPS 来展示整个消息是如何加密的。

启用 HTTPS 服务器

因此,首先我们必须创建一个证书 KeyStore。我们可以通过在终端上执行以下命令来实现这一点:

keytool -genkey -alias tomcat -keyalg RSA

当它要求你输入密码时,你应该使用 changeit,因为我们已经在本书中使用过它。

现在,我们查看 JBOSS_HOME/standalone/configuration/standalone.xml 文件,在包含 <connector name="http" 的行中,并添加以下内容:

<connector name="https" protocol="HTTP/1.1" scheme="https" 
socket-binding="https" secure="true">
  <ssl/>
</connector>

一旦完成这个更改,我们将重新启动应用程序服务器,部署应用程序,并编辑请求。这次,我们将使用端口 8443 和 HTTPS 协议。因此,URL 应该看起来像以下这样:

https://localhost:8443/encryption-1.0/services/encrypted

让我们使用 SoapUI 执行请求;现在,我们的流量分析器将显示以下结果:

启用 HTTPS 服务器

如我们所预期,这次,分析器清楚地显示所有信息都已加密。

在我们的示例中继续前进,我们现在将在 JBoss 中禁用 HTTPS。为此,我们必须删除之前添加的连接器。现在,我们将使用 S/MIME 来仅加密响应的消息体。首先,让我们检查一些将帮助我们理解其工作原理的概念。

S/MIME 来自 Secure MIME。MIME 代表多用途互联网邮件扩展,它不仅帮助我们发送“Hello world”之类的消息,还可以发送视频、音频等更有趣的内容。MIME 与 SMTP 和 HTTP 等电子邮件协议一起工作。这有助于我们处理 RESTful S/MIME Web 服务。另一方面,MIME 为我们提供了以下功能:

  • 消息加密

  • 验证发送消息的用户身份

  • 验证消息信息完整性的能力

由于 S/MIME 与证书一起工作,这是消息发送者信息保存的地方。当接收者收到消息时,他们会观察到消息的所有公开部分。然后,可以使用密钥解密消息。此外,接收者可以访问其内容。如果您想进一步了解 S/MIME,我们建议您访问链接datatracker.ietf.org/wg/smime/charter/

让我们先做一些修改。首先,我们在应用中创建源文件夹src/main/resources;在这个目录中,我们将放置加密消息所需的资源。

然后,我们使用openssl生成证书,从控制台进入我们刚刚创建的目录,并在终端上运行以下命令:

openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout demokey.pem -out democert.pem

现在,我们必须输入如下截图所示的信息:

启用服务器的 HTTPS

这将生成两个文件:demokey.pem,这是一个私钥,以及democert.pem,这是我们用来加密消息体的证书。为了表示已签名的响应,RESTEasy 使用EnvelopedOutput对象。在以下图中,我们向您展示了 RESTEasy 如何加密消息:

启用服务器的 HTTPS

因此,我们必须替换EncryptedService类中gretting()方法的返回类型。让我们将字符串更改为EnvelopedOutput,并使用我们之前生成的证书加密消息体。应用这些更改后,我们的方法应如下所示:

@GET
public EnvelopedOutput gretting() throws Exception {
  InputStream certPem = Thread.currentThread()
                       .getContextClassLoader()
                       .getResourceAsStream("democert.pem");
  X509Certificate myX509Certificate = PemUtils.
      decodeCertificate(certPem)
  EnvelopedOutput output = new 
    EnvelopedOutput("Hello world", MediaType.TEXT_PLAIN);
  output.setCertificate(myX509Certificate);
  return output;
}

让我们在pom.xml文件中进行一些修改。我们将按照以下方式修改dependencies部分:

  <dependencies>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.8.1</version>
    </dependency>
    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-jaxrs</artifactId>
      <version>3.0.6.Final</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-jaxb-provider</artifactId>
      <version>3.0.6.Final</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>org.jboss.resteasy</groupId>
      <artifactId>resteasy-crypto</artifactId>
      <version>3.0.6.Final</version>
    </dependency>
  </dependencies>

注意我们如何更改了resteasy-jaxrsresteasy-jaxb-provider组件的作用域;这是在加密消息时避免重复类所必需的。由于这些组件是应用服务器内的模块,您需要指明我们想要加载它们。为此,我们将修改pom.xml文件中的maven-war-plugin插件部分,如下所示:

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-war-plugin</artifactId>
  <configuration>
    <failOnMissingWebXml>false</failOnMissingWebXml>
    <archive>
      <manifestEntries>
        <Dependencies>org.jboss.resteasy.resteasy-jaxb-provider export, org.jboss.resteasy.resteasy-jaxrs export</Dependencies>
      </manifestEntries>
    </archive>
  </configuration>
</plugin>

由于 JBoss 版本 7 是一个基于模块的应用服务器,默认情况下,启动时只激活了少数几个模块。如果您想访问其他模块,则需要明确指出这些依赖项。这可以通过MANIFEST.MF文件或创建一个名为jboss-deployment-structure.xml的文件来完成。

在这种情况下,我们将使用 maven-war- 插件选择第一个文件,以指示所需的依赖项。

测试功能

现在,让我们再次从 SoapUI 向 URL http://localhost:8080/encryption-1.0/services/encrypted 发送请求。

这次,我们将得到的响应如下截图所示:

测试功能

以下是从流量分析器中可以看到的内容:

测试功能

如我们所见,它显示的内容与 SoapUI 的响应非常相似。为了解密内容,我们必须拥有私钥和证书。通过这两个资源,我们可以获取对象 EnvelopedInput 并从中获取消息,如图所示:

测试功能

这将通过以下代码中的单元测试进行演示。然而,在继续之前,我们想展示当使用 S/MIME 加密消息时,标题仍然是可读的,但消息体是完全加密的。因此,如果我们没有这些资源,信息就是过时的,无法解释。

现在,我们将编写一个类,使我们能够读取消息体。为此,我们将创建一个新的源文件夹,名为 src/main/test

在这个文件夹中,让我们创建一个名为 com.packtpub.EncryptedServiceTest 的类,其内容如下:

package com.packtpub;

import java.security.PrivateKey;
import java.security.cert.X509Certificate;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.WebTarget;

import junit.framework.Assert;

import org.jboss.resteasy.client.jaxrs.ResteasyClientBuilder;
import org.jboss.resteasy.security.PemUtils;
import org.jboss.resteasy.security.smime.EnvelopedInput;
import org.junit.Test;

public class EncryptedServiceTest {

  @Test
  public void testEncryptedGet() throws Exception {
    // LOADING THE CERTIFICATE
    X509Certificate myX509Certificate = PemUtils.decodeCertificate(
        Thread
        .currentThread().getContextClassLoader()
        .getResourceAsStream("democert.pem"));
    // LOADING THE KEY
    PrivateKey myPrivateKey = PemUtils.decodePrivateKey(Thread
        .currentThread().getContextClassLoader()
        .getResourceAsStream("demokey.pem"));
    // CREATING A CLIENT FOR THE WEB SERVICE
    Client client = new ResteasyClientBuilder().build();
    WebTarget target = client.target(
      "http://localhost:8080/encryption-1.0/services/encrypted"
    );
    // RETRIEVING THE RESULT OF METHOD EXECUTION
    EnvelopedInput<?> input = target.request().
            get(EnvelopedInput.class);
    Assert.assertEquals("Hello world",
        input.getEntity(String.class, 
        myPrivateKey, myX509Certificate));
    client.close();
  }

}

注意,我们需要私钥和证书来解密消息,以获取由包含消息 Hello world 的字符串形成的实体。

当我们运行这个单元测试时,如果一切顺利,我们应该得到一个绿色的条形。这表明,为了解密消息,使用之前的资源(私钥和证书)已经获得了预期的消息。

摘要

在本章中,我们处理了数字签名,并学习了如何在 RESTful 网络服务中使用它们。这些天,数字签名经常被使用,因为它们保证了消息的完整性,信息在从发送者传输到接收者的过程中不会被破坏。我们已经知道信息在传输过程中可能会被修改,但在验证签名信息时,接收者可以注意到这一点,并采取他们认为适当的行动。例如,他们可以发送另一个请求以避免处理损坏的信息。在本章的结尾,我们处理了消息体加密,并看到了使用这些加密与 HTTPS 之间的区别。最后,我们看到了接收者如何使用密钥来解析消息体,并根据他们的需求使用信息。

posted @ 2025-09-10 15:11  绝不原创的飞龙  阅读(1)  评论(0)    收藏  举报