vlambda博客
学习文章列表

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

Securing Applications

安全性是每个企业系统的关键要求。在本章中,我们将学习如何使用各种方法有效地保护 Quarkus 服务。我们将付诸实践的第一种方法是将安全层嵌入到我们的服务中。这仍然可以被认为是快速应用程序开发和测试的有效解决方案。另一方面,在将我们的服务转移到生产环境时,我们需要避免这种极端的集中化。因此,我们将学习的下一个策略是 Quarkus 服务如何连接到 Keycloak 等分布式安全系统。本章的最后一个主题是通过一些简单的配置步骤来加密 HTTP 通道。

在本章中,我们将介绍以下主题:

  • Securing our customer service
  • Securing Quarkus services with Elytron
  • Securing Quarkus services with Keycloak
  • Securing Quarkus services with MicroProfile JWT
  • Using HTTPS with Quarkus

Securing our customer service

Quarkus 安全基础架构源自标准 Java Enterprise Edition (Java EE) 规范,该规范基于简单的基于角色的安全模型。通过使用它,您可以通过注释和配置文件指定您的安全约束。

在 Java 注释方面,以下注释可用于指定可应用于单个方法或类的安全约束:

  • @javax.annotation.security.RolesAllowed: This is the most common annotation as it specifies one or more roles that have been authorized to invoke a certain method or class.
  • @javax.annotation.security.RunAs: This annotation assigns a role dynamically during the invocation of a method or class. It can be a handy option if we need to temporarily allow the execution of some methods.
  • @javax.annotation.security.PermitAll: This annotation allows us to release security constraints from methods. It can be useful in some scenarios where you haven't identified which role will be entitled to invoke a method.
  • @javax.annotation.security.DenyAll: This annotation is the exact opposite of @PermitAll as it denies access to a method or class that bears this annotation.

为简单起见,我们将为我们的客户服务应用程序定义一个简单的安全策略。这将包括两个角色:

  • User role: This role will be entitled to perform read-only operations, such as querying the Customer list.
  • Admin role: This role will be entitled to perform all the available operations, including create, update, and delete.

以下是我们的 CustomerEndpoint 类的代码,该类已用 @RolesAllowed 安全注释进行了修饰:

public class CustomerEndpoint {

    @Inject CustomerRepository customerRepository;

    @GET
    @RolesAllowed("user")
    public List<Customer> getAll() {
        return customerRepository.findAll();
    }

    @POST
    @RolesAllowed("admin")
    public Response create(Customer customer) {

        customerRepository.createCustomer(customer);
        return Response.status(201).build();

    }

    @PUT
    @RolesAllowed("admin")
    public Response update(Customer customer) {
        customerRepository.updateCustomer(customer);
        return Response.status(204).build();
    }
    @DELETE
    @RolesAllowed("admin")
    public Response delete(@QueryParam("id") Long customerId) {
        customerRepository.deleteCustomer(customerId);
        return Response.status(204).build();
    }

}

为简洁起见,我们不会在此处包含 OrderEndpoint 类,该类已以相同方式更新,以保护具有 user 角色的读取方法和编写具有 admin 角色的方法。

定义了我们的安全策略后,我们现在可以选择将哪个安全提供程序应用于我们的服务。这将要求我们在 application.properties 中添加正确的设置,并将依赖项包含在我们项目的 pom.xml 文件中。

我们将从 Elytron 安全提供商开始,它不需要我们安装任何外部应用程序或工具来保护我们的服务。

Securing Quarkus services with Elytron

Elytron 是一个安全框架,旨在统一 WildFly 和 JBoss 的 企业应用平台 (EAP) 的安全方面。因此,该框架最初是为单体应用程序设计的,以便覆盖安全性的各个方面。在 Quarkus 等容器就绪的原生平台中使用 Elytron 有什么优势?

尽管它可能看起来像是保护资产的过于简单的解决方案,但在开发或测试包含安全角色的应用程序时,它可以证明是有利的。 Quarkus 开箱即用地提供了一个基于文件的安全领域,以便提供基于角色的访问控制 (RBAC)我们具有最低配置要求的基本端点。

在库方面,在编写本书时,我们可以使用三个可用的 Elytron 扩展来保护我们的应用程序:

  • quarkus-elytron-security-properties-file: Provides support for basic authentication via property files.
  • quarkus-elytron-security-jdbc: Provides support for database authentication via JDBC.
  • quarkus-elytron-security-oauth2: Provides support for OAuth2 authentication. This extension may be deprecated in future versions of Quarkus and replaced by a reactive Vert.x version.

由于我们的客户应用程序已经使用数据库作为后端,我们将向您展示如何使用数据库身份验证。在继续之前,请查看 Chapter07/elytron-demo 文件夹中的源代码。

pom.xml 文件所示,我们在项目中添加了以下扩展名:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-elytron-security-jdbc</artifactId>
</dependency>

要配置身份验证,我们需要指定哪个表包含用户和角色列表。我们还需要添加几个具有不同角色的用户。为此,我们在 import.sql 脚本中包含了以下 SQL 语句,这些脚本位于 src/main/resources 文件夹中:

CREATE TABLE quarkus_user (
    id INT,
    username VARCHAR(255),
    password VARCHAR(255),
    role VARCHAR(255)
);
INSERT INTO quarkus_user (id, username, password, role) VALUES (1, 'admin', 'password123', 'admin');
INSERT INTO quarkus_user (id, username, password, role) VALUES (2, 'frank','password123', 'user');

现在,在 application.properties 文件中,我们需要通过为其提供一些基本配置参数来激活 JDBC 身份验证。这是我们添加的属性列表:

quarkus.security.jdbc.enabled=true
quarkus.security.jdbc.principal-query.sql=SELECT u.password, u.role FROM quarkus_user u WHERE u.username=?
quarkus.security.jdbc.principal-query.clear-password-mapper.enabled=true
quarkus.security.jdbc.principal-query.clear-password-mapper.password-index=1
quarkus.security.jdbc.principal-query.attribute-mappings.0.index=2
quarkus.security.jdbc.principal-query.attribute-mappings.0.to=groups 

让我们快速讨论这些属性:

  • The quarkus.security.jdbc.enabled property, when set to true, enables JDBC authentication.
  • The quarkus.security.jdbc.principal-query.sql property is used to specify the SQL statements that will check for a valid username/password combination.
  • The quarkus.security.jdbc.principal-query.clear-password-mapper.enabled property, when set to true, configures a mapper that maps a column that's returned from a SQL query to a clear password key type.
  • The quarkus.security.jdbc.principal-query.clear-password-mapper.password-index property sets the column index from the clear text authentication query.
  • Finally, quarkus.security.jdbc.principal-query.attribute-mappings.0.index and quarkus.security.jdbc.principal-query.attribute-mappings.0.to are used to bind the second field in the authentication query (index=2) with the principal's role (groups).

这就是使用 Elytron 安全域保护服务所需的全部内容。将所有部分放在正确的位置后,我们将使用一个测试类来验证针对 REST 端点的身份验证。

Creating a test class that performs basic authentication

我们的测试类需要调整以适应新的安全场景。在实践中,我们需要发送带有用户凭据的头文件以及 HTTP 请求,以授权服务的执行。感谢 Fluent RestAssured API,将 auth() 方法插入到我们的 HTTP 请求中相当容易,如下面的代码所示:

@Test
public void testCustomerService() {
    // Test GET for Customer size
    given()
            .auth()
            .preemptive()
            .basic("frank", "password123")
            .when().get("/customers")
            .then()
            .statusCode(200)
            .body("$.size()", is(2));


    JsonObject objOrder = Json.createObjectBuilder()
            .add("item", "bike")
            .add("price", new Long(100))
            .build();


    // Test POST Order for Customer #1
    given()
            .auth()
            .preemptive()
            .basic("admin", "password123")
            .contentType("application/json")
            .body(objOrder.toString())
            .when()
            .post("/orders/1")
            .then()
            .statusCode(201);

    // Create new JSON for Order #1
    objOrder = Json.createObjectBuilder()
            .add("id", new Long(1))
            .add("item", "mountain bike")
            .add("price", new Long(100))
            .build();

    // Test UPDATE Order #1
    given()
            .auth()
            .preemptive()
            .basic("admin", "password123")
            .contentType("application/json")
            .body(objOrder.toString())
            .when()
            .put("/orders")
            .then()
            .statusCode(204);

    // Test GET for Order #1
    given()
            .auth()
            .preemptive()
            .basic("admin", "password123")
            .when().get("/orders?customerId=1")
            .then()
            .statusCode(200)
            .body(containsString("mountain bike"));

    // Test DELETE Order #1
    given()
            .auth()
            .preemptive()
            .basic("admin", "password123")
            .when().delete("/orders/1")
            .then()
            .statusCode(204);


}
Please note that we have to use a preemptive basic authentication. This means that the authentication details are sent in the request header immediately, regardless of whether the server has already challenged the authentication. Without that, the current Vert.x implementation in Quarkus would return an unauthorized response.

假设 PostgreSQL 数据库已经启动,我们的测试类就可以执行了:

mvn compile test

您应该看到所有 CRUD 操作都已成功完成:

[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 5.799 s - in com.packt.quarkus.chapter7.CustomerEndpointTest
 2019-08-16 15:30:12,281 INFO  [io.quarkus] (main) Quarkus stopped in 0.012s
[INFO]
[INFO] Results:
[INFO]
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

在下一节中,我们将介绍如何使用 Keycloak 来利用 OpenID (https://openid.net/) 安全性在我们的应用程序中的标准。

Securing Quarkus services with Keycloak

Keycloak (https://www.keycloak.org/) 是开源的建立在 WildFly 应用服务器之上的访问管理解决方案。您可以在您的架构中采用它来利用各种功能,例如:

  • Client adapters
  • Single Sign-On (SSO)
  • Identity management and social login
  • Standard protocols (OpenID Connect or SAML)
  • A rich admin console
  • A user account management console

由于这些功能和连接到现有身份标准的能力,Keycloak 已成为许多大型组织的事实上的标准。它的受支持版本,称为 Red Hat Single Sign-On (RH-SSO: https://access.redhat.com/products/red-hat-single-sign-on),也可用于企业客户。

安装后,Keycloak 将充当网络中应用程序的主要安全端点。因此,您的应用程序不必添加登录表单来验证用户并存储他们的凭据。相反,应用程序被配置为指向 Keycloak,它支持 OpenID 或 SAML 等协议标准来保护您的端点。

简而言之,客户端应用程序将从他们的域重定向到 Keycloak 的身份服务器,在那里他们展示他们的凭据。这样,您的服务与您的安全策略和用户凭据完全隔离。相反,服务被授予数字签名的身份令牌或断言。这些令牌保存身份信息(例如姓名或电子邮件地址),但也可以保存有关被授权执行业务操作的角色的信息。在下一节中,我们将学习如何配置 Keycloak,以便我们可以发布可用于访问我们的示例客户服务的令牌。

Adding a Keycloak extension to our service

Chapter07/keycloak-demo 文件夹中,您会发现我们的客户服务应用程序的另一个版本,它使用 Keycloak 来保护 REST 端点。为了使用 Keycloak 授权和 OpenID 扩展,我们在 pom.xml 文件中包含了以下一组依赖项:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-keycloak-authorization</artifactId>
</dependency>
<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-oidc</artifactId>
</dependency>

现在所有必需的库都已准备就绪,让我们学习如何安装 Keycloak Identity Server 并在其中加载安全领域。

Setting up Keycloak

在本节中,我们将使用 Docker 快速掌握 Keycloak。首先,我们需要拉取 Keycloak 镜像并启动一个容器实例。以下命令将在后台启动 Keycloak Server,并选择 8180 作为 HTTP 端口并在本地公开主机和端口:

docker run --rm  \
   --name keycloak \
   -e KEYCLOAK_USER=admin \
   -e KEYCLOAK_PASSWORD=admin \
   -p 8180:8180 \
   -it quay.io/keycloak/keycloak:7.0.1 \
   -b 0.0.0.0 \
   -Djboss.http.port=8180 \
   -Dkeycloak.profile.feature.upload_scripts=enabled

在控制台中,您应该看到服务器已成功启动:

10:33:15,519 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0060: Http management interface listening on http://127.0.0.1:9990/management
 10:33:15,519 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0051: Admin console listening on http://127.0.0.1:9990
 10:33:15,519 INFO  [org.jboss.as] (Controller Boot Thread) WFLYSRV0025: Keycloak 6.0.1 (WildFly Core 8.0.0.Final) started in 15991ms - Started 672 of 937 services (652 services are lazy, passive or on-demand)

现在 Keycloak 已经启动并运行,让我们加载一个领域,其中包含我们将应用于我们的服务的有效安全配置。

Defining the security realm

Keycloak 配置的一个关键方面是安全领域,它包含与一个安全上下文相关的所有配置(用户、角色、客户端策略等)。当您第一次启动 Keycloak 时,它将只包含一个领域:ma​​ster 领域。这是领域层次结构中的最高级别。您不应使用此领域来配置组织中的用户和服务。相反,请考虑为负责定义组织中其他领域的管理员使用主领域。

本章的 GitHub 存储库包含一个名为 Quarkus 领域 的应用程序领域,这对我们的目的很有用。我们将向您展示如何导入它,然后我们将逐步完成其配置,以便您能够基于此模板创建新领域。让我们继续以下步骤:

  1. Start by connecting to the Keycloak console, which is available at http://localhost:8180. An authentication challenge will be displayed. Log in with admin/admin:
读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序
  1. Now, from the top-left panel, choose to add a new realm, as shown in the following screenshot:
读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序
  1. Choose to Import a realm and point to the JSON file (quarkus-realm.json) that contains an export of Quarkus' realm:
读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序
  1. Click on Create to continue. Now, let's look at a short overview of the realm's options.

在领域设置窗口中,您将能够定义一些核心设置,例如域名和登录设置,预定义令牌的生命周期和超时等等:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

出于我们学习路径的目的,我们不会更改这些设置。在领域面板中,您将能够验证我们是否包含了相同的角色(adminuser),以便他们匹配我们现有的安全约束:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

在这里,我们重新创建了我们在基于文件的 Elytron 域中测试过的相同用户列表:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

Role Mappings 选项卡中,您可以检查 test 用户是否是 用户角色:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

另一方面,admin 用户将同时分配给 admin用户 角色:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

现在我们已经查看了用户和角色,让我们讨论领域客户端配置的一个关键方面,它显示在以下 UI 中:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

让我们看一下客户端配置的一些关键方面。首先,我们必须定义客户端协议。有两种类型的客户端身份验证协议:

  • OpenID Connect (OIDC) is an authentication system where the client requests an access token that's used to call other services on behalf of the authenticated user.
  • SAML authentication requires Keycloak to provide an SSO for the users of your organization.

对于我们的需求,我们需要基于 OIDC 令牌的身份验证来授予对我们服务的访问权限。我们还需要选择是使用标准流程还是隐式流程进行身份验证。

默认选项(启用标准流程)涉及到/来自 OIDC 提供程序 (Keycloak) 的初始浏览器重定向,以进行用户身份验证和同意。然后,需要第二个反向通道请求来检索 ID 令牌。此流程提供了最佳的安全性,因为令牌不会显示给浏览器,并且客户端可以安全地进行身份验证。

让我们以序列图的形式来描述这个流程:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

为了提供更好的性能,Keycloak 还支持隐式流,在成功验证后立即发送访问令牌时会发生这种情况。尽管此选项可能会更好地扩展(因为没有额外的请求将代码交换为令牌),但您将负责监控令牌何时过期,以便您可以发布新的令牌。

由于我们选择使用标准流程,我们将指定一个适当的 有效重定向 URI,它需要设置为我们在 Quarkus 上运行的应用程序的默认 HTTP 端口。

对于 Access Type,我们已将其配置为 confidential,这需要客户端应用程序提供秘密才能获得 ID 令牌。当您将 Access Type 设置为 confidential 时,您将能够选择 Client Authenticator 来自 Credentials 选项卡,该选项卡定义了您将用于客户端的凭据类型及其机密。为此客户端 ID 定义的密钥是 mysecret,如以下屏幕截图所示:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

如果您想更改默认密码,只需单击 Regenerate Secret 按钮并相应地更新您的客户端应用程序。

现在我们已经配置了 Keycloak,我们将使用这个领域来执行一个使用不记名令牌认证的测试。但是,在此之前,我们将相应地配置我们的 Quarkus 服务。

Configuring Quarkus for Keycloak

信不信由你,一旦我们配置了身份服务器,我们在 Quarkus 应用程序中就没有太多工作要做了。我们只需要将 Keycloak URL 和客户端设置提供给我们应用的 application.properties 配置文件:

keycloak.url=http://localhost:8180

quarkus.oidc.enabled=true
quarkus.oidc.auth-server-url=${keycloak.url}/auth/realms/quarkus-realm
quarkus.oidc.client-id=quarkus-client
quarkus.oidc.credentials.secret=mysecret# Enable Policy Enforcement
quarkus.keycloak.policy-enforcer.enable=true

quarkus.http.cors=true

在前面的配置中,我们提供了连接 Keycloak Identity Server 的强制设置。我们还添加了一个名为 keycloak.url 的属性来定义 Keycloak 的 IP 地址和端口。在下表中,我们添加了有关每个参数的一些详细信息:

Parameter Description
quarkus.oidc.enabled When set to true, the OIDC extension will be enabled.
quarkus.oidc.auth-server-url The root URL where Keycloak authenticates client requests.
quarkus.oidc.client-id The client ID.
quarkus.oidc.credentials.secret The client secret.
quarkus.keycloak.policy-enforcer.enable By enabling the policy enforcer, requests are not allowed in, although there is no policy associated with that resource.

除此之外,如果您计划在与您的应用程序不同的机器上运行 Keycloak,建议启用 HTTP CORS 以便您可以访问 Keycloak 跨域边界(查看第 5 章使用 Quarkus 管理数据持久性< /em>,有关此内容的更多详细信息)。

现在,让我们深入研究将用于执行已通过 Keycloak 的 Identity Server 授权的 CRUD 操作的测试类。

Coding the test class

我们的测试类由两个块组成。在第一个块中,我们检索 test 用户和 admin 用户的令牌。然后,我们使用这两个令牌来测试应用程序。更具体地说,属于用户角色的 test 令牌将用于 GET 请求。另一方面,admin 令牌将用于授权 POSTPUTDELETE 请求。

这是测试类的第一个块:

@ConfigProperty(name = "keycloak.url")
String keycloakURL;

    @Test
    public void testHelloEndpoint() {

        RestAssured.baseURI = keycloakURL;
        Response response = given().urlEncodingEnabled(true)
                .auth().preemptive().basic("quarkus-client", 
                  "mysecret")
                .param("grant_type", "password")
                .param("client_id", "quarkus-client")
                .param("username", "test")
                .param("password", "test")
                .header("Accept", ContentType.JSON.getAcceptHeader())
                .post("/auth/realms/quarkus-realm/protocol/openid-
                  connect/token")
                .then().statusCode(200).extract()
                .response();

        JsonReader jsonReader = Json.createReader(new 
         StringReader(response.getBody().asString()));
        JsonObject object = jsonReader.readObject();
        String userToken = object.getString("access_token");

        response = given().urlEncodingEnabled(true)
                .auth().preemptive().basic("quarkus-client", 
                  "mysecret")
                .param("grant_type", "password")
                .param("client_id", "quarkus-client")
                .param("username", "admin")
                .param("password", "test")
                .header("Accept", ContentType.JSON.getAcceptHeader())
                .post("/auth/realms/quarkus-realm/protocol/openid-
                  connect/token")
                .then().statusCode(200).extract()
                .response();

        jsonReader = Json.createReader(new 
         StringReader(response.getBody().asString()));
        object = jsonReader.readObject();
        String adminToken = object.getString("access_token");

       // Test CRUD Methods here

     }

此代码针对我们的 Keycloak 领域的身份验证 URL 发出 POST 请求。该请求包含用户名和密码,以及作为参数的客户端 ID (quarkus_client) 及其密钥 (mysecret)。 RESTAssured API 验证返回状态码 200 并返回响应对象。然后,我们提取了 JSON 响应中包含的令牌,该令牌位于 access_token 键下。

如果要调试令牌声明的低级详细信息,可以使用 curl 等工具来检查 Keycloak 返回的响应。例如,如果你要为 test 用户请求一个令牌,那么下面是一个简单的 curl 命令来完成这项工作:

curl -X POST http://localhost:8180/auth/realms/quarkus-realm/protocol/openid-connect/token \
    --user quarkus-client:mysecret \
    -H 'content-type: application/x-www-form-urlencoded' \
    -d 'username=test&password=test&grant_type=password'

在第二个代码块中,我们通过在每个 REST 调用中包含用户的令牌来访问我们的服务。我们使用 oauth2 方法来做到这一点:

RestAssured.baseURI = "http://localhost:8081";
given().auth().preemptive()
        .oauth2(userToken)   
        .when().get("/customers")
        .then()
        .statusCode(200)
        .body("$.size()", is(2));


JsonObject objOrder = Json.createObjectBuilder()
        .add("item", "bike")
        .add("price", new Long(100))
        .build();

// Test POST Order for Customer #1

given().auth()
        .oauth2(adminToken)
        .contentType("application/json")
        .body(objOrder.toString())
        .when()
        .post("/orders/1")
        .then()
        .statusCode(201);

// Create new JSON for Order #1
objOrder = Json.createObjectBuilder()
        .add("id", new Long(1))
        .add("item", "mountain bike")
        .add("price", new Long(100))
        .build();

// Test UPDATE Order #1
given().auth()
        .oauth2(adminToken)
        .contentType("application/json")
        .body(objOrder.toString())
        .when()
        .put("/orders")
        .then()
        .statusCode(204);

// Test GET for Order #1
given().auth()
        .oauth2(adminToken)
        .when().get("/orders?customerId=1")
        .then()
        .statusCode(200)
        .body(containsString("mountain bike"));

// Test DELETE Order #1
given().auth()
        .oauth2(adminToken)
        .when().delete("/orders/1")
        .then()
        .statusCode(204);

可以通过以下命令执行测试:

$ mvn compile test

您应该期望它成功完成,如下所示:

[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.84 s - in com.packt.quarkus.chapter7.CustomerEndpointTest
2019-08-24 12:28:28,056 INFO  [io.quarkus] (main) Quarkus stopped in 0.011s
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

恭喜!您刚刚设法为您的服务设置了企业级安全基础架构。

Gathering principal and role information at runtime

在继续下一个身份验证模式之前,值得注意的是,您可以通过注入代表当前登录的 SecurityIdentity 接口 在运行时确定主体名称和角色-in user. 在这段简短的摘录中,我们将学习如何检索和记录已连接的用户、他们用于注册的姓名/姓氏以及他们有权获得的角色集:

@Inject SecurityIdentity securityContext;
@Inject CustomerRepository customerRepository;

@GET
@RolesAllowed("user")
public List<Customer> getAll() {

    LOGGER.info("Connected with User 
     "+securityContext.getPrincipal().getName());
    Iterator<String> roles = securityContext.getRoles().iterator();
    while (roles.hasNext()) {
       LOGGER.info("Role: "+roles.next());
    }
    return customerRepository.findAll();
}

在这种情况下,当访问客户列表时,服务将记录以下信息:

Connected with User test
Role: offline_access
Role: uma_authorization
Role: user

您可以通过查看其源代码来检查 SecurityIdentity 接口中的所有可用方法,该源代码位于 https://github.com/quarkusio/quarkus-security/blob/master/src/main/ java/io/quarkus/security/identity/SecurityIdentity.java.

Securing Quarkus services with MicroProfile JWT

在前面的示例中,我们介绍了如何使用 Keycloak 使用不记名令牌对请求进行身份验证和授权。然而,单独的不记名令牌是一种简化的安全模式,因为它基于交换潜在的任意字符串。

任何拥有有效不记名令牌的客户端都可以使用它来访问相关资源,而无需证明他/她的身份,这只能通过加密密钥进行验证。为了填补这一空白,我们将学习如何使用 JSON Web Tokens (JWTs),一种用于令牌的编码标准,使用可以签名的 JSON 数据有效负载,并且加密。 JWT 包括以下部分:

  • Header: This is a Base64-encoded string and consists of two parts: the type of the token, which is JWT, and the hashing algorithm being used, such as HMAC SHA256 or RSA. Here is a sample decoded header JWT:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

  • Payload: This is also a Base64-encoded string that contains claims. Claims are statements about an entity (user or group) and additional metadata. Here is a sample payload that's been returned to our service:
读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序
  • Signature: The signature is used to verify that the message wasn't altered along the way. In the case of tokens that have been signed with a private key, it can also assert that the sender of the JWT is who they say they are:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

JWT 令牌也可以由 Keycloak 提供,因此我们无需更改领域配置即可将其与 JWT 一起使用。另一方面,我们必须包含 groups 声明,以便 JWT 令牌将令牌主题的组成员身份映射到服务中定义的应用程序级角色。

此信息已通过 Token Claim Name 字段包含在我们的领域中(在我们客户端配置的 Mappers 部分):

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

现在,让我们配置我们的服务,以便它可以使用此身份验证模式。

Configuring our service to use JWT

我们的概念验证项目可以位于 Chapter07/jwt-demo 文件夹中。我们建议您将它导入到您的 IDE 中,以便与本章中的其他项目进行比较。

首先,我们将 Keycloak 授权和 OpenID 扩展替换为 quarkus-smallrye-jwt 扩展:

<dependency>
  <groupId>io.quarkus</groupId>
  <artifactId>quarkus-smallrye-jwt</artifactId>
</dependency>

然后,我们为我们的项目包含了一组不同的属性。以下是 application.properties 配置文件的代码,它针对同一个 Keycloak Server 并提供有关公钥位置和身份验证机制的详细信息:

keycloak.url=http://localhost:8180

# MP-JWT Config
mp.jwt.verify.publickey.location=${keycloak.url}/auth/realms/quarkus-realm/protocol/openid-connect/certs
mp.jwt.verify.issuer=${keycloak.url}/auth/realms/quarkus-realm
quarkus.smallrye-jwt.realmName=quarkus-realm

下表提供了每个属性的简要说明:

Parameter Description

mp.jwt.verify.publickey.location

The location where the provider's public key is stored. It can be a relative path or a URL.
mp.jwt.verify.issuer Specifies the value of the iss (short for issuer) claim of the JWT that the server will accept as valid.
quarkus.smallrye-jwt.realmName The security realm that's used for authentication.

现在,我们已经准备好使用 JWT 身份验证机制来执行我们的测试类了。

Running our test

我们的 CustomerEndpointTest 类包含我们用来验证 Keycloak 身份验证的相同代码。然而,在幕后,它将执行以下步骤:

  1. Request the access token.
  2. Validate the access token fields.
  3. Perform signature verification using the realm's RSA public key, which is available at the location defined in the mp.jwt.verify.publickey.location system property.

可以使用以下命令执行测试:

$ mvn compile test

您应该看到它成功完成,如下所示:

[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.463 s - in com.packt.quarkus.chapter7.CustomerEndpointTest
2019-08-24 15:37:29,879 INFO  [io.quarkus] (main) Quarkus stopped in 0.006s
[INFO] 
[INFO] Results:
[INFO] 
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0

至于纯 Keycloak 身份验证,让我们学习如何从 JWT 上下文中收集更多信息。

Injecting JWT claims and token information

为了以编程方式管理 JWT 中包含的信息,我们可以将相关 API 注入到我们的服务中。主类 org.eclipse.microprofile.jwt.JsonWebToken 可以使用以下命令简单地注入:

@Inject
JsonWebToken jwt;

此类提供了可用于检索令牌本身、其主题以及令牌中包含的声明的方法。可以在源代码中找到更多详细信息,该源代码位于 https://github.com/eclipse/microprofile-jwt-auth/blob/master/api/src/main/java/org/eclipse/microprofile/jwt /JsonWebToken.java.

另一方面,检索特定声明的快捷方式可以通过 @Claim 注释完成,其中包括其 standard 属性和 Claim< 的名称/kbd>。使用以下代码注入令牌声明中包含的组和用户名:

@Inject
@Claim(standard = Claims.groups)
Optional<JsonString> groups;

@Inject
@Claim(standard = Claims.preferred_username)
Optional<JsonString> currentUsername;

那是我们本章关于 Keycloak 的最后一个主题。现在,让我们再分析一个安全主题,即使用 SSL 进行 HTTP 通信。

Using HTTPS with Quarkus

本章的最后一节专门介绍 Quarkus 中的 HTTP 通信加密。为此,您需要在配置中提供有效的(自签名或由 CA 签名)密钥库或 PEM 证书。

首先,让我们学习如何生成自签名 PEM 密钥和证书对。最简单的方法是使用 OpenSSL 工具,如下所示:

 $ openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem

上述命令会在当前目录生成一个名为cert.pem的证书和一个名为key.pem的相关密钥文件。接下来,在 application.properties 中配置证书的文件系统路径和密钥文件:

quarkus.http.ssl.certificate.file=/path/cert.pem
quarkus.http.ssl.certificate.key-file=/path/key.pem

另一方面,您还可以生成和使用已经包含带有证书的默认条目的密钥库。您可以使用 keytool 实用程序生成它并为其提供密码:

$ keytool -genkey -keyalg RSA -alias quarkus -keystore keystore.jks -storepass password -validity 365 -keysize 2048
 
 Enter key password for <quarkus>
     (RETURN if same as keystore password):

在我们的例子中,将创建的唯一文件是 keystore.jks;我们可以将它包含在我们的配置中,如下所示:

quarkus.http.ssl.certificate.key-store-file=/path/keystore.jks
quarkus.http.ssl.certificate.key-store-password=password

最后,我们可以指定 Undertow 服务器用来绑定 HTTPS 协议的端口:

quarkus.http.ssl-port=8443

作为概念证明,您可以构建并运行包含在 Chapter07/https 中的应用程序,以验证它是否可以通过 SSL 端口访问:

读书笔记《hands-on-cloud-native-applications-with-java-and-quarkus》保护应用程序

您可以在浏览器栏中看到的警告仅表示正在使用的 SSL 证书 不是由受信任的机构颁发的。

在本节中,我们已经介绍了我们需要遵循的基本配置步骤,以便在传输级别保护我们的应用程序。现在,我们不通过明文渠道与我们的客服沟通;相反,我们使用 安全套接字层 (SSL) 来保护我们的连接。

Summary

我们从讨论可应用于 Quarkus 服务的安全策略开始本章。开箱即用,您可以使用 Elytron 扩展提供基于文件的安全身份验证和授权。然后,我们仔细研究了 Keycloak,它可以通过支持 OpenID 标准来提供企业级安全标准。我们介绍了一个使用不记名令牌的基本示例和一个使用数字签名令牌的更复杂的示例,两者均符合 JWT 规范。最后,我们发现了如何生成和配置证书以使用 HTTPS 保护对 Quarkus 端点的访问。

在下一章中,我们将介绍一些可以提高 Quarkus 服务未开发潜力的高级策略!