Quarkus-和-Kubernetes-的-Java-微服务高级教程-二-

158 阅读1小时+

Quarkus 和 Kubernetes 的 Java 微服务高级教程(二)

原文:Pro Java Microservices with Quarkus and Kubernetes

协议:CC BY-NC-SA 4.0

四、升级单体应用

编写源代码并运行它并不是真正的胜利!真正的成功是编写覆盖测试的代码,保证业务逻辑被正确实现。

测试覆盖率是一个非常重要的指标,它显示了代码的阴暗面。覆盖面越大,我们就越能确保我们的代码免受任何草率或肮脏的更新或重构。

在这个例子中,测试将是防止将这个单体应用拆分为微服务时出现问题的保护屏障。

实施 QuarkuShop 测试

quartus 中的测试库简介

在 Java 生态系统中,JUnit 是最常见的测试框架。这就是 Quarkus 在生成新项目时自动提供它作为测试依赖项的原因:

<dependencies>
...
    <!-- Test dependencies -->
    <dependency>
        <groupId>io.quarkus</groupId>
        <artifactId>quarkus-junit5</artifactId>
        <scope>test</scope>
    </dependency>

    <dependency>
        <groupId>io.rest-assured</groupId>
        <artifactId>rest-assured</artifactId>
        <scope>test</scope>
    </dependency>
</dependencies>

请注意放心库,它是随 JUnit 5 一起推出的。

What is Rest Assured?

Rest Assured 是一个非常容易用 Java 测试和验证 REST web 服务的库。它只是发出 HTTP 请求并验证 HTTP 响应。它有一组非常丰富的匹配器和方法来获取数据和解析请求/响应。它与构建工具(如 Maven)和 ide 有很好的集成。

放心框架使用核心 Java 知识使 API 自动化测试变得非常简单,这是一件非常值得做的事情。

对于这些测试,我们需要另一个库:AssertJ。

What is AssertJ?

AssertJ 是一个开源的社区驱动库,它提供了丰富的断言和真正有用的错误消息。它提高了测试代码的可读性,并且在任何 IDE 或构建工具中都非常容易使用。

下面是要添加到您的pom.xml文件中的 AssertJ Maven 依赖项:

<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <scope>test</scope>
</dependency>

要使用 Maven 运行测试,只需键入mvn verify。应用将在test概要文件下执行测试。为了定义测试配置属性,比如将用于测试的数据库,您需要将这些属性添加到带有前缀%testapplication.properties中。这个前缀通知应用这些属性是用于test概要文件的。

What is An Application Profile?

应用的开发生命周期有不同的阶段;最常见的有开发测试生产。Quarkus 概要文件对应用配置的各个部分进行分组,使它们只在特定的环境中可用。

一个配置文件是一组配置设置。Quarkus 允许您使用属性的前缀%profile来定义特定于概要文件的属性。然后,它会根据激活的配置文件自动加载属性。参见清单 4-1 。

img/509649_1_En_4_Figa_HTML.gif当没有%profile出现时,该属性与所有配置文件相关联。

img/509649_1_En_4_Figb_HTML.gif在测试这本书的概念验证时,我发现有多个application.properties存在一个问题,就像我们过去对 Spring Boot 那样。我在夸库斯 GitHub #11072 中打开了一个问题。Quarkus 团队负责人之一圣乔治·安德里亚纳基斯告诉我,强烈建议只有一个application.properties文件。

  • ①您需要定义专用test数据库实例的参数和凭证。

  • ②您需要激活 Flyway 进行测试。

...=
# Test Datasource config properties
%test.quarkus.datasource.db-kind=postgresql     ①
%test.quarkus.datasource.username=developer
%test.quarkus.datasource.password=p4SSW0rd
%test.quarkus.datasource.jdbc.url=jdbc:postgresql://localhost:5432/test

# Test Flyway minimal config properties
%test.quarkus.flyway.migrate-at-start=true      ②

Listing 4-1src/main/resources/application.properties

img/509649_1_En_4_Figc_HTML.gif不需要复制src/test文件夹中的 Flyway 迁移脚本。在入住src/test/resources之前,Flyway 会在src/main/resources找到他们。

说到数据库,我们需要一个专门用于测试的数据库。我们将使用TestContainers来提供通用数据库的轻量级实例作为 Docker 容器。我们不需要几千字来定义TestContainers。你会通过实践锻炼发现它,爱上它。

首先添加TestContainers Maven 依赖项,如下所示:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>junit-jupiter</artifactId>
    <version>1.15.3</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>postgresql</artifactId>
    <version>1.15.3</version>
    <scope>test</scope>
</dependency>

接下来,您将创建将TestContainers引入 Quarkus 的Glue类。这只是一个测试实用程序类。在测试代码下,在com.targa.labs.quarkushop.utils包中,您需要创建TestContainerResource类,如清单 4-2 所示。

  • TestContainerResource将实现QuarkusTestResourceLifecycleManager,它管理一个测试资源的生命周期。这些资源在第一次测试运行之前启动,在测试套件结束时关闭。使用@QuarkusTestResource(TestContainerResource.class)注释将该资源引入测试类。

  • ②这是这个自定义测试资源的核心元素。postgres:13参数是您将使用的 PostgreSQL Docker 映像的名称。

  • ③当调用start()方法时,首先启动DATABASE容器。

  • ④接下来,您收集由confMap中的TestContainers动态生成的Datasource凭证。当confMap返回时,这些凭证被应用,而不是那些在application.properties中可用的凭证。

  • ⑤当调用close()方法时,关闭DATABASE容器。

public class TestContainerResource implements QuarkusTestResourceLifecycleManager { ①

    private static final PostgreSQLContainer<?> DATABASE =
                                    new PostgreSQLContainer<>("postgres:13");   ②

    @Override
    public Map<String, String> start() {

        DATABASE.start();   ③

        Map<String, String> confMap = new HashMap<>();  ④

        confMap.put("quarkus.datasource.jdbc.url", DATABASE.getJdbcUrl());      ④
        confMap.put("quarkus.datasource.username", DATABASE.getUsername());     ④
        confMap.put("quarkus.datasource.password", DATABASE.getPassword());     ④

        return confMap;     ④
    }

    @Override
    public void stop() {
        DATABASE.close();   ⑤
    }
}

Listing 4-2src/test/com.targa.labs.quarkushop.utils.TestContainerResource

img/509649_1_En_4_Figd_HTML.gif从 Quarkus 1.13 开始,TestContainers不再需要,新特性叫做 DevServices。

DevServices 为您提供开箱即用的零配置数据库。根据您的数据库类型,您可能需要安装 Docker 才能使用该功能。很多数据库都支持 DevServices,比如 PostgreSQL,MySQL 等。

如果您想使用 DevServices,您需要做的就是包含您想要的数据库类型的相关扩展(反应式或 JDBC,或两者都有),并且不要配置数据库 URL、用户名和密码。Quarkus 将提供数据库,您可以开始编码,不用担心配置。

要了解更多关于 DevServices 的信息,请看这里: https://quarkus.io/guides/datasource#dev-services

虽然 Quarkus 默认监听端口 8080,但在运行测试时,它默认监听端口 8081。这允许您在并行运行应用的同时运行测试。这个 HTTP 测试端口可以使用quarkus.http.test-port=9999属性在application.properties中更改为 9999。

img/509649_1_En_4_Fige_HTML.gif如果把quarkus.http.test-port=8888%test.quarkus.http.test-port=9999插入application.properties会怎么样?

放轻松!这里,您处理的是test概要文件的 HTTP 端口。因此带有%test的属性将覆盖在它之前定义的任何值。当运行测试时,您将看到测试的运行时公开了 9999 端口。

您需要的最后一个配置是测量测试覆盖率,这是衡量代码质量的一个非常重要的指标。我们将使用JaCoCo来生成代码覆盖报告。

首先,您需要在pom.xml文件中添加JaCoCo Quarkus 扩展名:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-jacoco</artifactId>
    <scope>test</scope>
</dependency>

不需要更多的 Maven 配置!夸库斯扩展将会完成所有的魔法!img/509649_1_En_4_Figf_HTML.gif

稍后您将使用这个JaCoCo报告来查看您的测试覆盖了多少。

编写第一个测试

让我们从测试第一个 REST API 开始:Cart REST API。img/509649_1_En_4_Figg_HTML.gif

基于CartResource或 Swagger UI,注意有六个服务:

img/509649_1_En_4_Figh_HTML.jpg

因此您将至少有六个测试,每个服务一个测试。

在开始测试之前,清单 4-3 展示了一个典型的test类在 Quarkus 世界中的样子。

  • Quarkus 中控制 JUnit 5 测试框架的注释。

  • ②使TestContainerResource对您的CartResourceTest可用的注释。

  • ③用于表示带注释的方法是一个测试。

@QuarkusTest@QuarkusTestResource(TestContainerResource.class)class CartResourceTest {

    @Testvoid testSomeOperationOrFeature() {

    }
}

Listing 4-3src/test/java/com/targa/labs/quarkushop/web

我们将创建测试来验证给定所需的输入,并且在进行正确的调用时,我们将获得预期的结果。

让我们首先创建一个典型的测试:findAllCarts用于在/api/carts上使用 HTTP GET 请求列出所有购物车。这个 REST API 将返回一个CartDto数组。我们可以使用 JUnit 5 将这个用例转化为一个测试,并轻松放心:

  • ①启动一个放心测试用例,向/carts REST API 发送一个 HTTP GET 请求。

  • ②提取前一行中请求的放心ValidatableResponse

  • ③验证响应状态代码是否与200 OK匹配,响应状态代码由OK.getStatusCode()从来自放心的Response.Status枚举中返回。

  • ④验证 JSON 或 XML 响应主体元素size()符合 Hamcrest 匹配器greaterThan(0)

@Test
void testFindAll() {
    get("/carts")    ①
        .then()             ②
            .statusCode(OK.getStatusCode())    ③
            .body("size()", greaterThan(0));   ④
}

总而言之,这个测试将验证在/cart上执行 HTTP GET 将返回以下内容:

  • 包含作为状态代码的200的标题

  • 具有非空元素数组的主体

    img/509649_1_En_4_Figi_HTML.gif在之前的调用中,我们调用了/carts路径而不是/api/carts,因为/api根基础是由quarkus.http.root-path=/api属性添加的,我们之前已经将它添加到了application.properties中。

您使用相同的风格来测试findAllActiveCarts()getActiveCartForCustomer()方法、findById()deleteById():

@Testvoid testFindAllActiveCarts() {
    get("/carts/active").then()
            .statusCode(OK.getStatusCode());
}

@Testvoid testGetActiveCartForCustomer() {
    get("/carts/customer/3").then()
            .contentType(ContentType.JSON)
            .statusCode(OK.getStatusCode())
            .body(containsString("Peter"));
}

@Testvoid testFindById() {
    get("/carts/3").then()
            .statusCode(OK.getStatusCode())
            .body(containsString("status"))
            .body(containsString("NEW"));

    get("/carts/100").then()
            .statusCode(NO_CONTENT.getStatusCode());
}

@Testvoid testDelete() {
    get("/carts/active").then()
            .statusCode(OK.getStatusCode())
            .body(containsString("Jason"))
            .body(containsString("NEW"));

    delete("/carts/1").then()
            .statusCode(NO_CONTENT.getStatusCode());

    get("/carts/1").then()
            .statusCode(OK.getStatusCode())
            .body(containsString("Jason"))
            .body(containsString("CANCELED"));
}

在这些测试中,我们验证:

  • ①对/carts/active的 HTTP GET 请求的响应将200作为其状态代码。

  • ②对/carts/customer/3的 HTTP GET 请求的响应将200作为其状态代码,并且主体包含"Peter"。Peter Quinn 是 ID 为 3 的客户,他有一个活动的购物车。这个值来自我们在V1.1__Insert_samples.sql脚本中使用 Flyway 导入的样本数据。

  • ③对/carts/3的 HTTP GET 请求的响应将200作为其状态代码,并且主体包含"NEW"作为购物车状态。在/carts/100上的 HTTP GET 请求的响应将404作为它的状态代码,它的主体是空的,因为我们没有 ID 为 100 的购物车。

  • ④对于客户 Jason 拥有的 ID 为 1 的给定活动购物车,在我们于/carts/1执行 HTTP DELETE 后,购物车状态将从"NEW"变为"CANCELED "

现在我们将进入更深层次的测试用例。我们将核实一个不正确的情况。在我们的业务逻辑中,一个客户在任何时候都不能有一个以上的活动购物车。因此,我们将创建一个测试来验证当给定客户有两个活动购物车时,应用将抛出一个错误。

我们需要为 ID 为 3 的客户插入一个额外活动购物车的记录。保持冷静!img/509649_1_En_4_Figj_HTML.gif我们将在测试结束时删除该记录,以保持数据库干净。为了执行这些插入和删除 SQL 查询,我们需要从测试上下文访问数据库。为了使这种交互成为可能,我们需要在测试中获得一个数据源。Quarkus 支持这一点,它允许您通过@Inject注释将 CDI beans 注入到测试中。由于数据源是一个 CDI bean,我们可以在测试中注入它。

Quarkus 中的测试是完整的 CDI beans,因此您可以享受所有的 CDI 特性。img/509649_1_En_4_Figl_HTML.gif

测试看起来像这样:

@QuarkusTest
@QuarkusTestResource(TestContainerResource.class)
class CartResourceTest {

    private static final String INSERT_WRONG_CART_IN_DB =
     "insert into carts values (999, current_timestamp, current_timestamp, 'NEW', 3)";

    private static final String DELETE_WRONG_CART_IN_DB =
            "delete from carts where id = 999";

    @Inject
    Datasource datasource;

...

    @Test
    void testGetActiveCartForCustomerWhenThereAreTwoCartsInDB() {
        executeSql(INSERT_WRONG_CART_IN_DB);

        get("/carts/customer/3").then()
                .statusCode(INTERNAL_SERVER_ERROR.getStatusCode())
                .body(containsString(INTERNAL_SERVER_ERROR.getReasonPhrase()))
                .body(containsString("Many active carts detected !!!"));

        executeSql(DELETE_WRONG_CART_IN_DB);
    }

    private void executeSql(String query) {
        try (var connection = dataSource.getConnection()) {
            var statement = connection.createStatement();
            statement.executeUpdate(query);
        } catch (SQLException e) {
            throw new IllegalStateException("Error has occurred while trying to execute SQL Query: " + e.getMessage());
        }
    }
...
}

该测试将验证/carts/customer/3上的 HTTP GET 请求将具有以下内容:

  • 状态代码为500,表示内部服务器错误

  • Body包含"Internal Server Error"

下一个测试将是关于创建一个新的购物车。

要创建购物车,我们需要创建一个客户。然后,基于它的 ID,我们可以创建购物车。这个测试将调用客户 API 来创建客户,并调用购物车 API 来创建购物车。为了数据库的一致性,在测试结束时,我们将删除创建的记录:

  • ①您正在将请求参数打包到一个Map中,它将被放心地序列化到 JSON 中。

  • extract().jsonPath().getInt("id")用于提取响应 JSON 主体中"id"属性的值。

  • extract().jsonPath().getMap("$")用于提取所有 JSON 体并反序列化到一个Map中。

@Test
void testCreateCart() {
    var requestParams = Map.of("firstName", "Saul", "lastName", "Berenson", "email", "call.saul@mail.com"); ①

    var newCustomerId = given()
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
            .body(requestParams).post("/customers").then()
            .statusCode(OK.getStatusCode())
            .extract()          ②
            .jsonPath()         ②
            .getInt("id");      ②

    var response = post("/carts/customer/" + newCustomerId).then()
            .statusCode(OK.getStatusCode())
            .extract()          ③
            .jsonPath()         ③
            .getMap("$");       ③

    assertThat(response.get("id")).isNotNull();
    assertThat(response).containsEntry("status", CartStatus.NEW.name());

    delete("/carts/" + response.get("id")).then()
            .statusCode(NO_CONTENT.getStatusCode());

    delete("/customers/" + newCustomerId).then()
            .statusCode(NO_CONTENT.getStatusCode());
}

购物车 API 的最后一个测试是验证当客户已经有一个活动的购物车时,API 将拒绝为该客户创建另一个购物车:

@Test
void testFailCreateCartWhileHavingAlreadyActiveCart() {

    var requestParams = Map.of("firstName", "Saul", "lastName", "Berenson", "email", "call.saul@mail.com");

    var newCustomerId = given()
            .header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON)
            .body(requestParams)
            .post("/customers").then()
            .statusCode(OK.getStatusCode())
            .extract()
            .jsonPath()
            .getLong("id");

    var newCartId = post("/carts/customer/" + newCustomerId).then()
            .statusCode(OK.getStatusCode())
            .extract()
            .jsonPath()
            .getLong("id");

    post("/carts/customer/" + newCustomerId).then()
            .statusCode(INTERNAL_SERVER_ERROR.getStatusCode())
            .body(containsString(INTERNAL_SERVER_ERROR.getReasonPhrase()))
            .body(containsString("There is already an active cart"));

    assertThat(newCartId).isNotNull();

    delete("/carts/" + newCartId).then()
            .statusCode(NO_CONTENT.getStatusCode());

    delete("/customers/" + newCustomerId).then()
            .statusCode(NO_CONTENT.getStatusCode());
}

在这个测试中,我们验证了除了Status Code 500和臭名昭著的Internal Server Error之外,响应体还包含消息"There is already an active cart"

img/509649_1_En_4_Figm_HTML.gif我只涉及CartResourceTest,因为它是八个测试类中最全面的测试。你会在我的img/509649_1_En_4_Fign_HTML.gif GitHub 库中找到所有代码。img/509649_1_En_4_Figo_HTML.gif

我们将使用 SonarQube 来检查测试的覆盖率,并分析代码的质量。

发现 SonarQube

SonarQube 是 SonarSource 开发的一个开源平台,用于持续检查代码质量。您可以通过对代码的静态分析来执行自动审查,以检测 20 多种编程语言上的错误、代码气味和安全漏洞。SonarQube 提供关于重复代码、编码标准、单元测试、代码覆盖率、代码复杂性、注释、错误和安全漏洞的报告。

SonarQube 可以记录度量历史并提供演化图。SonarQube 提供与 Maven、Ant、Gradle、MSBuild 和持续集成工具(Atlassian Bamboo、Jenkins、Hudson 等)的全自动分析和集成。).

您需要在您的机器上本地安装 SonarQube 或使用托管版本。例如,您可以使用 SonarCloud,在这里您可以免费分析项目。

What is SonarCloud?

SonarCloud 是领先的在线服务,用于捕获您的拉取请求和整个代码库中的错误和安全漏洞。

SonarCloud 是 SonarQube 基于云的代码质量和安全服务。SonarCloud 的主要特点是:

  • 支持 23 种语言,包括 Java、JS、C#、C/C++、Objective-C、TypeScript、Python、ABAP、PLSQL、T-SQL 等等。

  • 数以千计的规则来跟踪难以发现的错误和质量问题,这要归功于它强大的静态代码分析器。

  • 与 Travis、Azure DevOps、BitBucket、AppVeyor 等的云 CI 集成。

  • 深入的代码分析探索所有的源文件,无论是在分支中还是在拉请求中,以达到绿色质量关并促进构建。

  • 快速且可扩展。

您可以在 SonarCloud 中创建一个免费帐户。

img/509649_1_En_4_Figp_HTML.jpg

接下来,选择使用 GitHub 或 Azure DevOps 甚至 BitBucket 或 GitLab 帐户从 SonarCloud 开始。我在这种情况下使用img/509649_1_En_4_Figq_HTML.gif GitHub。

接下来,单击手动创建项目:

img/509649_1_En_4_Figr_HTML.jpg

接下来,单击在 GitHub 上选择一个组织,将您的组织导入 GitHub:

img/509649_1_En_4_Figs_HTML.jpg

接下来,选择存储项目源代码的组织:

img/509649_1_En_4_Figt_HTML.jpg

接下来,从列表中选择项目:

img/509649_1_En_4_Figu_HTML.jpg

接下来,您需要定义组织名称:

img/509649_1_En_4_Figv_HTML.jpg

您可以选择适用于所有公共存储库和项目的免费计划:

img/509649_1_En_4_Figw_HTML.jpg

现在,您可以选择想要分析的公共存储库。单击设置:

img/509649_1_En_4_Figx_HTML.jpg

您将进入项目配置屏幕旁边:

img/509649_1_En_4_Figy_HTML.jpg

选择手动分析方法,并选择 Maven 作为构建工具:

img/509649_1_En_4_Figz_HTML.jpg

您将获得定制配置,用于在 SonarCloud 上分析您的项目。有些属性需要添加到pom.xml文件中:

<properties>
  <sonar.projectKey>nebrass_quarkushop</sonar.projectKey>
  <sonar.organization>nebrass</sonar.organization>
  <sonar.host.url>https://sonarcloud.io</sonar.host.url>
</properties>

您需要用这里生成的值定义一个名为SONAR_TOKEN的环境变量。这个令牌用于向 SonarCloud 认证 SonarQube Maven 插件。

现在,项目被配置为在 SonarCloud 上进行分析,只需运行mvn verify sonar:sonar:

img/509649_1_En_4_Figaa_HTML.jpg

哇哦!只覆盖了 2.2%?img/509649_1_En_4_Figab_HTML.gif img/509649_1_En_4_Figac_HTML.gif我们以为自己做了足够强大的测试,可以测试一切,但似乎还缺了点什么。原因是,在应用中使用 Lombok 时,需要在项目根文件夹中添加一个额外的配置文件,如清单 4-4 所示。

  • ①告诉 Lombok 这是你的根目录。然后,您可以在任何子目录(通常表示项目或源包)中用不同的设置创建lombok.config文件。

  • ② Lombok 可以配置为对所有生成的节点添加@lombok.Generated注释,这对于JaCoCo(它有内置支持)或者其他样式检查器和代码覆盖工具非常有用。

config.stopBubbling = true                      ①
lombok.addLombokGeneratedAnnotation = true      ②

Listing 4-4lombok.config

通过键入mvn clean verify sonar:sonar再次运行声纳分析仪:

img/509649_1_En_4_Figad_HTML.jpg

耶!现在 Sonar 知道了 Lombok 生成的代码,分析结果也更容易接受。img/509649_1_En_4_Figaf_HTML.gif

建立和经营夸库商店

建造采石场

quartus 中的包装模式

QuarkuShop 使用 Maven 作为构建工具,所以您可以使用mvn package来构建它。该命令将构建以下内容:

  • target/目录中的quarkushop-1.0.0-SNAPSHOT.jar文件,它不是可运行的 JAR。它包含项目类和资源。

  • target/quarkus-app目录中的quarkus-run.jar文件,这是一个可运行的 JAR。但是如果没有target/quarkus-app/lib/文件夹,这个 JAR 文件就不能在任何地方执行,所有需要的库都被复制到这个文件夹中。所以如果你想分发quarkus-run.jar,你需要分发整个quarkus-app目录。

要有一个独立的 JAR 文件来打包 QuarkuShop 和所有必要的文件,可以创建一个 Fat JAR(也称为 UberJAR)。

What Are Fat or Uber Jars?

Maven(尤其是 Spring Boot)推广了这种众所周知的打包方法,它包括在标准 Java 运行时环境中运行整个应用所需的一切(也就是说,你可以用java -jar myapp.jar运行应用)。

要为 QuarkuShop 构建 UberJAR,只需键入以下命令:

mvn clean package -Dquarkus.package.type=uber-jar

如此容易,如此简单!没有要配置或添加到项目中的内容。Quarkus 天生支持这种创作。

UberJAR 的主要缺点是它不能在映像构建期间分层,这会大大增加构建时间和映像大小。

Quarkus 还支持原生模式,这是这个伟大框架中最好的和最受推崇的特性。

遇见夸特斯土著

Quarkus 通过与 GraalVM 深度集成,使得创建原生二进制文件变得非常容易。这些二进制文件也被称为本地映像。GraalVM 可以将 Java 字节码编译成本机映像,从而使应用启动更快,占用空间更小。

安装 GraalVM 时,默认情况下native-image功能不可用。要使用 GraalVM 安装本机映像,请运行以下命令:

gu install native-image

img/509649_1_En_4_Figah_HTML.gif确保配置了GRAALVM_HOME环境变量,并指向您的 GraalVM 安装目录。

要构建本地 QuarkuShop 二进制文件,请运行以下命令:

./mvnw clean package -Pnative

为了构建本机可执行文件,我们使用了pom.xml文件中的native maven 概要文件。Maven 概要文件是在项目生成时添加的:

<profiles>
    <profile>
        <id>native</id>
        <activation>
            <property><name>native</name></property>
        </activation>
        <build>
            <plugins>
                <plugin>
                    <artifactId>maven-failsafe-plugin</artifactId>
                    <version>${surefire-plugin.version}</version>
                    <executions>
                        <execution>
                            <goals>
                                <goal>integration-test</goal>
                                <goal>verify</goal>
                            </goals>
                            <configuration>
                                <systemProperties>
                                    <native.image.path>
${project.build.directory}/${project.build.finalName}-runner
                                    </native.image.path>
                                    <java.util.logging.manager>
                                        org.jboss.logmanager.LogManager
                                    </java.util.logging.manager>
                                    <maven.home>${maven.home}</maven.home>
                                </systemProperties>
                            </configuration>
                        </execution>
                    </executions>
                </plugin>
            </plugins>
        </build>
        <properties>
            <quarkus.package.type>native</quarkus.package.type>
        </properties>
    </profile>
</profiles>

在本机模式下运行测试

注意,这里配置了两个目标:integration-testverify。这些用于执行为应用的本地版本运行的测试。您可以重用经典 JAR 中的测试,并将它们带到本机映像中。这可以通过创建从每个@QuarkusTest类继承的新 Java 类来实现。继承类需要用@NativeImageTest注释,这表明这个测试应该使用本机映像运行,而不是在 JVM 中运行。

img/509649_1_En_4_Figai_HTML.gif @NativeImageTest将对现有的本地二进制文件执行测试。本地二进制路径在 Maven 故障保护插件configuration块中定义:

<systemProperties>
    <native.image.path>
        ${project.build.directory}/${project.build.finalName}-runner
    </native.image.path>
...
</systemProperties>

要了解更多关于本地测试的信息,请参阅本指南: https://quarkus.io/guides/building-native-image#testing-the-native-executable

例如,对于CartResourceTest:

@QuarkusTest
@QuarkusTestResource(TestContainerResource.class)
class CartResourceTest {
    ...
}

您创建了将在本机映像中运行测试的CartResourceIT:

@NativeImageTest
class CartResourceIT extends CartResourceTest {
}

img/509649_1_En_4_Figaj_HTML.gif我使用了与官方 Quarkus 文档相同的命名约定。我使用后缀Test进行 JVM 集成测试,使用后缀IT进行本机映像测试。

创建本机映像测试后,尝试使用本机映像运行测试:

mvn verify -Pnative

除了从CartResourceTest继承的本地测试类之外,所有的测试都通过了。img/509649_1_En_4_Figak_HTML.gif img/509649_1_En_4_Figal_HTML.gif错误信息非常明确:

[ERROR] Errors:
[ERROR]   CartResourceIT » JUnit @Inject is not supported in NativeImageTest tests. Offe...
[INFO]
[ERROR] Tests run: 39, Failures: 0, Errors: 1, Skipped: 0
[INFO]

这是因为缺乏对注入到本机模式的支持。尽管在CartResourceTest中,您将DataSource注入到数据库交互的测试中。这在 JVM 模式下是可能的,但在本机模式下是不可能的。让我们删除CartResourceIT,因为保持它的禁用是没有用的。

img/509649_1_En_4_Figam_HTML.gif要在本机模式下禁用特定的父测试类,只需使用@DisabledOnNativeImage对该类进行注释。

现在,如果您再次运行mvn verify -Pnative命令,您将跳过禁用的测试,所有剩余的测试都将通过:

[INFO] Results:
[INFO]
[WARNING] Tests run: 39, Failures: 0, Errors: 0, Skipped: 1

打包并运行本地 QuarkuShop

使用mvn verify -Pnative命令的本地二进制编译不能被分发,也不能在其他机器上执行。本机可执行文件特定于您的操作系统,它是在那里编译的。img/509649_1_En_4_Figan_HTML.gif

保持冷静!奇妙的容器化和神话般的 Quarkus 团队为您带来了解决方案!解决方案是在 Docker 容器中构建原生二进制文件,这样它将与主机操作系统隔离开来。您可以使用以下命令来完成此操作:

$ mvn package -Pnative -Dquarkus.native.container-build=true

...
[INFO] --- quarkus-maven-plugin:1.13.3.Final:build (default) @ quarkushop ---
[INFO] [org.jboss.threads] JBoss Threads version 3.2.0.Final
[INFO] [io.quarkus.flyway.FlywayProcessor] Adding application migrations in path 'file:/home/nebrass/java/playing-with-java-microservices-monolith-example/target/quarkushop-1.0.0-SNAPSHOT.jar!/db/migration' using protocol 'jar'
[INFO] [org.hibernate.Version] HHH000412: Hibernate ORM core version 5.4.29.Final
[INFO] [io.quarkus.deployment.pkg.steps.JarResultBuildStep] Building native image source jar: ...quarkushop-1.0.0-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Building native image from ...quarkushop-1.0.0-SNAPSHOT-runner.jar
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Checking image status quay.io/quarkus/ubi-quarkus-native-image:21.0-java11
21.0-java11: Pulling from quarkus/ubi-quarkus-native-image
57de4da701b5: Pull complete
cf0f3ebe9f53: Pull complete
6d14943d1530: Pull complete
Digest: sha256:176e619ad7cc2881477d04a2b2681fae41db08a92be06cddffd698f9c9546388
Status: Downloaded newer image for quay.io/quarkus/ubi-quarkus-native-image:21.0-java11
quay.io/quarkus/ubi-quarkus-native-image:21.0-java11
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] Running Quarkus native-image plugin on GraalVM Version 21.0.0.2 (Java Version 11.0.10+8-jvmci-21.0-b06)
[INFO] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] docker run \
    -v /home/nebrass/java/playing-with-java-microservices-monolith-example/target/quarkushop-1.0.0-SNAPSHOT-native-image-source-jar:/project:z \
    --env LANG=C \
    --user 1000:1000 \
    --rm \
    quay.io/quarkus/ubi-quarkus-native-image:21.0-java11 \
    -J-Dsun.nio.ch.maxUpdateArraySize=100 \
    -J-DCoordinatorEnvironmentBean.transactionStatusManagerEnable=false \
    -J-Djava.util.logging.manager=org.jboss.logmanager.LogManager \
    -J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory \
    -J-Dvertx.disableDnsResolver=true \
    -J-Dio.netty.leakDetection.level=DISABLED \
    -J-Dio.netty.allocator.maxOrder=1 \
    -J-Duser.language=en \
    -J-Dfile.encoding=UTF-8 \
    --initialize-at-build-time= \
    -H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime \
    -H:+JNI -jar quarkushop-1.0.0-SNAPSHOT-runner.jar \
    -H:FallbackThreshold=0 \
    -H:+ReportExceptionStackTraces \
    -H:-AddAllCharsets \
    -H:EnableURLProtocols=http,https \
    --enable-all-security-services \
    --no-server \
    -H:-UseServiceLoaderFeature \
    -H:+StackTrace quarkushop-1.0.0-SNAPSHOT-runner
[quarkushop-1.0.0-SNAPSHOT-runner:25]    classlist:  12 734,03 ms,  1,15 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]        (cap):     786,61 ms,  1,15 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]        setup:   2 837,21 ms,  1,15 GB
18:02:44,230 INFO  [org.hib.Version] HHH000412: Hibernate ORM core version 5.4.29.Final
18:02:44,258 INFO  [org.hib.ann.com.Version] HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
18:02:44,357 INFO  [org.hib.dia.Dialect] HHH000400: Using dialect: io.quarkus.hibernate.orm.runtime.dialect.QuarkusPostgreSQL10Dialect
18:02:44,526 INFO  [org.hib.val.int.uti.Version] HV000001: Hibernate Validator 6.2.0.Final
18:03:20,036 INFO  [org.jbo.threads] JBoss Threads version 3.2.0.Final
[quarkushop-1.0.0-SNAPSHOT-runner:25]     (clinit):   2 685,63 ms,  3,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]   (typeflow):  53 377,69 ms,  3,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]    (objects):  54 520,56 ms,  3,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]   (features):   2 615,98 ms,  3,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]     analysis: 118 704,92 ms,  3,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]     universe:   4 451,62 ms,  3,93 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]      (parse):  21 315,61 ms,  4,98 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]     (inline):  11 952,68 ms,  6,25 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]    (compile):  40 647,63 ms,  6,54 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]      compile:  79 193,30 ms,  6,54 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]        image:  13 638,67 ms,  6,29 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]        write:   4 589,92 ms,  6,29 GB
[quarkushop-1.0.0-SNAPSHOT-runner:25]      [total]: 236 996,22 ms,  6,29 GB
[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] objcopy executable not found in PATH. Debug symbols will not be separated from executable.
[WARNING] [io.quarkus.deployment.pkg.steps.NativeImageBuildStep] That will result in a larger native image with debug symbols embedded in it.
[INFO] [io.quarkus.deployment.QuarkusAugmentor] Quarkus augmentation completed in 254801ms
[INFO] --------------------------------------------------------------------
[INFO] BUILD SUCCESS
[INFO] --------------------------------------------------------------------
[INFO] Total time:  04:42 min
[INFO] Finished at: 2021-05-01T20:06:24+02:00
[INFO] --------------------------------------------------------------------

在前面的 Maven 日志中,列出了一个很长的 Docker 命令:

docker run \
-v /home/nebrass/java/playing-with-java-microservices-monolith-example/target/quarkushop-1.0.0-SNAPSHOT-native-image-source-jar:/project:z \
--env LANG=C \
--user 1000:1000 \
--rm \
quay.io/quarkus/ubi-quarkus-native-image:21.0-java11 \
-J-Dsun.nio.ch.maxUpdateArraySize=100 \
-J-DCoordinatorEnvironmentBean.transactionStatusManagerEnable=false \
-J-Djava.util.logging.manager=org.jboss.logmanager.LogManager \
-J-Dvertx.logger-delegate-factory-class-name=io.quarkus.vertx.core.runtime.VertxLogDelegateFactory \
-J-Dvertx.disableDnsResolver=true \
-J-Dio.netty.leakDetection.level=DISABLED \
-J-Dio.netty.allocator.maxOrder=1 \
-J-Duser.language=en \
-J-Dfile.encoding=UTF-8 \
--initialize-at-build-time= \
-H:InitialCollectionPolicy=com.oracle.svm.core.genscavenge.CollectionPolicy$BySpaceAndTime \
-H:+JNI -jar quarkushop-1.0.0-SNAPSHOT-runner.jar \
-H:FallbackThreshold=0 \
-H:+ReportExceptionStackTraces \
-H:-AddAllCharsets \
-H:EnableURLProtocols=http,https \
--enable-all-security-services \
--no-server \
-H:-UseServiceLoaderFeature \
-H:+StackTrace quarkushop-1.0.0-SNAPSHOT-runner

执行这个很长的命令是为了在 Docker 容器中基于quay.io/quarkus/ubi-quarkus-native-image:21.0-java11映像构建本地可执行文件,该映像支持 GraalVM。因此,即使您没有在本地安装 GraalVM,您也可以毫无问题地构建本机可执行文件。img/509649_1_En_4_Figap_HTML.gif

img/509649_1_En_4_Figaq_HTML.gif您可以使用以下命令明确选择容器化引擎:

# To select Docker
mvn package -Pnative -Dquarkus.native.container-runtime=docker
# To select Podman

mvn package -Pnative -Dquarkus.native.container-runtime=podman

生成的可执行文件将是一个 64 位 Linux 可执行文件,您可以在 Docker 容器中运行它。当我们生成 QuarkuShop 时,我们在src/main/docker目录中得到一个默认的Dockerfile.native文件,其内容如清单 4-5 所示。

FROM registry.access.redhat.com/ubi8/ubi-minimal
WORKDIR /work/
COPY target/*-runner /work/application
RUN chmod 775 /work
EXPOSE 8080
CMD ["./application", "-Dquarkus.http.host=0.0.0.0"]

Listing 4-5src/main/docker/Dockerfile.native

让我们构建并运行Dockerfile.native。在运行容器之前,请确保 PostgreSQL 容器正在运行。img/509649_1_En_4_Figar_HTML.gif

$ docker build -f src/main/docker/Dockerfile.native -t nebrass/quarkushop-native .
...
Successfully built b14f563446d1
Successfully tagged nebrass/quarkushop-native:latest

$ docker run --network host --name quarkushop-native -p 8080:8080 nebrass/quarkushop-native
WARNING: Published ports are discarded when using host network mode

___  ____                       __          _____  __
_ __/ __ \ __  __ ____   _____ / /__ __  __/ ___/ / /_   ____   ____
 --/ / / // / / // __ \ / ___// //_// / / /\__ \ / __ \ / __ \ / __ \
 -/ /_/ // /_/ // /_/ // /   / ,<  / /_/ /___/ // / / // /_/ // /_/ /
--\___\_\\____/ \__,_//_/   /_/|_| \____//____//_/ /_/ \____// ,___/
/_/ Part of the #PlayingWith Series
Powered by Quarkus 1.13.3.Final
2020-08-08 13:42:37,722 INFO  [org.fly.cor.int.lic.VersionPrinter] (main) Flyway Community Edition 6.5.3 by Redgate
2020-08-08 13:42:37,725 INFO  [org.fly.cor.int.dat.DatabaseFactory] (main) Database: jdbc:postgresql://localhost:5432/demo (PostgreSQL 9.6)
2020-08-08 13:42:37,729 INFO  [org.fly.cor.int.com.DbMigrate] (main) Current version of schema "public": 1.1
2020-08-08 13:42:37,729 INFO  [org.fly.cor.int.com.DbMigrate] (main) Schema "public" is up to date. No migration necessary.
2020-08-08 13:42:37,799 INFO  [io.quarkus] (main) quarkushop 1.0.0-SNAPSHOT native (powered by Quarkus 1.13.3.Final) started in 0.085s. Listening on: http://0.0.0.0:8080
2020-08-08 13:42:37,799 INFO  [io.quarkus] (main) Profile prod activated.
...

Hakuna Matata!img/509649_1_En_4_Figas_HTML.gif一切都好!该应用正在端口 8080 上运行并可用。

img/509649_1_En_4_Figat_HTML.gif你可能会问自己,为什么会有(main) Profile prod activatedimg/509649_1_En_4_Figau_HTML.gif我们不是使用本地概要文件构建应用吗?img/509649_1_En_4_Figav_HTML.gif这里为什么会有prodimg/509649_1_En_4_Figaw_HTML.gif

这个概要文件是应用运行时概要文件:prod。如上所述,Quarkus 应用有三个预定义的概要文件:devtestprod。当我们运行打包的应用时,我们处于prod模式,尽管当我们使用mvn quarkus:dev运行源代码时,我们显然处于dev模式。

Maven 本机概要文件用于构建源代码,而prod是运行时概要文件。img/509649_1_En_4_Figax_HTML.gif

很好!我们基于 QuarkuShop 的本机二进制文件构建了这个 Docker 映像:用 Quarkus 构建的超音速亚原子 Java 二进制文件。但是夸库斯的表现如何呢?它真的比 Spring Boot 产生更好的结果吗?原生映像真的优化了启动时间吗?它减少了内存占用吗?

img/509649_1_En_4_Figay_HTML.gif Quarkus 有一个很棒的扩展叫做container-image-docker,它处理 Docker 和src/main/docker文件夹中的 Docker 文件。

JVM 和本机模式之间的差异

我们需要进行科学检验,看看 Quarkus 是否真的如此有效。img/509649_1_En_4_Figaz_HTML.gif性能是 Quarkus 的第一卖点。你会发现无数的比较报告展示了 Quarkus JVM、Quarkus Native 和许多其他框架如 Spring Boot 之间的差异。

让我们先回顾一下 Quarkus 团队制作的最著名的度量图。它将 Quarkus JVM 和原生模式与传统的云原生栈(我认为是 Spring Boot img/509649_1_En_4_Figba_HTML.gif)进行了比较:

img/509649_1_En_4_Figbb_HTML.png

img/509649_1_En_4_Figbc_HTML.gif内存 RSS(代表驻留集大小)是当前由一个进程分配和使用的物理内存量(不包括换出的页面)。它包括代码、数据和共享库(在每个使用它们的进程中都被计算在内)。

但是我们都知道这种东西纯粹是一种营销手段。我们何不自己尝试一下呢!img/509649_1_En_4_Figbe_HTML.gif

我首先考虑使用 Spring Boot、Quarkus JVM 和 Quarkus Native,用一些 Hello World REST APIs 创建一个完整的环境。在开始任务之前,我在img/509649_1_En_4_Figbf_HTML.gif GitHub 里搜索了一下,看看有没有类似的项目。幸运的是,我找到了哈拉尔德·莱因米勒 ( https://github.com/rmh78/quarkus-performance )制作的优秀实验室。该实验室为 sample REST 和 REST plus CRUD 应用进行了基准测试和指标收集,包括许多框架:

  • Payara Micro 公司

  • Spring Boot

  • quartus jvm 和本机

  • 甚至 Python,我将省略它,因为超出了范围img/509649_1_En_4_Figbg_HTML.gif

我分叉了这个项目(在 MIT 许可下)并更新了版本,因此基准测试更加有效。你可以在我的img/509649_1_En_4_Figbh_HTML.gif GitHub 仓库的 nebrass/quarkus-performance 找到这个项目。

我不会详细解释实验室做什么,但我会列出基本步骤:

  1. 所有任务都在基于 CentOs 8 的 Docker 容器中执行。

  2. 第一步是安装所有需要的软件,如 Maven、GraalVM 21.1.0 CE 和企业版、Python 和测试工具如 Jabba(类似于 NVM 的 Java 版本管理器)等。

  3. 构建 Docker 映像并运行它,在访问它的 bash 时,构建所有示例应用(所有 Java 变体和 Python)的源代码。

  4. 通过特定的运行时对每个构建的二进制文件应用基准脚本。基准测试脚本是一个基于 Apache 基准测试工具的负载测试。

  5. 为每种情况生成一个 Python MatplotLib(Python 的绘图库)图形。这些数字包含 CPU 和内存利用率指标的可视化。

What is the Difference Between GraalVM CE and EE?

GraalVM 分社区版和企业版。

GraalVM Community edition 是一款开源软件,构建自 GitHub 上可用的源代码,根据 GNU 通用公共许可证第 2 版分发,但“类路径”除外,这与 Java 的条款相同。

GraalVM 社区可以免费用于任何目的,没有任何附加条件,也没有任何保证或支持。

Oracle GraalVM Enterprise Edition 根据 GraalVM OTN 许可协议(免费用于测试、评估或开发非生产应用)或 Oracle 客户主许可协议的条款获得许可。

—来自甲骨文 www.graalvm.org/docs/why-graal/ 的官方 GraalVM 文档

基准测试结果:

|

诺誓

|

休息+污物

| | --- | --- | | img/509649_1_En_4_Figbi_HTML.jpg | img/509649_1_En_4_Figbj_HTML.jpg | | img/509649_1_En_4_Figbk_HTML.jpg | img/509649_1_En_4_Figbl_HTML.jpg |

  • quartus via graalvm 本机映像
|

诺誓

|

多生一点

| | --- | --- | | img/509649_1_En_4_Figbm_HTML.jpg | img/509649_1_En_4_Figbn_HTML.jpg | | img/509649_1_En_4_Figbo_HTML.jpg | img/509649_1_En_4_Figbp_HTML.jpg |

  • 通过 java 运行时的 quartus
|

诺誓

|

多生一点

| | --- | --- | | img/509649_1_En_4_Figbq_HTML.jpg | img/509649_1_En_4_Figbr_HTML.jpg | | img/509649_1_En_4_Figbs_HTML.jpg | img/509649_1_En_4_Figbt_HTML.jpg |

  • Payara Micro via Java runtime
|

诺誓

|

多生一点

| | --- | --- | | img/509649_1_En_4_Figbu_HTML.jpg | img/509649_1_En_4_Figbv_HTML.jpg | | img/509649_1_En_4_Figbw_HTML.jpg | img/509649_1_En_4_Figbx_HTML.jpg |

  • 通过 Java 运行时的 Spring Boot

对于 QuarkuShop 这样的 REST plus CRUD 应用,您的第一个请求是:

  • Quarkus 原生:响应时间 0.054s,内存 RSS 48MB。

  • Quarkus JVM: 响应时间 1.622s,内存 RSS 413MB。Quarkus 土著速度快 30 倍,重量轻 8 倍。

  • Spring Boot: 响应时间 5.925s,内存 RSS 468MB。Quarkus native 快 109 倍,轻 9 倍。

  • **Payara 微:**响应时间 6.723s,内存 RSS 607 MB。Quarkus native 快 124 倍,轻 12 倍。

你可以看到在性能上确实有巨大的差异;夸库斯土著是冠军!img/509649_1_En_4_Figby_HTML.gif img/509649_1_En_4_Figbz_HTML.gif

还要注意 Quarkus JVM 和 Spring Boot 的区别;夸库斯比 Spring Boot 快。img/509649_1_En_4_Figca_HTML.gif img/509649_1_En_4_Figcb_HTML.gif

GraalVM 强大功能背后的魔力

在 Java VM 中运行应用会带来启动和内存占用成本。

图像生成过程使用静态分析来查找任何可从main() Java 方法获得的代码,然后执行完全提前(AOT)编译。

这种强大的组合正在创造奇迹!超音速亚原子 Java 故事在这里制作!img/509649_1_En_4_Figcc_HTML.gif

结论

QuarkuShop 已经可以测试、构建和发布了。您可以享受 GraalVM 的强大功能来生成强大且速度极快的本机二进制文件。

因为我们使用 Maven 作为构建工具,所以可以使用 CI/CD 管道(例如通过 Jenkins 或 Azure DevOps)轻松地构建和部署这个应用。

五、构建和部署单体应用

介绍

在 QuarkuShop 中,我们使用 Maven 作为构建工具。可以使用 CI/CD 管道(例如,通过 Jenkins 或 Azure DevOps)轻松构建该应用并将其部署到生产环境中。

将项目导入 Azure DevOps

首先,你需要在 Azure DevOps 中创建一个项目。好吧,但是什么是 Azure DevOps?img/509649_1_En_5_Figa_HTML.gif

Azure DevOps 是来自img/509649_1_En_5_Figb_HTML.gif微软的软件即服务(SaaS)产品,为软件团队提供了许多出色的功能。这些特性涵盖了典型应用的生命周期:

  • Azure pipelines :可以与任何语言、平台和云(不仅仅是 Azure img/509649_1_En_5_Figc_HTML.gif)一起工作的 CI/CD。

  • Azure boards :强大的工作跟踪功能,包括看板、积压订单、团队仪表盘和定制报告。

  • Azure 工件:来自公共和私有来源的 Maven、npm 和 NuGet 包提要。

  • Azure Repos :为您的项目提供无限的云托管私有 Git repos。协作拉取请求、高级文件管理等等。

  • Azure 测试计划:一体化的计划和探索性测试解决方案。

对于 Java 开发人员来说,Azure DevOps(也称为 ADO)是 Jenkins/Hudson 或 GitHub 操作的一个很好的替代品。本章展示了如何轻松地为这个项目创建一个完整的 CI/CD 管道。

首先,进入 Azure DevOps 门户,点击开始免费创建一个帐户:

img/509649_1_En_5_Fige_HTML.jpg

接下来,验证您的 Outlook/Hotmail/Live 帐户(或创建一个新帐户img/509649_1_En_5_Figf_HTML.gif),然后确认注册:

img/509649_1_En_5_Figg_HTML.jpg

接下来,您需要创建一个 Azure DevOps 组织:

img/509649_1_En_5_Figh_HTML.jpg

接下来,创建您的第一个 Azure DevOps 项目:

img/509649_1_En_5_Figi_HTML.jpg

接下来,转到回购➤文件:

img/509649_1_En_5_Figj_HTML.jpg

在这里,您会对从命令行推送现有存储库感兴趣。

ADO 生成将本地项目推向上游所需的git命令。在运行这些命令之前,让本地项目成为支持 git 的项目。

为此,只需运行这些命令,这将启动并添加所有文件,然后进行第一次提交:

git init
git add .
git commit -m "first commit"

接下来,您应该运行将整个项目推送到 ADO 的git命令:

1 git remote add origin https://nebrass-lamouchi@dev.azure.com/nebrass-lamouchi/quarkushop-monolithic-application/_git/quarkushop-monolithic-application
2 git push -u origin --all

源代码现在存放在 ADO 存储库中。现在您将创建 CI/CD 管道。img/509649_1_En_5_Figk_HTML.gif

创建 CI/CD 管道

下一步是配置持续集成管道,它将在每次主开发分支(在我们的例子中是master分支)上有新代码时运行。

创建持续集成管道

要创建第一个 CI 管道,请转到“管道”部分,然后单击“创建管道”:

img/509649_1_En_5_Figl_HTML.jpg

接下来,选择源代码的存储位置。点按“使用经典编辑器:

img/509649_1_En_5_Figm_HTML.jpg

接下来,选择 AzureRepos Git 并选择您刚刚创建的 QuarkuShop 存储库:

img/509649_1_En_5_Fign_HTML.jpg

将出现管道配置屏幕。选择 Maven 管道:

img/509649_1_En_5_Figo_HTML.jpg

然后插入最重要的部分,使用定义 YAML 文件定义管道配置。你有两个选择:

  • 直接在 ADO 中构建基于 Maven 的 Java 项目

  • 使用 Docker 多级构建来构建您的项目

创建基于 Maven 的 CI 管道

对于用 ADO 构建的 Maven 项目来说,这是最常见的情况。这也是 Maven 构建最简单的选择。见清单 5-1 。

不幸的是,这种方法对环境有很强的依赖性。例如,您需要安装 JDK 11 和 Maven 3.6.2+。如果在主机中找不到这些依赖项之一,构建将会失败。

使用这种方法时,您需要根据您的具体需求和工具采用 CI 平台。

  • ①当master分支上有更新时,该流水线将被触发。

  • ②使用 Ubuntu 的最新镜像。写这本书的时候是 20.04。

  • ③CI 场景中的第一个任务是运行所有的测试并生成 Sonar 报告。为 Java 11 定义运行时,并为 Maven 任务分配 3GB 内存。

  • ④将目标文件夹的内容复制到预定义的$(Build.ArtifactStagingDirectory)

  • ⑤最后,将$(Build.ArtifactStagingDirectory)的内容作为 Azure DevOps 神器上传。

trigger:                                                ①
- master

pool:
  vmImage: 'ubuntu-latest'                              ②

steps:
- task: Maven@3
  displayName: 'Maven verify & Sonar Analysis'          ③
  inputs:
    mavenPomFile: 'pom.xml'
    mavenOptions: '-Xmx3072m'
    javaHomeOption: 'JDKVersion'
    jdkVersionOption: '1.11'
    jdkArchitectureOption: 'x64'
    publishJUnitResults: true
    testResultsFiles: '**/surefire-reports/TEST-*.xml'
    goals: 'verify sonar:sonar'

- task: CopyFiles@2
  displayName: 'CopyFiles for Target'                   ④
  inputs:
    SourceFolder: 'target'
    Contents: '*.*'
    TargetFolder: '$(Build.ArtifactStagingDirectory)'

- task: PublishBuildArtifacts@1
  displayName: 'Publish Artifact: drop'                 ⑤
  inputs:
    pathtoPublish: '$(Build.ArtifactStagingDirectory)'
    artifactName: drop

Listing 5-1azure-pipelines.yml

创建基于 Docker 多级的 CI 管道

这是构建项目的时髦方式。这是构建具有非常具体需求的项目的最佳选择。与 QuarkuShop 应用一样,您需要安装 JDK 11 和 GraalVM 以及 Maven 3.6.2+。即使在 ADO 中也无法满足这一点,因为 Azure pipelines 中没有 GraalVM(至少到目前为止,在我写这些文字的时候)。

幸运的是,这种构建方法不依赖于环境。每个必需的组件将在不同的 Dockerfile 阶段进行安装和配置。

使用这种方法时,您可以将您特定需要的工具带到 CI 平台。

让我们看看Dockerfile.multistage文件的阶段 1,如清单 5-2 所示。

## Stage 1 : build with maven builder image with native capabilities
FROM quay.io/quarkus/centos-quarkus-maven:20.1.0-java11 AS build

Listing 5-2src/main/docker/Dockerfile.multistage

在构建阶段,我们使用的是centos-quarkus-maven:20.1.0-java11 Docker 基础映像,这是 Java 11 附带的 GraalVM、Maven、Podman 和 Buildah。这些正是我们需要的工具和版本。

你可能注意到了,我正试图说服你采用这种方法。首先,这是唯一可能的方法,因为 ADO 没有 GraalVM,而且这是避免任何意外问题的最合适的方法。img/509649_1_En_5_Figs_HTML.gif

Docker 多级 CI 管道的azure-pipelines.yml文件如清单 5-3 所示。

  • ①使用第一个Docker@2任务:

    • 基于Dockerfile.multistage文件构建 Docker 映像。

    • 将构建的图像命名为nebrass/quarkushop-monolithic-application

    • $(Build.BuildId)latest标签标记构建的图像,?? 是一个 ADO 构建变量。

  • ②使用第二个Docker@2任务将nebrass/quarkushop-monolithic-application图像推送到适当的 Docker Hub 帐户。

  • ③使用名为nebrass@DockerHub的 Azure 服务连接,它存储适当的 Docker Hub 凭证。

    img/509649_1_En_5_Figt_HTML.gifDocker@2中的@定义了 ADO 中 Docker 任务的选定版本。

    img/509649_1_En_5_Figu_HTML.gif要了解更多关于创建到 SonarCloud 的 Azure 服务连接的信息,请在 https://www.azuredevopslabs.com/labs/vstsextend/sonarcloud/ 查看这个优秀的 Azure DevOps 实验室教程

trigger:
- master

pool:
  vmImage: 'ubuntu-latest'

steps:
- task: Maven@3
  displayName: 'Maven verify & Sonar Analysis'
  inputs:
    mavenPomFile: 'pom.xml'
    mavenOptions: '-Xmx3072m'
    javaHomeOption: 'JDKVersion'
    jdkVersionOption: '1.11'
    jdkArchitectureOption: 'x64'
    publishJUnitResults: true
    testResultsFiles: '**/surefire-reports/TEST-*.xml'
    goals: 'verify sonar:sonar'

- task: Docker@2
  displayName: 'Docker Multistage Build'                      ①
  inputs:
    containerRegistry: 'nebrass@DockerHub'                    ③
    repository: 'nebrass/quarkushop-monolithic-application'
    command: 'build'
    Dockerfile: '**/Dockerfile.multistage'
    buildContext: '.'
    tags: |
      $(Build.BuildId)
      latest

- task: Docker@2
  displayName: 'Push Image to DockerHub'                      ②
  inputs:
    containerRegistry: 'nebrass@DockerHub'                    ③
    repository: 'nebrass/quarkushop-monolithic-application'
    command: 'push'

Listing 5-3azure-pipelines.yml

不要惊讶我没有删除 Maven verify & Sonar 分析任务。不幸的是,我们用于这些集成测试的Testcontainers库不能从 Docker 上下文中调用。这就是为什么我决定使用 Maven 命令运行测试,然后完成 Docker 容器中的所有步骤。

这个 CI 管道缺少一个需求:Maven 将用来向 SonarCloud 认证以发布管道中生成的分析报告的SONAR_TOKEN环境变量。

要在 Azure pipeline 中定义环境变量,请转到 Pipelines ➤选择您的 Pipeline ➤编辑➤变量➤新变量,然后将环境变量定义为SONAR_TOKEN,并给它一个创建项目时获得的 SonarCloud 令牌。

img/509649_1_En_5_Figv_HTML.jpg

img/509649_1_En_5_Figw_HTML.gif如果没有SONAR_TOKEN环境变量,sonar:sonar将失败,并显示错误消息:java.lang.IllegalStateException: You're not authorized to run analysis. Please contact the project administrator

在这一级别,CI 管道已经准备就绪。您现在需要开始查看持续部署管道。

制作持续部署管道

对于部署部分,Docker 容器可以部署到许多产品和位置:

  • Kubernetes 集群:我们还没有到达那里

  • 托管 Docker 托管槽:Azure 容器实例、亚马逊容器服务、Docker Enterprise 等。

  • 虚拟计算机

在这种情况下,我们将使用 Azure VM 来部署 Docker 容器。您可以按照相同的步骤制作相同的 CD 管道。

创建虚拟机

第一步是创建 Azure 资源组,这是一个保存 Azure 资源的逻辑组,就像您想要创建的虚拟机一样。

img/509649_1_En_5_Figy_HTML.jpg

接下来,通过定义以下内容来创建虚拟机:

  • 虚拟机名称 : quarkushop-vm

  • 地区:法国中部

  • 影像:Ubuntu server 18.04 lt

  • 大小:标准 _B2ms 2 个 vCPUS 加 8GB 内存

img/509649_1_En_5_Figz_HTML.jpg

接下来,您需要定义以下内容:

  • 认证类型 : Password

  • 用户名 : nebrass

  • 密码:创建并确认——我不会给你我的img/509649_1_En_5_Figaa_HTML.gif

  • 确保将 SSH 端口设置为allowed

img/509649_1_En_5_Figab_HTML.jpg

通过单击“查看+创建”来确认创建。然后在验证屏幕中,单击创建。

img/509649_1_En_5_Figac_HTML.jpg

通过单击转到资源检查创建的虚拟机:

img/509649_1_En_5_Figad_HTML.png

接下来,您需要在 VM 网络安全规则中为 8080 端口(QuarkuShop HTTP 端口)的传入访问添加一个例外。为此,请转到 Azure 虚拟机的网络部分,并选择添加入站端口规则:

img/509649_1_En_5_Figae_HTML.jpg

您已经有了创建的虚拟机的 IP 地址。要访问它,只需从终端打开一个 SSH 会话。您可以使用定义的凭据来访问虚拟机实例:

$ ssh nebrass@51.103.26.144
The authenticity of host '51.103.26.144 (51.103.26.144)' can't be established.
ECDSA key fingerprint is SHA256:bioO7HNjtKKgy8g7EAgfR+82Pz4gFyEml0QyMjpLNVk.
Are you sure you want to continue connecting (yes/no/[fingerprint])? yes
Warning: Permanently added '51.103.26.144' (ECDSA) to the list of known hosts.
nebrass@51.103.26.144's password:
Welcome to Ubuntu 18.04.4 LTS (GNU/Linux 5.3.0-1034-azure x86_64)
...
nebrass@quarkushop-vm:~$

从更新虚拟机开始:

sudo apt update && sudo apt upgrade

安装 Docker 引擎:

$ sudo apt-get install apt-transport-https \
    ca-certificates curl gnupg-agent \
    software-properties-common

添加官方码头工人 GPG 键:

$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

使用以下命令设置稳定的存储库:

$ sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"

现在,是时候安装(最后img/509649_1_En_5_Figaf_HTML.gif)Docker 引擎了:

$ sudo apt-get update && sudo apt-get install docker-ce docker-ce-cli containerd.io

下一步,安装坞站组成:

$ sudo curl -L \
  "https://github.com/docker/compose/releases/download/1.26.2/docker-compose-$(uname -s)-$(uname -m)" \
    -o /usr/local/bin/docker-compose

对二进制文件应用可执行权限:

$ sudo chmod +x /usr/local/bin/docker-compose

然后,在/opt/文件夹中创建docker-compose.yml文件:

version: '3'
services:
  quarkushop:
    image: nebrass/quarkushop-monolithic-application:latest
    environment:
      - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql-db:5432/prod
    ports:
      - 8080:8080
  postgresql-db:
    image: postgres:13
    volumes:
      - /opt/postgres-volume:/var/lib/postgresql/data
    environment:
      - POSTGRES_USER=developer
      - POSTGRES_PASSWORD=p4SSW0rd
      - POSTGRES_DB=prod
      - POSTGRES_HOST_AUTH_METHOD=trust
    ports:
      - 5432:5432

接下来,您需要在 Azure VM 中创建一个本地文件夹,用作 PostgreSQL 的 Docker 卷:

$ sudo mkdir /opt/postgres-volume
$ sudo chmod 777 /opt/postgres-volume

Azure VM 现在可以用作您的生产运行时了。img/509649_1_En_5_Figag_HTML.gif让我们转向 CD 渠道。

创建持续部署管道

回到 Azure DevOps,然后转到 Pipelines ➤发布:

img/509649_1_En_5_Figah_HTML.jpg

接下来,单击新建管道,然后单击空作业:

img/509649_1_En_5_Figai_HTML.jpg

添加一个工件。选择“生成”作为源类型,并从列表中选择适当的项目和源:

img/509649_1_En_5_Figaj_HTML.jpg

接下来,单击可用的阶段 1,然后单击代理作业。将代理规格更改为ubuntu-20.04

向管道添加任务:

img/509649_1_En_5_Figak_HTML.jpg

单击添加的任务,然后单击 SSH 服务连接附近的管理以访问服务连接管理器:

img/509649_1_En_5_Figal_HTML.jpg

接下来,单击新建服务连接。搜索 SSH 并选择它:

img/509649_1_En_5_Figam_HTML.jpg

在下一个屏幕中,您需要配置对 Azure VM 实例的访问:

img/509649_1_En_5_Figan_HTML.jpg

返回到发布管道屏幕,单击 SSH 服务连接的刷新按钮。选择创建的服务连接。

接下来,给Commands添加这些docker-compose指令:

docker login -u $(docker.username) -p $(docker.password)
docker-compose -f /opt/docker-compose.yml stop
docker-compose -f /opt/docker-compose.yml pull
docker-compose -f /opt/docker-compose.yml rm quarkushop
docker-compose -f /opt/docker-compose.yml up -d

这些命令将停止所有 Docker 合成服务,提取最新的图像,删除 QuarkuShop 服务,然后重新创建它。

img/509649_1_En_5_Figao_HTML.gif$(docker.username)``$(docker.password)是我的 Docker Hub 凭证。我们将它们定义为环境变量。

然后单击保存。

img/509649_1_En_5_Figap_HTML.jpg

转到变量以定义环境变量:

img/509649_1_En_5_Figaq_HTML.jpg

最后一步是激活触发器:

img/509649_1_En_5_Figar_HTML.jpg

最后,保存修改并通过点击img/509649_1_En_5_Figas_HTML.gif创建发布来触发发布:

img/509649_1_En_5_Figat_HTML.jpg

耶!!当释放管道执行完成时,只需在浏览器中打开 URL:IP_ADDRESS:8080

例如,在我的例子中,可以在 51.103.26.144:8080 到达 QuarkuShop。您将得到默认响应:

img/509649_1_En_5_Figau_HTML.jpg

最后,您可以访问预定义的 Quarkus index.html页面:

img/509649_1_En_5_Figav_HTML.jpg

在 QuarkuShop 生产环境中,您还可以通过 51.103.26.144:8080/api/swagger-ui/ 访问 Swagger UI。

img/509649_1_En_5_Figaw_HTML.jpg

img/509649_1_En_5_Figax_HTML.gifSwagger UI 可用于dev/test环境,出于安全考虑,需要在生产环境中禁用它。

很好!img/509649_1_En_5_Figay_HTML.gif现在让我们更改此索引页面中列出的版本,并推送修改,以查看 CI/CD 管道是否如预期那样工作:img/509649_1_En_5_Figaz_HTML.gif

img/509649_1_En_5_Figba_HTML.jpg

耶!这家 CI/CD 工厂运转得非常好!img/509649_1_En_5_Figbb_HTML.gif恭喜你!img/509649_1_En_5_Figbc_HTML.gif

结论

QuarkuShop 现在有一个专门的 CI/CD 渠道:

  • 持续集成管道:在master分支上的每次提交时,管道将运行所有测试并构建本机二进制文件,该文件将被打包在 Docker 映像中。

  • 持续部署管道:当 CI 成功时,Docker 镜像将被部署到 Azure VM。img/509649_1_En_5_Figbd_HTML.gif

在下一章中,您将实现更多的层:

  • 安全:防止未经认证的访客访问应用。

  • 监控:确保应用正确运行,避免任何不良意外。

为什么要等待灾难的发生? img/509649_1_En_5_Figbe_HTML.gif**

六、添加防灾难层

介绍

编写代码,运行单元和集成测试,进行代码质量分析,创建 CI/CD 管道——许多开发人员认为旅程到此结束,新的迭代将重新开始。我们忘记了应用运行时。我不是指在哪里执行这个应用,我们已经说过我们将在 Docker 容器中运行这个应用。我说的是应用将如何运行:

  • 用户会如何使用 QuarkuShop?

  • 如何控制用户对应用的访问?

  • 我们能处理未经授权的访问吗?我们知道哪些该承认,哪些该拒绝吗?

  • 如何测量和跟踪 CPU 和内存资源的消耗?

  • 如果应用耗尽资源会发生什么?

关于运行时还有更多问题要问。这些问题揭示了 QuarkuShop 中缺失的两层:

  • 安全层:所有的认证和授权部分。

  • 监控层:所有的度量,即测量和跟踪组件。

实现安全层

保安!对于开发人员来说,这是最令人痛苦的话题之一,但它可能是任何企业应用中最关键的主题。安全性在 IT 界一直是一个非常具有挑战性的话题:技术和框架在不断发展,黑客也在不断发展。img/509649_1_En_6_Figb_HTML.gif

对于这个 QuarkuShop,我们将使用专用的 Quarkus 组件以及推荐的实践和设计选择。本章讨论如何实现一个典型的认证和授权引擎。

img/509649_1_En_6_Figc_HTML.gif我将认证和授权过程称为auth 2

分析安全要求和需求

在编写任何代码之前,我们从创建设计开始,例如使用 UML 图。安全层也是如此;我们需要在实现代码之前创建设计。但是哪个设计呢?代码在那里。我们将设计什么?

QuarkuShop 的全部功能已经实现,但还有很多需要设计。

我喜欢把建筑软件比作盖房子。到目前为止,我们所做的是:

  • 建了房子,这和写源代码是一样的。

  • 验证建筑与计划的一致性,这与编写测试相同。

  • 将房子连接到电、水和下水道网络,这与配置数据库、SonarCloud 等的访问权限是一样的。

  • 得到家具并装饰房子,这与创建 CI/CD 管道是一样的。

房子现在准备好了,业主希望有一个安全系统。我们从检查窗户和门开始,以定位可能的入口,这就是我们放置锁的地方。只有钥匙持有人可以进入,视人而定,主人会分配钥匙。例如,只有司机才有车库钥匙。园丁将有两把钥匙:一把开外门,一把开存放工具的花园小屋。住在房子里的家庭成员将毫无例外地拥有所有的钥匙。

我们也将有摄像头和传感器来监控和审计进入房子。当我们怀疑有人非法进入房子,我们可以检查摄像头,看看发生了什么。

这个家庭安全系统部署过程在某种程度上与添加应用的安全层是一样的。我们遵循相同的基本步骤:

  1. 我们分析并定位应用的所有访问点。这个过程叫做攻击面分析。

攻击面分析帮你:

  • 确定系统的哪些功能和部分需要检查/测试安全漏洞。

  • 识别需要纵深防御保护的高风险代码区域;你需要保护系统的哪些部分。

  • 确定何时改变了攻击面,何时需要进行某种威胁评估。

—OWASP 小抄系列 https://cheatsheetseries.owasp.org/cheatsheets/Attack_Surface_Analysis_Cheat_Sheet.html

  1. 我们会在这些入口上锁。这些锁是认证过程的一部分。

认证是验证个人、实体或网站是否是其所声称的人的过程。web 应用上下文中的身份验证通常通过提交用户名或 ID 以及一项或多项只有给定用户才应该知道的私有信息来执行。

—OWASP 小抄系列 https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html

  1. 我们将定义一个访问控制机制,以确保只有被允许的人才能访问给定的“门”。这个过程叫做授权

授权是访问特定资源的请求应该被准许或拒绝的过程。应该注意的是,授权并不等同于认证——因为这些术语及其定义经常被混淆。身份验证是提供和验证身份。授权包括确定用户(或主体)可以访问哪些功能和数据的执行规则,确保在身份验证成功后正确分配访问权限。

—OWASP 小抄系列 https://cheatsheetseries.owasp.org/cheatsheets/Access_Control_Cheat_Sheet.html

QuarkuShop 是一个 Java 企业应用,它公开了 REST APIs,这是与应用用户的唯一通信渠道。

QuarkuShop 的用户可分为三类:

  • 访客或匿名:未经认证的客户

  • 用户:经过认证的客户

  • Admin :应用超级用户

下一步是定义允许哪个用户类别访问每个 REST API 服务。这可以使用授权矩阵来完成。

为 REST APIs 定义授权矩阵

**Cart REST API的授权矩阵

* |

操作

|

匿名的

|

用户

|

管理

| | --- | --- | --- | --- | | 获取所有购物车 | img/509649_1_En_6_Figd_HTML.gif | img/509649_1_En_6_Fige_HTML.gif | img/509649_1_En_6_Figf_HTML.gif | | 获取活动购物车 | img/509649_1_En_6_Figg_HTML.gif | img/509649_1_En_6_Figh_HTML.gif | img/509649_1_En_6_Figi_HTML.gif | | 按客户 ID 获取购物车 | img/509649_1_En_6_Figj_HTML.gif | img/509649_1_En_6_Figk_HTML.gif | img/509649_1_En_6_Figl_HTML.gif | | 为给定客户创建新购物车 | img/509649_1_En_6_Figm_HTML.gif | img/509649_1_En_6_Fign_HTML.gif | img/509649_1_En_6_Figo_HTML.gif | | 按 ID 获取购物车 | img/509649_1_En_6_Figp_HTML.gif | img/509649_1_En_6_Figq_HTML.gif | img/509649_1_En_6_Figr_HTML.gif | | 按 ID 删除购物车 | img/509649_1_En_6_Figs_HTML.gif | img/509649_1_En_6_Figt_HTML.gif | img/509649_1_En_6_Figu_HTML.gif |

类别 REST API 的授权矩阵

* |

操作

|

匿名的

|

用户

|

管理

| | --- | --- | --- | --- | | 列出所有类别 | img/509649_1_En_6_Figv_HTML.gif | img/509649_1_En_6_Figw_HTML.gif | img/509649_1_En_6_Figx_HTML.gif | | 创建新类别 | img/509649_1_En_6_Figy_HTML.gif | img/509649_1_En_6_Figz_HTML.gif | img/509649_1_En_6_Figaa_HTML.gif | | 按 ID 获取类别 | img/509649_1_En_6_Figab_HTML.gif | img/509649_1_En_6_Figac_HTML.gif | img/509649_1_En_6_Figad_HTML.gif | | 按 ID 删除类别 | img/509649_1_En_6_Figae_HTML.gif | img/509649_1_En_6_Figaf_HTML.gif | img/509649_1_En_6_Figag_HTML.gif | | 按类别 ID 获取产品 | img/509649_1_En_6_Figah_HTML.gif | img/509649_1_En_6_Figai_HTML.gif | img/509649_1_En_6_Figaj_HTML.gif |

客户休息 API 的授权矩阵

* |

操作

|

匿名的

|

用户

|

管理

| | --- | --- | --- | --- | | 获取所有客户 | img/509649_1_En_6_Figak_HTML.gif | img/509649_1_En_6_Figal_HTML.gif | img/509649_1_En_6_Figam_HTML.gif | | 创建新客户 | img/509649_1_En_6_Figan_HTML.gif | img/509649_1_En_6_Figao_HTML.gif | img/509649_1_En_6_Figap_HTML.gif | | 获得活跃客户 | img/509649_1_En_6_Figaq_HTML.gif | img/509649_1_En_6_Figar_HTML.gif | img/509649_1_En_6_Figas_HTML.gif | | 获得不活跃的客户 | img/509649_1_En_6_Figat_HTML.gif | img/509649_1_En_6_Figau_HTML.gif | img/509649_1_En_6_Figav_HTML.gif | | 按 ID 获取客户 | img/509649_1_En_6_Figaw_HTML.gif | img/509649_1_En_6_Figax_HTML.gif | img/509649_1_En_6_Figay_HTML.gif | | 按 ID 删除客户 | img/509649_1_En_6_Figaz_HTML.gif | img/509649_1_En_6_Figba_HTML.gif | img/509649_1_En_6_Figbb_HTML.gif |

授权矩阵为 订单 REST API

|

操作

|

匿名的

|

用户

|

管理

| | --- | --- | --- | --- | | 获取所有订单 | img/509649_1_En_6_Figbc_HTML.gif | img/509649_1_En_6_Figbd_HTML.gif | img/509649_1_En_6_Figbe_HTML.gif | | 创建新订单 | img/509649_1_En_6_Figbf_HTML.gif | img/509649_1_En_6_Figbg_HTML.gif | img/509649_1_En_6_Figbh_HTML.gif | | 按客户 ID 获取订单 | img/509649_1_En_6_Figbi_HTML.gif | img/509649_1_En_6_Figbj_HTML.gif | img/509649_1_En_6_Figbk_HTML.gif | | 检查是否有给定 ID 的订单 | img/509649_1_En_6_Figbl_HTML.gif | img/509649_1_En_6_Figbm_HTML.gif | img/509649_1_En_6_Figbn_HTML.gif | | 按 ID 获取订单 | img/509649_1_En_6_Figbo_HTML.gif | img/509649_1_En_6_Figbp_HTML.gif | img/509649_1_En_6_Figbq_HTML.gif | | 按 ID 删除订单 | img/509649_1_En_6_Figbr_HTML.gif | img/509649_1_En_6_Figbs_HTML.gif | img/509649_1_En_6_Figbt_HTML.gif |

订单授权矩阵-项目 REST API

* |

操作

|

匿名的

|

用户

|

管理

| | --- | --- | --- | --- | | 创建新的订单项目 | img/509649_1_En_6_Figbu_HTML.gif | img/509649_1_En_6_Figbv_HTML.gif | img/509649_1_En_6_Figbw_HTML.gif | | 按订单 ID 获取订单项目 | img/509649_1_En_6_Figbx_HTML.gif | img/509649_1_En_6_Figby_HTML.gif | img/509649_1_En_6_Figbz_HTML.gif | | 按 ID 获取订单项目 | img/509649_1_En_6_Figca_HTML.gif | img/509649_1_En_6_Figcb_HTML.gif | img/509649_1_En_6_Figcc_HTML.gif | | 按 ID 删除订单项目 | img/509649_1_En_6_Figcd_HTML.gif | img/509649_1_En_6_Figce_HTML.gif | img/509649_1_En_6_Figcf_HTML.gif |

授权矩阵为 支付 休息 API

|

操作

|

匿名的

|

用户

|

管理

| | --- | --- | --- | --- | | 获得所有付款 | img/509649_1_En_6_Figcg_HTML.gif | img/509649_1_En_6_Figch_HTML.gif | img/509649_1_En_6_Figci_HTML.gif | | 创建新的付款 | img/509649_1_En_6_Figcj_HTML.gif | img/509649_1_En_6_Figck_HTML.gif | img/509649_1_En_6_Figcl_HTML.gif | | 获得低于或等于限额的付款 | img/509649_1_En_6_Figcm_HTML.gif | img/509649_1_En_6_Figcn_HTML.gif | img/509649_1_En_6_Figco_HTML.gif | | 通过 ID 获得付款 | img/509649_1_En_6_Figcp_HTML.gif | img/509649_1_En_6_Figcq_HTML.gif | img/509649_1_En_6_Figcr_HTML.gif | | 按 ID 删除付款 | img/509649_1_En_6_Figcs_HTML.gif | img/509649_1_En_6_Figct_HTML.gif | img/509649_1_En_6_Figcu_HTML.gif |

产品 REST API 的授权矩阵

* |

操作

|

匿名的

|

用户

|

管理

| | --- | --- | --- | --- | | 获取所有产品 | img/509649_1_En_6_Figcv_HTML.gif | img/509649_1_En_6_Figcw_HTML.gif | img/509649_1_En_6_Figcx_HTML.gif | | 创造新产品 | img/509649_1_En_6_Figcy_HTML.gif | img/509649_1_En_6_Figcz_HTML.gif | img/509649_1_En_6_Figda_HTML.gif | | 按类别 ID 获取产品 | img/509649_1_En_6_Figdb_HTML.gif | img/509649_1_En_6_Figdc_HTML.gif | img/509649_1_En_6_Figdd_HTML.gif | | 清点所有产品 | img/509649_1_En_6_Figde_HTML.gif | img/509649_1_En_6_Figdf_HTML.gif | img/509649_1_En_6_Figdg_HTML.gif | | 按类别 ID 计算产品数量 | img/509649_1_En_6_Figdh_HTML.gif | img/509649_1_En_6_Figdi_HTML.gif | img/509649_1_En_6_Figdj_HTML.gif | | 通过 ID 获取产品 | img/509649_1_En_6_Figdk_HTML.gif | img/509649_1_En_6_Figdl_HTML.gif | img/509649_1_En_6_Figdm_HTML.gif | | 按 ID 删除产品 | img/509649_1_En_6_Figdn_HTML.gif | img/509649_1_En_6_Figdo_HTML.gif | img/509649_1_En_6_Figdp_HTML.gif |

授权矩阵为 审核 REST API

|

操作

|

匿名的

|

用户

|

管理

| | --- | --- | --- | --- | | 按产品 ID 获取评论 | img/509649_1_En_6_Figdq_HTML.gif | img/509649_1_En_6_Figdr_HTML.gif | img/509649_1_En_6_Figds_HTML.gif | | 按产品 ID 创建新评论 | img/509649_1_En_6_Figdt_HTML.gif | img/509649_1_En_6_Figdu_HTML.gif | img/509649_1_En_6_Figdv_HTML.gif | | 按 ID 获取评论 | img/509649_1_En_6_Figdw_HTML.gif | img/509649_1_En_6_Figdx_HTML.gif | img/509649_1_En_6_Figdy_HTML.gif | | 按 ID 删除评论 | img/509649_1_En_6_Figdz_HTML.gif | img/509649_1_En_6_Figea_HTML.gif | img/509649_1_En_6_Figeb_HTML.gif |

实现安全层

我们将使用专用的身份存储来处理 QuarkuShop 用户的凭证。我们使用 Keycloak 来实现这个目的。

What is Keycloak?

Keycloak 是一个开源的身份和访问管理(IAM)解决方案,面向现代应用和服务。它使保护应用和服务变得很容易,只需要很少的代码,甚至不需要代码。

img/509649_1_En_6_Figec_HTML.png

用户使用 Keycloak 进行身份验证,而不是使用单独的应用。这意味着您的应用不必处理登录表单、认证用户和存储用户。一旦登录到 Keycloak,用户无需再次登录即可访问不同的应用。这也适用于注销。

Keycloak 提供单点注销,这意味着用户只需注销一次,就可以从所有使用 Keycloak 的应用中注销。

Keycloak 基于标准协议,并提供对 OpenID Connect、OAuth 2.0 和 SAML 的支持。

如果基于角色的授权不能满足您的需求,Keycloak 还提供了细粒度的授权服务。这允许您从 Keycloak 管理控制台管理所有服务的权限,并赋予您准确定义所需策略的权力。

我们将分四步实施这一安全策略:

  1. 准备和配置 Keycloak。

  2. 在 QuarkuShop 中实现 auth 2 Java 组件。

  3. 更新集成测试以支持授权 2

  4. 向我们的生产环境添加 Keycloak。

准备和配置钥匙锁

安全性实现的第一步是拥有一个 Keycloak 实例。本节逐步讨论如何创建和配置 Keycloak。

img/509649_1_En_6_Figed_HTML.gif很多教程里都有准备好的配置,可以导入到 Keycloak 里轻松上手。你不能在这里这样做。您将逐步执行所有需要的配置。这是最好的学习方法:边做边学。

首先在 Docker 容器中创建 Keycloak 实例:

  • ①默认情况下,没有创建admin用户,因此您将无法登录到admin控制台。要创建一个admin账户,您需要使用环境变量来传递初始用户名和密码。

  • ②使用H2作为 Keycloak 数据库。

  • ③列出暴露的端口。

  • ④这是基于 Keycloak 11.0.0。

docker run -d --name docker-keycloak \
          -e KEYCLOAK_USER=admin \        ①
          -e KEYCLOAK_PASSWORD=admin \    ①
          -e DB_VENDOR=h2 \               ②
          -p 9080:8080 \                  ③
          -p 8443:8443 \                  ③
          -p 9990:9990 \                  ③
          jboss/keycloak:11.0.0

打开http://localhost:9080进入欢迎页面:

img/509649_1_En_6_Figee_HTML.jpg

接下来,单击“管理控制台”向控制台进行身份验证。使用admin凭证作为用户名和密码:

img/509649_1_En_6_Figef_HTML.jpg

接下来,您需要创建一个新的领域。首先单击添加领域:

img/509649_1_En_6_Figeg_HTML.png

What is a Keycloak Realm?

一个领域是 Keycloak 中的核心概念。一个领域管理一组用户、凭据、角色和组。用户属于并登录到一个领域。领域相互隔离,只能管理和验证它们控制的用户。

当你第一次启动 Keycloak 时,它会为你创建一个预定义的领域。这个初始领域就是master领域。它是领域层级中的最高级别。该领域中的管理员帐户有权查看和管理在服务器实例上创建的任何其他领域。当您定义您的初始admin帐户时,您在master领域中创建了一个帐户。您首次登录管理控制台也将通过master领域。

接下来你将踏上创建新领域的第一步。调用领域quarkushop-realm然后点击创建:

img/509649_1_En_6_Figeh_HTML.jpg

接下来,您将看到quarkushop-realm配置页面:

img/509649_1_En_6_Figei_HTML.jpg

quarkushop-realm中,您需要定义您将使用的角色:useradmin角色。为此,只需转到角色➤Add 角色:

img/509649_1_En_6_Figej_HTML.jpg

现在您可以创建admin角色:

img/509649_1_En_6_Figek_HTML.jpg

创建user角色:

img/509649_1_En_6_Figel_HTML.jpg

现在您已经创建了角色,您需要创建用户。只需进入用户菜单:

img/509649_1_En_6_Figem_HTML.jpg

有时在这个屏幕上,当你打开页面时,用户列表并没有载入。如果发生这种情况,只需单击查看所有用户来加载列表。

单击 Add User 创建用户(在本例中为 Nebrass、Jason 和 Marie):

img/509649_1_En_6_Figeo_HTML.jpg

然后在单击 Save 之后,单击 Credentials 选项卡,您将在其中定义用户密码。

img/509649_1_En_6_Figep_HTML.gif不要忘记将Temporary设置为OFF以防止 Keycloak 在你第一次登录时要求你更新密码。然后,单击设置密码:

img/509649_1_En_6_Figeq_HTML.jpg

接下来,转到 Role Mappings 选项卡,将所有角色添加到 Nebrass 中。img/509649_1_En_6_Figer_HTML.gif是的!我将是这个应用的管理员。img/509649_1_En_6_Figes_HTML.gif

img/509649_1_En_6_Figet_HTML.jpg

现在创建 Jason 用户并定义密码:

img/509649_1_En_6_Figeu_HTML.jpg

将用户角色添加到 Jason:

img/509649_1_En_6_Figev_HTML.jpg

接下来,创建 Marie 用户并定义密码:

img/509649_1_En_6_Figew_HTML.jpg

给 Marie 添加user角色:

img/509649_1_En_6_Figex_HTML.jpg

您已完成角色和用户的配置!现在,单击“客户机”列出领域客户机。

img/509649_1_En_6_Figey_HTML.jpg

What is a Realm Client?

客户端是可以请求 Keycloak 对用户进行身份验证的实体。大多数情况下,客户端是希望使用 Keycloak 来保护自己并提供单点登录解决方案的应用和服务。客户端也可以是只是想要请求身份信息或访问令牌的实体,以便它们可以安全地调用网络上由 Keycloak 保护的其他服务。

单击“创建”添加具有以下设置的新客户端:

img/509649_1_En_6_Figez_HTML.jpg

  • 客户端 ID: quarkushop

  • 客户端协议:openid-connect

  • 根 URL: http://localhost:8080/

单击保存以显示客户端配置屏幕:

img/509649_1_En_6_Figfa_HTML.jpg

必须为您的客户端定义一个协议映射器。

What is a Protocol Mapper?

协议映射器对令牌和文档执行转换。他们可以将用户数据映射到协议声明,并转换客户端和auth服务器之间的任何请求。

单击映射器选项卡以配置映射器:

img/509649_1_En_6_Figfb_HTML.jpg

然后单击创建以添加新的映射器:

img/509649_1_En_6_Figfc_HTML.jpg

img/509649_1_En_6_Figfd_HTML.gif 为什么我们将令牌声明名称定义为组?

我们将使用协议映射器将用户领域角色(可以是useradmin)映射到一个名为groups的属性,我们将把它作为一个普通字符串添加到 ID 令牌、访问令牌和 userinfo 中。

选择groups是基于 Quarkus SmallRye JWT 库的MpJwtValidator Java 类(这是用于管理 JWT 的 Quarkus 实现);在这里,我们使用jwtPrincipal.getGroups()来定义SecurityIdentity角色:

JsonWebToken jwtPrincipal = parser.parse(request.getToken().getToken());
uniEmitter.complete(
        QuarkusSecurityIdentity.builder().setPrincipal(jwtPrincipal)
            .addRoles(jwtPrincipal.getGroups())
            .addAttribute(SecurityIdentity.USER_ATTRIBUTE, jwtPrincipal)
            .build()
);

这就是为什么我们定义映射器来影响嵌入在 JWT 令牌中的用户组属性的用户角色。img/509649_1_En_6_Figfe_HTML.gif

现在可以测试 Keycloak 实例是否按预期运行。只需使用cURL命令请求一个access_token:

curl -X POST http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/token \
      -H 'content-type: application/x-www-form-urlencoded' \
      -d 'client_id=quarkushop' \
      -d 'username=nebrass' \
      -d 'password=password' \
      -d 'grant_type=password' | jq '.'

img/509649_1_En_6_Figff_HTML.gifURLhttp://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/token由以下内容组成:

img/509649_1_En_6_Figfg_HTML.gif

img/509649_1_En_6_Figfh_HTML.gifjq是一个轻量级且灵活的命令行 JSON 处理器,我用它来格式化 JSON 输出。

您将得到如下的 JSON 响应:

{
  "access_token": "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJKcmlxWGQzYVBNOS13djhXUmVJekZkRnRJa3Z1WG5uNDd4a0JmTl95R19zIn0.eyJleHAiOjE1OTc0OTEwNTIsImlhdCI6MTU5NzQ5MDc1MiwianRpIjoiZmIxZmQxOWMtNWJlMC00YTgwLWExOTUtOTAxZjFkOTI3NDI5IiwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo5MDgwL2F1dGgvcmVhbG1zL3F1YXJrdXNob3AtcmVhbG0iLCJhdWQiOiJhY2NvdW50Iiwic3ViIjoiMzU1ODc3YWQtMjY3Ny00ODJiLWE5NWYtYTI4ZjdmZGI1OTk5IiwidHlwIjoiQmVhcmVyIiwiYXpwIjoicXVhcmt1c2hvcCIsInNlc3Npb25fc3RhdGUiOiJjM2E4ZmU3Mi02MzRmLTRiNmUtYTZkMS03MTkyOGI2YTBlN2YiLCJhY3IiOiIxIiwiYWxsb3dlZC1vcmlnaW5zIjpbImh0dHA6Ly9sb2NhbGhvc3Q6ODA4MCJdLCJyZWFsbV9hY2Nlc3MiOnsicm9sZXMiOlsib2ZmbGluZV9hY2Nlc3MiLCJhZG1pbiIsInVtYV9hdXRob3JpemF0aW9uIiwidXNlciJdfSwicmVzb3VyY2VfYWNjZXNzIjp7ImFjY291bnQiOnsicm9sZXMiOlsibWFuYWdlLWFjY291bnQiLCJtYW5hZ2UtYWNjb3VudC1saW5rcyIsInZpZXctcHJvZmlsZSJdfX0sInNjb3BlIjoiZW1haWwgcHJvZmlsZSIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJuYW1lIjoiTmVicmFzcyBMYW1vdWNoaSIsImdyb3VwcyI6WyJvZmZsaW5lX2FjY2VzcyIsImFkbWluIiwidW1hX2F1dGhvcml6YXRpb24iLCJ1c2VyIl0sInByZWZlcnJlZF91c2VybmFtZSI6Im5lYnJhc3MiLCJnaXZlbl9uYW1lIjoiTmVicmFzcyIsImZhbWlseV9uYW1lIjoiTGFtb3VjaGkiLCJlbWFpbCI6Im5lYnJhc3NAcXVhcmt1c2hvcC5zdG9yZSJ9.HZmicWhE9V8g74of9KGcZOVGvwC_oo2zs4-ElBBuV6XSWDUoiFLJVkSUzOV4WFzwvsM7V7_aZRzihZqq6QTtezweyhZIauo3pjmmtbMnq16WUFV-4oJWzk3P_6T5y74sh93aPuQtnw5hSQ4L68RjwQ6HIcaHJFkqrh6fX7uy0ZiHuPnRzhv38uQrD9YMC_z3tApWKTS2TA9igizZrlJCDfTdfiThUDuXEgOmw-pffYx1BASfL14O0c0apGPqirNkSgSrCpuFvikXlRdeu3YnI1JQ6S7Jn-qQI-bdCD5M0_ynaUiJn_p6sZqI6ioSmLGyA__S5J7nj_BO--fdIl0lUA",
  "expires_in": 300,
  "refresh_expires_in": 1800,
  "refresh_token": "...",
  "token_type": "bearer",
  "not-before-policy": 0,
  "session_state": "c3a8fe72-634f-4b6e-a6d1-71928b6a0e7f",
  "scope": "email profile"
}

img/509649_1_En_6_Figfi_HTML.gif为了更漂亮的输出,我省略了refresh_token的值,因为它很长而且没用。

复制access_token值。然后去 jwt.io 把 JWT access_token贴在那里解码:

img/509649_1_En_6_Figfj_HTML.jpg

注意,有一个名为groups的 JSON 属性保存着我们在 Keycloak 中分配给 Nebrass 的 Roles 数组。

很好!Keycloak 运行正常,工作正常。现在,您将开始在 Java 端实现安全性。

在 QuarkuShop 中实现 auth 2 Java 组件
Java 配置端

第一步是向 QuarkuShop 添加 Quarkus SmallRye JWT 依赖项:


./mvnw quarkus:add-extension -Dextensions="io.quarkus:quarkus-smallrye-jwt"

接下来,将安全配置添加到application.properties:

  • ①启用 HTTP CORS 过滤器。
1  ### Security
2  quarkus.http.cors=true3  # MP-JWT Config
4  mp.jwt.verify.issuer=http://localhost:9080/auth/realms/quarkushop-realm   ②
5  mp.jwt.verify.publickey.location=http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs   ③
6  # Keycloak Configuration
7  keycloak.credentials.client-id=quarkushop   ④

What is CORS?

跨源资源共享(CORS)是一种机制,允许从提供第一资源的域之外的另一个域请求网页上的受限资源。

  • ② Config 属性指定服务器将接受为有效的 JWT 令牌的iss(颁发者)声明的值。我们已经在解密的 JWT 令牌的iss字段中获得了这个值。

  • ③ Config 属性允许指定公钥的外部或内部位置。该值可以是相对路径或 URL。当使用 Keycloak 作为身份管理器时,该值将是mp.jwt.verify.issuer加上/protocol/openid-connect/certs

  • ④一个带有名为keycloak.credentials.client-id的键的自定义属性,它保存值quarkushop,这是领域客户端 ID。

Java 源代码端

在 Java 代码中,您需要根据我们为每个服务创建的授权矩阵来保护 REST APIs。

让我们从 Carts REST API 开始,其中所有操作都允许用户和管理员进行。Carts REST API 上只允许经过身份验证的主题。为了满足这个需求,我们从io.quarkus.security包中获得了一个@Authenticated注释,该注释将只授予经过身份验证的主体访问权限。

img/509649_1_En_6_Figfl_HTML.gif为了区分应用用户角色用户,我将应用用户称为主体。术语 subject 来自旧的 good Java 认证和授权服务(JAAS ),用来表示请求访问资源的调用者。主体可以是任何实体,包括作为服务

因为CartResource中的所有操作都需要一个经过认证的主题,所以你必须用@Authenticated来注释CartResource类,这样它将被应用到所有的操作中。见清单 6-1 。

@Authenticated
@Path("/carts")
@Tags(value = @Tag(name = "cart", description = "All the cart methods"))
public class CartResource {
    ...
}

Listing 6-1com.targa.labs.quarkushop.web.CartResource

下一个 REST API 是 Category API,只有管理员可以创建和删除类别。所有人(管理员、用户和匿名用户)都可以进行其他操作。要仅授予特定角色访问权限,请使用javax.annotation.security包中的@RolesAllowed注释。

img/509649_1_En_6_Figfm_HTML.gifJavaDoc of javax . annotation . security . roles allowed

@RolesAllowed:指定允许访问应用中的方法的安全角色列表。

@RolesAllowed注释的值是安全角色名称的列表。可以在类或方法上指定此批注:

  • 在类级别指定它意味着它适用于类中的所有操作。

  • 在方法上指定它意味着它仅适用于该方法。

  • 如果应用于类和操作级别,当两者冲突时,方法值将覆盖类值。

基于 JavaDoc,我们将传递期望的角色作为注释的值。将@RolesAllowed("admin")注释应用于创建和删除类别的操作:

@Path("/categories")
@Tags(value = @Tag(name = "category", description = "All the category methods"))
public class CategoryResource {
    ...

    @RolesAllowed("admin")
    @POST
        public CategoryDto create(CategoryDto categoryDto) {
        return this.categoryService.create(categoryDto);
    }

    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.categoryService.delete(id);
    }
}

对于 Customer REST API,只有管理员可以访问所有的服务。用@RolesAllowed("admin")注释CustomerResource类将应用这个策略。

接下来是 Order REST API,通过身份验证的主体可以访问所有操作,除了findAll()方法,该方法只允许用于admin。因此,我们将类级别的两个@Authenticated注释和需要特定角色的方法上的@RolesAllowed注释结合起来:

@Authenticated
@Path("/orders")
@Tag(name = "order", description = "All the order methods")
public class OrderResource {
    ...

    @RolesAllowed("admin")
    @GET
    public List<OrderDto> findAll() {
        return this.orderService.findAll();
    }
    ...
}

img/509649_1_En_6_Figfn_HTML.gif当我们在本地方法中使用@RolesAllowed时,我们覆盖了类级别@Authenticated应用的策略。

OrderResource之后,我们将处理 OrderItem REST API,其中只允许经过身份验证的主体访问所有服务。用@Authenticated注释OrderItemResource类将应用这个策略。

除了删除和列出所有付款仅授权给admin之外,PaymentResource仅授权给经过身份验证的主体访问:

@Authenticated
@Path("/payments")
@Tag(name = "payment", description = "All the payment methods")
public class PaymentResource {
    ...

    @RolesAllowed("admin")
    @GET
    public List<PaymentDto> findAll() {
        return this.paymentService.findAll();
    }

    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.paymentService.delete(id);
    }
    ...
}

对于产品 REST API,除了创建和删除产品之外,所有人都可以执行所有操作,只有admin才可以执行这些操作:

@Path("/products")
@Tag(name = "product", description = "All the product methods")
public class ProductResource {
    ...

    @RolesAllowed("admin")
    @POST
        public ProductDto create(ProductDto productDto) {
        return this.productService.create(productDto);
    }

    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.productService.delete(id);
    }
    ...
}

最后,最后一个 REST API 是ReviewResource:

@Path("/reviews")
@Tag(name = "review", description = "All the review methods")
public class ReviewResource {
    ...

    @Authenticated
    @POST
    @Path("/product/{id}")
        public ReviewDto create(ReviewDto reviewDto, @PathParam("id") Long id) {
        return this.reviewService.create(reviewDto, id);
    }

    @RolesAllowed("admin")
    @DELETE
    @Path("/{id}")
    public void delete(@PathParam("id") Long id) {
        this.reviewService.delete(id);
    }
}

太好了。您已经应用了一级授权层!img/509649_1_En_6_Figfo_HTML.gif

不过,不要太自信。img/509649_1_En_6_Figfp_HTML.gif这个实现泄露了更多级别的授权。例如,用户被授权删除OrdersOrderItems,但是我们无法验证被认证的主体只是删除了自己的OrdersOrderItems,而没有删除其他用户的OrdersOrderItems。我们知道如何定义基于角色的访问规则,但是我们如何收集关于被认证主体的更多信息呢?

为此,我们将创建一个名为UserResource的新 REST API。当被调用时,UserResource将返回当前已验证的主题信息作为响应。

UserResource会是这样的:

  • ①注入一个JsonWebToken,它是定义使用 JWT 作为承载令牌的规范的实现。这个注入将在传入的请求中使用 JWT 令牌的一个实例。

  • JsonWebToken将返回注入的 JWT 令牌。

  • getCurrentUserInfoClaims()方法将返回可用声明的列表以及嵌入在 JWT 令牌中的它们各自的值。声明将从注入到UserResource中的 JWT 令牌实例中提取出来。

@Path("/user")
@Authenticated
@Tag(name = " user", description = "All the user methods")
public class UserResource {

    @Inject
    JsonWebToken jwt;                                           ①

    @GET
    @Path("/current/info")
    public JsonWebToken getCurrentUserInfo() {                  ②
        return jwt;
    }

    @GET
    @Path("/current/info/claims")
    public Map<String, Object> getCurrentUserInfoClaims() {     ③
        return jwt.getClaimNames()
                .stream()
                .map(name -> Map.entry(name, jwt.getClaim(name)))
                .collect(Collectors.toMap(
                        entry -> entry.getKey(),
                        entry -> entry.getValue())
                );
    }
}

让我们测试这个新的 REST API。从获得新的access_token开始:

export access_token=$(curl -X POST http://localhost:9080/auth/realms/quarkushop-realm/protocol/openid-connect/token \
      -H 'content-type: application/x-www-form-urlencoded' \
      -d 'client_id=quarkushop' \
      -d 'username=nebrass' \
      -d 'password=password' \
      -d 'grant_type=password' | jq --raw-output '.access_token')

然后,调用/user/current/info REST API:

curl -X GET -H "Authorization: Bearer $access_token" http://localhost:8080/api/user/current/info | jq '.'

作为回应,您将获得 JWT 令牌:

{
  "audience": [
    "account"
  ],
  "claimNames": [
    "sub", "resource_access", "email_verified", "allowed-origins", "raw_token", "iss",
    "groups", "typ", "preferred_username", "given_name", "aud", "acr", "realm_access",
    "azp", "scope", "name", "exp", "session_state", "iat", "family_name", "jti", "email"
  ],
  "expirationTime": 1597530481,
  "groups": ["offline_access", "admin", "uma_authorization", "user"],
  "issuedAtTime": 1597530181,
  "issuer": "http://localhost:9080/auth/realms/quarkushop-realm",
  "name": "nebrass",
  "rawToken": "eyJhbGci...L5A",
  "subject": "355877ad-2677-482b-a95f-a28f7fdb5999",
  "tokenID": "7416ee6e-e74c-45ae-bf85-8889744eaacf"
}

很好,我们有可用的索赔。让我们得到它们各自的值:

{
  "sub": "355877ad-2677-482b-a95f-a28f7fdb5999",
  "resource_access": {
    "account": {
      "roles": ["manage-account", "manage-account-links", "view-profile"]
    }
  },
  "email_verified": true,
  "allowed-origins": ["http://localhost:8080"],
  "raw_token": "eyJhbGci...L5A",
  "iss": "http://localhost:9080/auth/realms/quarkushop-realm",
  "groups": ["offline_access", "admin", "uma_authorization", "user"],
  "typ": "Bearer",
  "preferred_username": "nebrass",
  "given_name": "Nebrass",
  "aud": ["account"],
  "acr": "1",
  "realm_access": {
    "roles": ["offline_access", "admin", "uma_authorization", "user"]
  },
  "azp": "quarkushop",
  "scope": "email profile",
  "name": "Nebrass Lamouchi",
  "exp": 1597530481,
  "session_state": "68069099-f534-434f-8d08-d8d75b8ff1c6",
  "iat": 1597530181,
  "family_name": "Lamouchi",
  "jti": "7416ee6e-e74c-45ae-bf85-8889744eaacf",
  "email": "nebrass@quarkushop.store"
}

太棒了!现在,您知道了如何访问安全细节,您可以使用这些细节来准确地识别经过身份验证的主题。顺便说一句,你可以得到关于 JWT 币的同样信息。你可以通过另一种方式获得同样的 JWT 代币:

@GET()
@Path("/current/info-alternative")
public Principal getCurrentUserInfoAlternative(@Context SecurityContext ctx) {
    return ctx.getUserPrincipal();
}

作为方法参数传递的@Context SecurityContext ctx用于在当前上下文中注入SecurityContext

img/509649_1_En_6_Figfr_HTML.gif SecurityContext是一个可注入的接口,像Principal一样提供对安全相关信息的访问。

img/509649_1_En_6_Figfs_HTML.gifPrincipal接口表示委托人的抽象概念,可以用来表示任何实体,比如个人、公司或登录 ID。在这种情况下,主体是 JWT 令牌。img/509649_1_En_6_Figft_HTML.gif

我不会深入研究授权层的完整实现。我就讲到这里;否则,我就需要花两章的时间来讨论这个内容。

安全部分的最后一步是添加一个 REST API,为给定的用户名和密码返回一个access_token。这可能很有用,尤其是对于 Swagger UI。说到 Swagger UI,我们需要让它知道我们的安全层。

我们将从创建TokenService开始,它将是从 Keycloak 发出access_token请求的 REST 客户端。

当我们使用 Java 11 时,我们可以享受它的一个伟大的新特性:全新的 HTTP 客户端。

正如您在使用cURL命令之前所做的那样,您需要使用 Java 11 HTTP 客户端来请求一个access_token:

  • ①注释为每个 HTTP 请求创建一个TokenService实例。

  • ②获取mp.jwt.verify.issuer属性值以构建 Keycloak 令牌的端点 URL。

  • ③获取请求access_token所需的keycloak.credentials.client-id属性值。

@RequestScopedpublic class TokenService {

    @ConfigProperty(name = "mp.jwt.verify.issuer", defaultValue = "undefined")
    Provider<String> jwtIssuerUrlProvider;  ②

    @ConfigProperty(name = "keycloak.credentials.client-id", defaultValue = "undefined")
    Provider<String> clientIdProvider;      ③

    public String getAccessToken(String userName, String password)
                        throws IOException, InterruptedException {

        String keycloakTokenEndpoint =
                    jwtIssuerUrlProvider.get() + "/protocol/openid-connect/token";

        String requestBody = "username=" + userName + "&password=" + password +
                    "&grant_type=password&client_id=" + clientIdProvider.get();

        if (clientSecret != null) {
            requestBody += "&client_secret=" + clientSecret;
        }

        HttpClient client = HttpClient.newBuilder().build();

        HttpRequest request = HttpRequest.newBuilder()
                .POST(BodyPublishers.ofString(requestBody))
                .uri(URI.create(keycloakTokenEndpoint))
                .header("Content-Type", "application/x-www-form-urlencoded")
                .build();

        HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());

        String accessToken = "";

        if (response.statusCode() == 200) {
            ObjectMapper mapper = new ObjectMapper();
            try {
                accessToken = mapper.readTree(response.body()).get("access_token").textValue();
            } catch (IOException e) {
                e.printStackTrace();
            }
        } else {
            throw new UnauthorizedException();
        }

        return accessToken;
    }
}

img/509649_1_En_6_Figfv_HTML.gif我们将属性值设为Provider<String>而不是String,以避免本地映像构建失败。

接下来,添加getAccessToken()方法:

@Path("/user")
@Authenticated
@Tag(name = " user", description = "All the user methods")
public class UserResource {

    @Inject JsonWebToken jwt;

    @POST
    @PermitAll
    @Path("/access-token")
    @Produces(MediaType.TEXT_PLAIN)
    public String getAccessToken(@QueryParam("username") String username,
                                @QueryParam("password") String password)
                        throws IOException, InterruptedException {
        return tokenService.getAccessToken(username, password);
    }
    ...
}

太棒了。您可以从 Swagger UI 测试新方法:

img/509649_1_En_6_Figfx_HTML.jpg

耶!img/509649_1_En_6_Figfy_HTML.gif您现在需要找到一种方法让 Swagger UI 应用access_token来请求所有的 REST APIs。

我原以为这将是一次长途旅行,但这是一项非常容易的任务。仅仅 10 行代码就足以让聚会:img/509649_1_En_6_Figfz_HTML.gif

  • @SecurityScheme定义了 OpenAPI 操作可以使用的安全方案。

  • ②添加 OpenAPI 定义的描述。

  • ③将创建的安全模式jwt链接到 OpenAPI 定义。

@SecurityScheme(
        securitySchemeName = "jwt",             ①
        description = "JWT authentication with bearer token",
        type = SecuritySchemeType.HTTP,         ①
        in = SecuritySchemeIn.HEADER,           ①
        scheme = "bearer",                      ①
        bearerFormat = "Bearer [token]")@OpenAPIDefinition(
        info = @Info(                           ②
                title = "QuarkuShop API",
                description = "Sample application for the book 'Playing with Java Microservices with Quarkus and Kubernetes'",
                contact = @Contact(name = "Nebrass Lamouchi", email = "lnibrass@gmail.com", url = "https://blog.nebrass.fr"),
                version = "1.0.0-SNAPSHOT"
        ),
        security = @SecurityRequirement(name = "JWT") ③
)
public class OpenApiConfig extends Application {
}

再次检查 Swagger UI:

img/509649_1_En_6_Figga_HTML.jpg

注意有锁图标img/509649_1_En_6_Figgb_HTML.gif

使用getAccessToken()操作创建一个access_token,然后点击【授权 将生成的access_token传递给SecurityScheme。最后,单击授权:

img/509649_1_En_6_Figgd_HTML.jpg

现在,当您单击任何操作时,Swagger UI 将包含access_token作为每个请求的载体。

太棒了!现在您已经有了 Keycloak 和安全性 Java 组件。接下来,您需要重构测试以了解安全层。

更新集成测试以支持授权 2

对于测试,您需要动态地提供 Keycloak 实例,就像您使用Testcontainers库为数据库所做的那样。

我们将使用同一个库来提供 Keycloak。我们不会像使用 PostgreSQL 那样使用普通的 Docker 容器,因为在那里,Flyway 为我们填充了数据库。对于 Keycloak,我们没有一个内置的机制来为我们创建 Keycloak 领域。唯一可能的解决方案是使用 Docker Compose 文件,该文件将导入一个示例 Keycloak 领域文件。幸运的是,Testcontainers提供了对 Docker 编写文件的强大支持。

要使用TestContainers从 Docker 合成文件中提供容器,请使用:

public static DockerComposeContainer KEYCLOAK = new DockerComposeContainer(
    new File("src/main/docker/keycloak-test.yml"))
       .withExposedService("keycloak_1", 9080,
           Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));

我们将使用一个包含以下内容的示例 Keycloak 领域:

  • 两个用户:

    • 管理员(用户名admin,密码test,角色admin

    • 测试(用户名user,密码test,角色user

  • Keycloak 客户端有一个client-id=quarkus-client:和``secret=mysecret`。

QuarkusTestResourceLifecycleManager将使用我们创建的TokenService与提供的 Keycloak 实例通信,并且它将使用示例领域凭证来获取我们将在集成测试中使用的access_tokens

我们将得到两个access_tokens:一个用于admin角色,一个用于user角色。我们将把它们与mp.jwt.verify.publickey.locationmp.jwt.verify.issuer一起存储,作为当前测试范围中的系统属性。

自定义QuarkusTestResourceLifecycleManager将是这样的:

public class KeycloakRealmResource implements QuarkusTestResourceLifecycleManager {
     @ClassRule
     public static DockerComposeContainer KEYCLOAK = new DockerComposeContainer(
           new File("src/main/docker/keycloak-test.yml"))
          .withExposedService("keycloak_1",
                    9080,
                    Wait.forListeningPort().withStartupTimeout(Duration.ofSeconds(30)));

       @Override
       public Map<String, String> start() {
              KEYCLOAK.start();

             String jwtIssuerUrl = String.format(
                    "http://%s:%s/auth/realms/quarkus-realm",
                    KEYCLOAK.getServiceHost("keycloak_1", 9080),
                    KEYCLOAK.getServicePort("keycloak_1", 9080)
             );

            TokenService tokenService = new TokenService();
            Map<String, String> config = new HashMap<>();
            try {
                 String adminAccessToken = tokenService.getAccessToken(jwtIssuerUrl,
                      "admin", "test", "quarkus-client", "mysecret"
                );

                String testAccessToken = tokenService.getAccessToken(jwtIssuerUrl,
                      "test", "test", "quarkus-client", "mysecret"
                );

               config.put("quarkus-admin-access-token", adminAccessToken);
               config.put("quarkus-test-access-token", testAccessToken);

           } catch (IOException | InterruptedException e) {
                  e.printStackTrace();
          }

          config.put("mp.jwt.verify.publickey.location", jwtIssuerUrl + "/protocol/openidconnect/certs");
          config.put("mp.jwt.verify.issuer", jwtIssuerUrl);

          return config;
      }

     @Override
      public void stop() {
              KEYCLOAK.stop();
     }
}

我们将把它和@QuarkusTestResource(KeycloakRealmResource.class)注释一起使用。

@BeforeAll方法中,我们从系统属性中获取access_tokens,使它们准备好在测试中使用。一个典型的测试头骨看起来像这样:

@QuarkusTest
@QuarkusTestResource(TestContainerResource.class)
@QuarkusTestResource(KeycloakRealmResource.class)
class CategoryResourceTest {

     static String ADMIN_BEARER_TOKEN;
     static String USER_BEARER_TOKEN;

     @BeforeAll
     static void init() {
       ADMIN_BEARER_TOKEN = System.getProperty("quarkus-admin-access-token");
       USER_BEARER_TOKEN = System.getProperty("quarkus-test-access-token");
    }

    ...
}

要在测试中使用这些令牌:

@Test
void testFindAllWithAdminRole() {
   given().when()
          .header(HttpHeaders.AUTHORIZATION, "Bearer " + ADMIN_BEARER_TOKEN)
          .get("/carts")
          .then()
          .statusCode(OK.getStatusCode())
          .body("size()", greaterThan(0));
}

您需要在测试中测试和验证授权规则,例如,验证给定的概要文件不是Unauthorized:

@Test
void testFindAll() {
    get("/carts").then()
          .statusCode(UNAUTHORIZED.getStatusCode());
}

要验证对于给定的请求,REST API 上不允许主题,请使用以下命令:

@Test
void testDeleteWithUserRole() {
   given().when()
          .header(HttpHeaders.AUTHORIZATION, "Bearer " + USER_BEARER_TOKEN)
          .delete("/products/1")
          .then()
          .statusCode(FORBIDDEN.getStatusCode());
}

很好!我实现了所有的测试;你可以在我的 GitHub 库中找到它们。img/509649_1_En_6_Figgf_HTML.gif

将 Keycloak 添加到生产环境

最后一步是将生产 Keycloak 条目添加到生产虚拟机的 Docker Compose 中。我们还需要添加生产领域。

您可以通过非常少的步骤从本地 Keycloak 实例导出领域。

要从本地 Keycloak 容器(名为docker-keycloak)中导出领域,请使用以下命令:

$docker exec -it docker-keycloak bash

bash-4.4$ cd /opt/jboss/keycloak/bin/

bash-4.4$ mkdir backup

bash-4.4$ ./standalone.sh -Djboss.socket.binding.port-offset=1000 \
      -Dkeycloak.migration.realmName=quarkushop-realm \
      -Dkeycloak.migration.action=export \
      -Dkeycloak.migration.provider=dir \
      -Dkeycloak.migration.dir=./backup/

要将存储的领域从docker-keycloak容器复制到本地目录,请使用以下命令:

$ mkdir ~/keycloak-realms

$ docker cp docker-keycloak:/opt/jboss/keycloak/bin/backup ~/keycloak-realms

你将在~/keycloak-realms中获得两个钥匙锁王国文件——quarkushop-realm-realm.jsonquarkushop-realm-users-0.json

您需要编辑quarkushop-realm-realm.json文件并将sslRequiredexternal更改为none:

{
    ...
    "sslRequired": "none",
    ...
}

img/509649_1_En_6_Figgg_HTML.gif"sslRequired": "none"属性禁用任何请求所需的 SSL 证书。

然后,将两个文件——quarkushop-realm-realm.jsonquarkushop-realm-users-0.json——复制到 Azure VM 实例中的/opt/realms目录,这是生产环境。

这是 Azure 虚拟机实例。img/509649_1_En_6_Figgh_HTML.gif我们将完成最后一步:将 Keycloak 服务添加到/opt/docker-compose.yml文件中:

  • ①覆盖MP_JWT_VERIFY_PUBLICKEY_LOCATIONMP_JWT_VERIFY_ISSUER属性以确保应用指向 Azure VM 实例而不是localhost

  • ②添加 Keycloak Docker 服务。

  • ③定义 Keycloak 集群的用户名和密码。

  • ④将 Keycloak 数据库供应商定义为 PostgreSQL,因为我们的服务中有一个 PostgreSQL DB 实例。

  • ⑤将 DB 主机定义为postgresql-db,由 Docker 用服务 IP 动态解析。

  • ⑥将 Keycloak 数据库凭据定义为与 PostgreSQL 凭据相同。

  • ⑦表示 Keycloak 和 PostgreSQL 服务之间的依赖关系。

 1 version: '3'
 2 services:
 3   quarkushop:
 4     image: nebrass/quarkushop-monolithic-application:latest
 5     environment:
 6       - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql-db:5432/demo
 7       -
  MP_JWT_VERIFY_PUBLICKEY_LOCATION=http://51.103.50.23:9080/auth/realms/quarkushoprealm/protocol/openid-connect/certs   ①
 8       - MP_JWT_VERIFY_ISSUER=http://51.103.50.23:9080/auth/realms/quarkushop-realm ①
 9    ports:
10      - 8080:8080
11   postgresql-db:
12     image: postgres:13
13     volumes:
14       - /opt/postgres-volume:/var/lib/postgresql/data
15     environment:
16       - POSTGRES_USER=developer
17       - POSTGRES_PASSWORD=p4SSW0rd
18       - POSTGRES_DB=demo
19       - POSTGRES_HOST_AUTH_METHOD=trust
20    ports:
21      - 5432:5432
22   keycloak:                                     ②
23     image: jboss/keycloak:latest
24     command:
25       [
26         '-b','0.0.0.0',
27
28         '-Dkeycloak.migration.action=import',
29         '-Dkeycloak.migration.provider=dir',
30         '-Dkeycloak.migration.dir=/opt/jboss/keycloak/realms',
31         '-Dkeycloak.migration.strategy=OVERWRITE_EXISTING',
32         '-Djboss.socket.binding.port-offset=1000',
33         '-Dkeycloak.profile.feature.upload_scripts=enabled',
34       ]
35
36
37   volumes:
38     - ./realms:/opt/jboss/keycloak/realms
39   environment:
40     - KEYCLOAK_USER=admin     ③
41     - KEYCLOAK_PASSWORD=admin ③
42     - DB_VENDOR=POSTGRES      ④
43     - DB_ADDR=postgresql-db   ⑤
44     - DB_DATABASE=demo        ⑥
45     - DB_USER=developer       ⑥
46     - DB_SCHEMA=public47     - DB_PASSWORD=p4SSW0rd    ⑥
48   ports:
49     - 9080:9080
50   depends_on:                 ⑦
51     - postgresql-db           ⑦

img/509649_1_En_6_Figgi_HTML.gif如您所见,所有凭证都以纯文本形式列在docker-compose.yml文件中。

您可以使用 Docker 机密来保护这些凭证。每个密码都将存储在 Docker secret 中。

docker service create --name POSTGRES_USER --secret developer
docker service create --name POSTGRES_PASSWORD --secret p4SSW0rd
docker service create --name POSTGRES_DB --secret demo
docker service create --name KEYCLOAK_USER --secret admin
docker service create --name KEYCLOAK_PASSWORD --secret admin

img/509649_1_En_6_Figgj_HTML.gif遗憾的是,该功能仅适用于 Docker Swarm 集群。

为了保护凭证,将它们存储在 Azure VM 实例上的一个~/.env文件中:

POSTGRES_USER=developer
POSTGRES_PASSWORD=p4SSW0rd
POSTGRES_DB=demo
KEYCLOAK_USER=admin
KEYCLOAK_PASSWORD=admin

然后,更改docker-compose.yml以使用~/.env元素:

version: '3'
services:
   quarkushop:
     image: nebrass/quarkushop-monolithic-application:latest
     environment:
       - QUARKUS_DATASOURCE_JDBC_URL=jdbc:postgresql://postgresql-db:5432/${POSTGRES_DB}
       - MP_JWT_VERIFY_PUBLICKEY_LOCATION=http://51.103.50.23:9080/auth/realms/quarkushop-realm/protocol/openid-connect/certs
       - MP_JWT_VERIFY_ISSUER=http://51.103.50.23:9080/auth/realms/quarkushop-realm
     ports:
       - 8080:8080
   postgresql-db:
     image: postgres:13
     volumes:
       - /opt/postgres-volume:/var/lib/postgresql/data
     environment:
       - POSTGRES_USER=${POSTGRES_USER}
       - POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
       - POSTGRES_DB=${POSTGRES_DB}
       - POSTGRES_HOST_AUTH_METHOD=trust
     ports:
       - 5432:5432
   keycloak:
     image: jboss/keycloak:latest
     command:
      [
       '-b',
       '0.0.0.0',
       '-Dkeycloak.migration.action=import',
       '-Dkeycloak.migration.provider=dir',
       '-Dkeycloak.migration.dir=/opt/jboss/keycloak/realms',
       '-Dkeycloak.migration.strategy=OVERWRITE_EXISTING',
       '-Djboss.socket.binding.port-offset=1000',
       '-Dkeycloak.profile.feature.upload_scripts=enabled',
      ]
     volumes:
       - ./realms:/opt/jboss/keycloak/realms
     environment:
       - KEYCLOAK_USER=${KEYCLOAK_USER}
       - KEYCLOAK_PASSWORD=${KEYCLOAK_PASSWORD}
       - DB_VENDOR=POSTGRES
       - DB_ADDR=postgresql-db
       - DB_DATABASE=${POSTGRES_DB}
       - DB_USER=${POSTGRES_USER}
       - DB_SCHEMA=public
       - DB_PASSWORD=${POSTGRES_PASSWORD}
     ports:
       - 9080:9080
     depends_on:
       - postgresql-db

很好!Docker 编写服务已经准备就绪!img/509649_1_En_6_Figgk_HTML.gif

我们只需要为 Keycloak 端口的 Azure VM 实例网络规则添加一个例外。在 Azure VM 实例中,转到网络部分,为端口 9080 添加一个例外:

img/509649_1_En_6_Figgl_HTML.jpg

太棒了!img/509649_1_En_6_Figgm_HTML.gif生产环境拥有部署新版本 QuarkuShop 容器所需的所有元素,而不会给 PostgreSQL 数据库或 Keycloak 集群带来风险。

转到生产 Swagger UI,享受 QuarkuShop:最伟大的在线商店!img/509649_1_En_6_Figgn_HTML.gif

img/509649_1_En_6_Figgo_HTML.jpg

很好!img/509649_1_En_6_Figgp_HTML.gif该进入下一个抗灾层了:监控 img/509649_1_En_6_Figgq_HTML.gif

实现监控层

安全性并不是应用中唯一重要的附加层。指标监控也是一个非常重要的层,可以防止灾难。想象一下这样一种情况,您在云中的服务器上部署了一个超级安全、功能强大的应用。如果您不定期检查应用指标,应用可能会在您不知道的情况下耗尽资源。

应用监控不仅仅是一种获取指标的机制;它包括分析不同组件的性能和行为。在本章中,我不会涵盖所有不同的监控工具和实践。

相反,您将看到如何实现两个组件:

  • 应用状态指示器,也称为运行状况检查指示器。

  • 应用指标服务,用于提供有关应用的各种指标和统计信息。

实施健康检查

在 Quarkus 中实现健康检查是一项非常简单的任务:只需添加 SmallRye 健康扩展。没错。你只要给pom.xml加一个库就行了!魔法会自动发生!

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

SmallRye Health 会自动将健康检查端点添加到您的 Quarkus 应用中。

对于那些习惯了 Spring Boot 的人来说,SmallRye Health 就相当于执行器。

新的健康检查终点是:

  • /health/live:应用启动并运行。

  • 应用已经准备好服务请求。

  • /health:累积应用中的所有健康检查程序。

运行应用并在http://localhost:8080/api/health上执行cURL GET:

curl -X GET http://localhost:8080/api/health | jq '.'

您将得到一个 JSON 响应:

{
  "status": "UP",
  "checks": [
    {
      "name": "Database connections health check",
      "status": "UP"
    }
  ]
}

这个 JSON 响应确认应用正在正确运行,并且进行了一次检查,确认数据库是UP

所有的 health REST 端点都返回一个简单的 JSON 对象,它有两个字段:

  • status:所有健康检查程序的总体结果

  • checks:单个支票的数组

Quarkus 开箱即用,包括一个数据库检查。让我们为 Keycloak 实例创建一个健康检查:

  • ①您获得了mp.jwt.verify.publickey.location属性,它将被用作 Keycloak URL。

  • ②你用 3000 毫秒的超时实例化 Java 11 HTTPClient

  • ③您将健康检查的名称定义为Keycloak connection health check

  • ④您验证了 Keycloak URL 是可访问的,并且响应状态代码是 HTTP 200。

  • ⑤如果keycloakConnectionVerification()抛出异常,健康检查状态将为down

  • ⑥您构建健康检查响应并将其发送回调用者。

@Liveness
@ApplicationScoped
public class KeycloakConnectionHealthCheck implements HealthCheck {

    @ConfigProperty(name = "mp.jwt.verify.publickey.location", defaultValue = "false")
    private Provider<String> keycloakUrl;                                 ①

    @Override
    public HealthCheckResponse call() {

        HealthCheckResponseBuilder responseBuilder =
                HealthCheckResponse.named("Keycloak connection health check");                               ③

        try {
            keycloakConnectionVerification();                             ④
            responseBuilder.up();                                         ⑤
        } catch (IllegalStateException e) {
            // cannot access keycloak
            responseBuilder.down().withData("error", e.getMessage());                                              ⑤
        }

        return responseBuilder.build();                                   ⑥
    }

    private void keycloakConnectionVerification() {
        HttpClient httpClient = HttpClient.newBuilder()                   ②
                .connectTimeout(Duration.ofMillis(3000))
                .build();

        HttpRequest request = HttpRequest.newBuilder()
                .GET()
                .uri(URI.create(keycloakUrl.get()))
                .build();

        HttpResponse<String> response = null;

        try {
            response = httpClient.send(request, HttpResponse.BodyHandlers.ofString());
        } catch (IOException e) {
            e.printStackTrace();
        } catch (InterruptedException e) {
            e.printStackTrace();
            Thread.currentThread().interrupt();
        }

        if (response == null || response.statusCode() != 200) {
            throw new IllegalStateException("Cannot contact Keycloak");
        }
    }
}

让我们再来看一下cURL/health终点:

curl -X GET http://localhost:8080/api/health | jq '.'

新的 JSON 响应将是:img/509649_1_En_6_Figgs_HTML.gif

{
  "status": "UP",
  "checks": [
    {
      "name": "Keycloak connection health check",
      "status": "UP"
    },
    {
      "name": "Database connections health check",
      "status": "UP"
    }
  ]
}

很好!img/509649_1_En_6_Figgt_HTML.gif就这么简单!

还有呢!SmallRye Health 在http://localhost:8080/api/health-ui/提供了一个非常有用的健康 UI:

img/509649_1_En_6_Figgu_HTML.jpg

甚至还有一个池服务,可以配置为按照您设置的时间间隔刷新页面:

img/509649_1_En_6_Figgv_HTML.jpg

这个健康用户界面非常有用,但不幸的是,在prod配置文件中它没有被默认激活。要为每个配置文件/环境启用它,请使用此属性:

### Health Check
quarkus.smallrye-health.ui.always-include=true

太好了。为了实现指标服务,是时候进入下一步了。

实现度量服务

指标是重要且关键的监控数据。Quarkus 有许多用于度量公开的专用库,以及非常丰富的工具集来构建定制的应用度量。毫不奇怪,第一个库也来自 SmallRye 家族。它被称为quarkus-smallrye-metrics,用于公开基于微文件规范的度量。

从 Quarkus v1.9 开始,不推荐使用 SmallRye 度量。Quarkus 正式采用微米作为其度量的新标准。这种采用是基于云市场的趋势和需求。

img/509649_1_En_6_Figgx_HTML.gif要了解更多关于从微轮廓度量到微米度量的转换,参见 https://quarkus.io/blog/micrometer-metrics/

实现度量服务的第一步是添加quarkus-micrometer Maven 依赖项:

<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-micrometer</artifactId>
</dependency>

这种依赖性将提供对千分尺核心度量类型的访问:

  • 计数器:计数器用于测量只增加的数值。例如,测量调用 REST API 的次数。

  • 标尺:显示当前值的指示器,如创建的对象或线程的数量。

  • 定时器:用于测量短时潜伏期及其频率。

  • 分布汇总:用于跟踪事件的分布。它在结构上类似于计时器,但记录的值不代表时间单位。例如,分布摘要可用于测量到达服务器的请求的有效负载大小。

所有这些对象都需要存储在某个地方,这就是仪表注册表的目的。As Micrometer 支持很多监控系统(Prometheus、Azure Monitor、Stackdriver、Datadog、Cloudwatch 等。),我们会为MeterRegistry找一个专用的实现。在这个例子中,我们使用 Prometheus 作为监控系统。在这种情况下,您需要添加它的 Maven 依赖项:

<dependency>
    <groupId>io.micrometer</groupId>
    <artifactId>micrometer-registry-prometheus</artifactId>
</dependency>

现在,重启应用并检查 Quarkus Micrometer 生成的http://localhost:8080/metrics URL:

# HELP jvm_threads_live_threads The current number of live threads including both daemon and non-daemon threads
# TYPE jvm_threads_live_threads gauge
jvm_threads_live_threads 64.0
# HELP jvm_memory_max_bytes The maximum amount of memory in bytes that can be used for memory management
# TYPE jvm_memory_max_bytes gauge
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'profiled nmethods'",} 1.63971072E8
jvm_memory_max_bytes{area="heap",id="G1 Survivor Space",} -1.0
jvm_memory_max_bytes{area="heap",id="G1 Old Gen",} 8.589934592E9
jvm_memory_max_bytes{area="nonheap",id="Metaspace",} -1.0
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-nmethods'",} 7598080.0
jvm_memory_max_bytes{area="heap",id="G1 Eden Space",} -1.0
jvm_memory_max_bytes{area="nonheap",id="Compressed Class Space",} 1.073741824E9
jvm_memory_max_bytes{area="nonheap",id="CodeHeap 'non-profiled nmethods'",} 1.63975168E8
...

这些指标由 Micrometer 生成,与 Prometheus 兼容。您可以尝试在独立的 Prometheus 实例中导入它们。

先从 https://prometheus.io/download/ 下载普罗米修斯:

img/509649_1_En_6_Figgy_HTML.png

为您的机器/操作系统选择合适的版本并下载。对我来说是darwin-amd64

解压缩档案后,编辑prometheus-2.27.0.darwin-amd64/prometheus.yml文件,如下所示:

# my global config
global:
  scrape_interval:     15s # Set the scrape interval to every 15 seconds. Default is every 1 minute.
  evaluation_interval: 15s # Evaluate rules every 15 seconds. The default is every 1 minute.
...
scrape_configs:
  # The job name is added as a label `job=<job_name>` to any timeseries scraped from this config.
  - job_name: 'prometheus'

    # metrics_path defaults to '/metrics'
    # scheme defaults to 'http'.

    static_configs:
    - targets: ['localhost:9090']

将定义目标/metrics API 主机位置的最后一行从localhost:9090改为localhost:8080。这是应用 URL

运行普罗米修斯:

$ ./prometheus

...msg="Start listening for connections" address=0.0.0.0:9090

您可以从http://localhost:9090/访问它:

img/509649_1_En_6_Figgz_HTML.png

在表达式输入中,输入system_cpu_usage并点击执行。然后单击图表选项卡:

img/509649_1_En_6_Figha_HTML.png

现在,您可以为TokenService创建自己的定制应用指标提供者:

  • ①为执行getAccessToken()方法的持续时间创建一个名为tokensRequestsTimer的计时器。

  • ②创建一个名为tokensRequestsCounter的计数器,它将在每次调用getAccessToken()方法时递增。

@Slf4j
@RequestScoped
public class TokenService {
    private static final String TOKENS_REQUESTS_TIMER = "tokensRequestsTimer";     ①
    private static final String TOKENS_REQUESTS_COUNTER = "tokensRequestsCounter";   ②

    @Inject MeterRegistry registry;
    ...
    @PostConstruct
    public void init() {
        registry.timer(TOKENS_REQUESTS_TIMER, Tags.empty());             ①
        registry.counter(TOKENS_REQUESTS_COUNTER, Tags.empty());         ②
    }

    public String getAccessToken(String userName, String password) {
        var timer = registry.timer(TOKENS_REQUESTS_TIMER);               ①
        return timer.record(() -> { var accessToken = "";
            try {
                accessToken = getAccessToken(jwtIssuerUrlProvider.get(),
                                userName, password, clientIdProvider.get(), null);
                registry.counter(TOKENS_REQUESTS_COUNTER).increment();   ②
            } catch (IOException e) { log.error(e.getMessage());
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                log.error("Cannot get the access_token");
            }
            return accessToken;
        });
    }
    ...
}

转到 Swagger UI 并多次请求一个access_token来生成一些指标。然后返回到 Prometheus UI 检查新的指标:

img/509649_1_En_6_Fighb_HTML.png

根据前面的截图,看看tokensRequestsCounter达到的值。它的普罗米修斯表达式是:

img/509649_1_En_6_Fighc_HTML.png

您可以检查tokensRequests计时器的最大值,它与 Prometheus 中的tokensRequestsTimer_seconds_max表达式相匹配:

img/509649_1_En_6_Fighd_HTML.png

太好了。现在,您已经拥有了基本的运行状况检查和监控组件,可以确保您的应用运行良好,并拥有所有必需的资源。

结论

我认为安全和监控是抗灾难层,但即使应用了这些组件,应用仍然面临许多风险。其中之一就是高可用性。*****