精通Spring Boot 3 : 13. Spring Cloud 与 Spring Boot (3)

运行我的复古应用程序

现在是时候运行我的复古应用了。请确保 Consul 服务器和客户端仍在正常运行,同时两个用户服务应用的实例也在运行。

将端口设置为 8081(以防我们需要多个实例)。您可以通过 IDE 运行它(请记得设置 PORT 环境变量),或者使用命令行来运行:

PORT=8081 ./gradlew bootRun

一旦系统启动并运行,您可以在 http://localhost:8500/ui/dc1/services 查看 Consul 用户界面。请参见图 13-10。

图 13-10 Consul UI 服务部分展示了 my-retro-app 和 users-service

现在我的复古应用程序已经上线。如果你调用 /retros/users 接口,你会看到用户信息已经被打印出来。

所以,OpenFeign 是如何知道在哪里找到用户服务的呢?如果有两个用户应用的实例,哪个会被选中呢?这就是 Spring Boot、Spring Cloud Consul 和 Spring Cloud OpenFeign 的魅力所在;你只需声明你想要消费的服务名称(用户服务),然后 Consul 会进行服务发现并告诉 OpenFeign 去哪里。在这种情况下,由于有两个实例,OpenFeign 在后台会通过轮询的方式使用负载均衡器来访问这些实例。这真是太神奇了!这只需要很少的代码——让 Spring Boot 来完成这项工作!

HashiCorp Vault 与 Spring Cloud Vault

接下来,让我们来了解一下 HashiCorp Vault (https://www.vaultproject.io/) 和 Spring Cloud Vault,以及它们如何帮助我们解决方案。首先,我们将回顾 HashiCorp Vault 的定义,然后在我们的两个项目中应用它。

在本讨论中,提到“Vault”时,如果前面没有“HashiCorp”或“Spring Cloud”,则始终指代 HashiCorp Vault。

HashiCorp 密钥管理系统

HashiCorp Vault 是一个基于身份的秘密和加密管理系统,作为中央存储库,它可以安全地存储、访问和控制对敏感数据的访问,例如:

  • API 密钥与密码
  • 证书与加密密钥
  • 数据库认证信息
  • 基础设施认证信息

Vault 在多种环境中应对各种安全挑战,包括:

  • 微服务:安全存储和管理服务凭证、API 密钥及数据库连接。
  • 云部署:在不同云服务提供商之间集中管理和轮换云平台凭据。
  • 数据加密:使用托管的加密密钥对静态数据和传输中的数据进行加密。
  • 自动化基础设施:通过动态密钥安全地实现基础设施的自动化供应和配置。
  • 合规与审计:保持可审计的访问日志,并对机密实施精细的访问控制。

Vault 提供了一系列强大的功能,用于安全地管理机密信息:

  • 安全存储:通过对静态和传输中的数据进行加密来保护敏感信息。
  • 基于身份的访问控制:根据用户的身份和角色实施精细化的访问控制。
  • 动态密钥:根据预设策略自动生成和更新密钥。
  • 秘密租赁:提供对具有设定过期时间的秘密的临时访问权限。
  • 审计日志:记录所有访问尝试和数据修改,以便进行审计和合规性。
  • 与多种工具的集成:可以连接流行的 DevOps 和基础设施平台。
  • 多数据中心部署:在多个数据中心之间进行扩展和复制,以确保高可用性。

Vault 的一些优势包括

  • 增强安全性:集中管理和严格的访问控制可以有效降低安全风险。
  • 运营效率:优化秘密管理并自动化工作流程。
  • 提高合规性:简化审计日志和访问控制的执行。
  • 提高可扩展性:能够无缝扩展以满足日益增长的需求。

在继续之前,请务必从 Consul 中移除 db.username 和 db.password 属性。

使用 HashiCorp Vault 为 PostgreSQL 创建凭据

让我们现在开始使用 HashiCorp Vault。我们的想法是创建一个特殊的用户角色,能够根据配置频繁地创建和轮换凭据,以确保服务的安全性,并防止凭据被共享。实际上,Vault 会自动轮换凭据,而 Spring Cloud Vault 会获取新的凭据并相应地使用它们。我们仍然在使用 Consul,并可以配置 Vault 使用其存储,从而利用 Consul 提供的高可用性。

请使用以下命令启动 Vault 服务器:

docker run --cap-add=IPC_LOCK -d --rm --name vault -p 8200:8200 \
-e 'VAULT_LOCAL_CONFIG={"backend": {"consul": {"address": "host.docker.internal:8500","path":"vault/"}}}' \
-e 'VAULT_DEV_ROOT_TOKEN_ID=my-root-id' \
vault:1.13.3

此命令使用 Consul 作为存储启动 Vault。同时,我们定义了一个名为 my-root-id 的 TOKEN_ID。这只是开发环境,但通常您需要生成这个密钥 (TOKEN_ID) 及其值,并将其用作身份验证机制。如果您查看 Consul 用户界面,应该能看到 Vault 被列为服务。请参见图 13-11。

图 13-11 Consul UI 服务部分显示了列出的 Vault(http://localhost:8500/ui/dc1/services)

如果你查看 Consul 中的键值部分,你应该能看到 Vault 的密钥配置。

接下来,我们需要配置 Vault,以便使用数据库密钥,并根据默认策略轮换我们的凭据。为此,您可以使用 cURL 指向其 REST API,或者使用 vault CLI 工具。您可以从 https://developer.hashicorp.com/vault/docs/install 下载并安装 CLI 工具。然后,您可以使用以下命令登录到 Vault:

vault login -address="http://127.0.0.1:8200"

该 TOKEN 将作为我的根 ID。此登录是执行后续命令所必需的。

接下来,启用数据库密钥管理引擎

vault secrets enable -address="http://127.0.0.1:8200" database

接下来,建立与数据库的连接,并使用 Vault 中自带的 PostgreSQL 插件:

vault write -address="http://127.0.0.1:8200" \
     database/config/users_db \
     plugin_name=postgresql-database-plugin \     connection_url="postgresql://{{username}}:{{password}}@host.docker.internal:5432/users_db?sslmode=disable" \
     allowed_roles="*" \
     username="admin" \
     password="mysecretpassword"

需要注意的一个主要事项是 connection_url 指向 host.docker.internal,这是因为我们要访问容器外部,因此需要知道 docker 主机的 IP。它不能是 localhost,因为 PostgreSQL 有自己的 IP,但通过 5432 端口提供访问。

接下来,我们需要创建一个角色,该角色将负责我们数据库中的所有操作,其凭据将根据 Vault 的默认策略进行轮换(在这种情况下,仅需几分钟)。当然,您可以更改这些默认设置,但为了在这里进行概念验证,我们将保留默认值。请执行以下命令:

vault write -address="http://127.0.0.1:8200" \
     database/roles/users-role db_name=users_db \
     creation_statements="CREATE ROLE \"{{name}}\" WITH LOGIN PASSWORD '{{password}}' VALID UNTIL '{{expiration}}'; GRANT ALL PRIVILEGES ON DATABASE users_db TO \"{{name}}\"; GRANT USAGE ON SCHEMA public TO \"{{name}}\"; GRANT ALL PRIVILEGES ON ALL TABLES IN SCHEMA public TO \"{{name}}\"; ALTER DATABASE users_db OWNER TO \"{{name}}\";" \
     default_ttl="30s" \
     max_ttl="1m"

前面的命令基本上是创建一个角色(用户角色),并使其能够在 users_db 上进行操作。我们使用 SQL 语句为这个将在需要时创建的用户角色授予一些权限。

Vault 会定期更换其凭据,这意味着如果您使用之前的凭据,用户将不再有效。如果您想了解用户及其凭据,可以执行以下命令:

vault read -address="http://127.0.0.1:8200" \
database/creds/users-role
Key                Value
---                -----
lease_id           database/creds/users-role/t8vRNfXvONqy15u41EQqqX3Q
lease_duration     30s
lease_renewable    true
password           ci8HZxkgJJn-KIvMRWoe
username           v-token-users-ro-2wtaGdop4qbR63NMsI1L-1707512396

输出中已创建用户名和密码,现在你可以开始使用了。


您可以通过访问 http://localhost:8200 来打开 Vault 用户界面。请参见图 13-12。


图 13-12 Vault UI 登录页面 (http://localhost:8200 - 令牌/我的根 ID)

令牌是我的根 ID。接下来,您可以浏览并查看秘密引擎(见图 13-13)和数据库引擎(见图 13-14)。


图 13-14 保险库用户界面数据库引擎

在秘密引擎页面,点击“启用新引擎”链接以查看其他可用的秘密引擎(见图 13-15)。在我们的情况下,我们将只使用标准数据库。

图 13-15 Vault 用户界面显示可用的秘密引擎列表 (http://localhost:8200/ui/vault/settings/mount-secret-backend)

Spring Cloud Vault

现在我们已经成功启动了 Vault,让我们来了解一下 Spring Cloud Vault 的概念,以及它如何帮助我们优化应用程序。

Spring Cloud Vault 是一个项目,旨在帮助您将 Spring 应用程序与 HashiCorp Vault 集成,以安全地管理机密。简单来说,Spring Cloud Vault 为使用 Vault 作为中央配置源的外部化配置提供了客户端支持。让我们深入探讨它的优势和特点:

Spring Cloud Vault 提供以下优势(包括但不限于):

  • 提升安全性:将密码、API 密钥及其他敏感凭据存储在 Vault 中,而不是直接在应用程序代码中,可以大大降低信息泄露和潜在安全漏洞的风险。
  • 集中管理:Vault 提供一个统一的平台,帮助您在所有应用程序和环境中管理机密,简化操作流程,确保一致性。
  • 动态配置:Spring Cloud Vault 可以为各种服务(如数据库、云平台等)动态生成凭据,确保这些凭据始终保持新鲜和安全。
  • 降低代码复杂性:您无需在代码中直接嵌入机密信息,这样可以避免硬编码,提高代码的可维护性。
  • 增强的可配置性:可以轻松管理配置更改和回滚,而不会影响您的应用部署。

这些是它的一些特性:

  • 秘密获取:从 Vault 中提取秘密,并根据设定的策略自动进行更新。
  • 环境初始化:通过从 Vault 获取的远程属性源来初始化 Spring 环境。
  • 安全通信:支持通过 SSL 和多种身份验证机制与 Vault 进行安全的通信。
  • 凭证生成:为 MySQL、PostgreSQL、AWS 等多种服务动态生成凭证。
  • 多种身份验证方式:支持令牌、AppId、AppRole、客户端证书、Cubbyhole、AWS EC2 和 IAM,以及 Kubernetes 身份验证,确保安全访问。
  • 云平台集成:通过 Spring Cloud Vault Connector 实现与 HashiCorp Vault 服务代理的集成。

在用户服务应用中使用 HashiCorp Vault 实现凭证生成

我们将使用用户应用程序,并进行一些修改。请记住,您可以在 13-cloud/users 文件夹中访问源代码,以便跟随学习。

因此,我们首先需要将必要的依赖项添加到 build.gradle 文件中。在这种情况下,请添加 spring-cloud-vault-config-databases 和 spring-cloud-starter-vault-config 依赖项,如清单 13-10 所示。

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.2.2'
    id 'io.spring.dependency-management' version '1.1.4'
    id 'org.hibernate.orm' version '6.1.7.Final'
    id 'org.graalvm.buildtools.native' version '0.9.20'
}
group = 'com.apress'
version = '0.0.1-SNAPSHOT'
java {
    sourceCompatibility = '17'
}
configurations {
    compileOnly {
        extendsFrom annotationProcessor
    }
}
ext {
    set('springCloudVersion', "2023.0.0")
}
repositories {
    mavenCentral()
}
dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}
dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-validation'
    implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-actuator'
    // Consult
    implementation 'org.springframework.cloud:spring-cloud-starter-consul-config'
    implementation 'org.springframework.cloud:spring-cloud-starter-consul-discovery'
    // Vault
    implementation 'org.springframework.cloud:spring-cloud-vault-config-databases'
          implementation 'org.springframework.cloud:spring-cloud-starter-vault-config'
    runtimeOnly 'org.postgresql:postgresql'
    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'
    // Web
    implementation 'org.webjars:bootstrap:5.2.3'
    // Test
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
    useJUnitPlatform()
}
tasks.named("bootBuildImage") {
    builder = "dashaun/builder:tiny"
    environment = ["BP_NATIVE_IMAGE" : "true"]
}
hibernate {
    enhancement {
        lazyInitialization true
        dirtyTracking true
        associationManagement true
    }
}

列表 13-10 的 build.gradle 文件

接下来,我们来修改 application.yaml。请按照列表 13-11 中所示的内容进行添加或替换。

spring:
  application:
    name: users-service
  config:
    import: consul://,vault://
  cloud:
    vault:
      authentication: TOKEN
      token: ${VAULT_TOKEN}  # my-root-id
      scheme: http
      database:
        enabled: true
        role: users-role
      config:
        lifecycle:
          enabled: true
          min-renewal: 10s
          expiry-threshold: 1m
      fail-fast: true
  datasource:
    url: jdbc:postgresql://localhost:5432/users_db?sslmode=disable
  jpa:
    generate-ddl: true
    show-sql: true
    hibernate:
      ddl-auto: update
info:
  developer:
    name: Felipe
    email: felipe@email.com
  api:
    version: 1.0
management:
  endpoints:
    web:
     exposure:
       include: health,info,event-config,shutdown,configprops,beans
  endpoint:
    configprops:
      show-values: always
    health:
      show-details: always
      status:
        order: events-down, fatal, down, out-of-service, unknown, up
    shutdown:
      enabled: true
  info:
    env:
      enabled: true
logging:
  level:
    org.springframework.vault: ERROR
server:
  port: ${PORT:8080}

列表 13-11 源自 main/resources/application.yaml

让我们来回顾一下应用程序.yaml 文件中的新内容和变化。

  • 由于我们使用 Vault,因此需要从 vault://获取凭据(这些凭据设置在 spring.datasource.username 和 spring.datasource.password 属性中)。同时,我们仍然使用 Consul 来获取其他属性,因此可以同时声明这两者,例如 consul://和 vault://。
  • spring.cloud.vault.*:本节内容全部与 Vault 相关。首先,我们需要告诉 Vault 我们将如何进行身份验证,这里使用的是 TOKEN,以及指向值为 my-root-id 的${VAULT_TOKEN}环境变量的令牌值。接着,我们有 database.enabled 和 database.role 属性,它们使用的是我们之前通过命令行创建的 users-role。此外,还有一些额外的属性被视为默认值。
  • 这个 URL 仅指向数据库 users_db,仔细观察可以发现没有用户名和密码,因为 Vault 会生成凭据并授予创建的用户权限,而 Spring Cloud Vault 会自动收集这些凭据并将其添加到 spring.datasource.username 和 spring.datasource.password 属性中。

其他属性保持不变。就这样。这更多是配置而不是代码,对吗?

使用 Vault 启动用户应用程序

在运行用户应用程序之前,请确保在您的 IDE 中(如果您是从那里运行)或通过执行以下命令设置环境变量 VAULT_TOKEN=my-root-id:

PORT=8091 VAULT_TOKEN=my-root-id ./gradlew bootRun

现在一切应该都能正常工作了!数据库的凭据是由 Vault 创建的,Spring Cloud Vault 将其配置在我们的应用程序中。但是等等!如果你执行一个 curl 命令或者在浏览器中访问 http://localhost:8091/users,会发生什么呢?

几分钟后,它将出现如下错误:

org.postgresql.util.PSQLException: ERROR: permission denied for table people

你知道为什么吗?没错,Vault 具有凭证轮换功能,因此它在一两分钟内就会自动更改凭证;这取决于你可以在 Vault 中修改的策略。那么,我们该如何解决这个问题呢?

通过添加监听器来优化凭证轮换

Vault 和 Spring Cloud Vault 的一个很酷的功能是可以通过监听器发送任何更改的事件。在这种情况下,我们可以在凭证轮换发生时添加一个监听器。Spring Cloud Vault 提供的 SecretLeaseContainer 类可以帮助我们检测 Vault 何时续租或轮换。

创建或打开 UserConfiguration 类,详见第 13-12 页。

package com.apress.users.config;
import com.apress.users.model.User;
import com.apress.users.model.UserRole;
import com.apress.users.service.UserService;
import com.zaxxer.hikari.HikariConfigMXBean;
import com.zaxxer.hikari.HikariDataSource;
import com.zaxxer.hikari.HikariPoolMXBean;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.vault.core.lease.SecretLeaseContainer;
import org.springframework.vault.core.lease.domain.RequestedSecret;
import org.springframework.vault.core.lease.event.SecretLeaseCreatedEvent;
import org.springframework.vault.core.lease.event.SecretLeaseExpiredEvent;
import java.util.Arrays;
@Slf4j
@RequiredArgsConstructor
@Configuration
@EnableConfigurationProperties({UserProperties.class})
public class UserConfiguration {
    @Bean
    CommandLineRunner init(UserService userService){
        return args -> {
            userService.saveUpdateUser(new User("ximena@email.com","Ximena","https://www.gravatar.com/avatar/23bb62a7d0ca63c9a804908e57bf6bd4?d=wavatar","aw2s0meR!", Arrays.asList(UserRole.USER),true));
            userService.saveUpdateUser(new User("norma@email.com","Norma" ,"https://www.gravatar.com/avatar/f07f7e553264c9710105edebe6c465e7?d=wavatar", "aw2s0meR!", Arrays.asList(UserRole.USER, UserRole.ADMIN),false));
        };
    }
    @Value("${spring.cloud.vault.database.role}")
    private String databaseRoleName;
    private final SecretLeaseContainer secretLeaseContainer;
    private final HikariDataSource hikariDataSource;
    @PostConstruct
    private void postConstruct() {
        final String vaultCredentialsPath = String.format("database/creds/%s", databaseRoleName);
        secretLeaseContainer.addLeaseListener(event -> {
            log.info("[SecretLeaseContainer]> Received event: {}", event);
            if (vaultCredentialsPath.equals(event.getSource().getPath())) {
                if (event instanceof SecretLeaseExpiredEvent &&
                        event.getSource().getMode() == RequestedSecret.Mode.RENEW) {
                    log.info("[SecretLeaseContainer]> Let's replace the RENEWED lease by a ROTATE one.");
                    secretLeaseContainer.requestRotatingSecret(vaultCredentialsPath);
                } else if (event instanceof SecretLeaseCreatedEvent secretLeaseCreatedEvent &&
                        event.getSource().getMode() == RequestedSecret.Mode.ROTATE) {
                    String username = secretLeaseCreatedEvent.getSecrets().get("username").toString();
                    String password = secretLeaseCreatedEvent.getSecrets().get("password").toString();
                    updateHikariDataSource(username, password);
                }
            }
        });
    }
    private void updateHikariDataSource(String username, String password) {
        log.info("[SecretLeaseContainer]> Soft evict the current database connections");
        HikariPoolMXBean hikariPoolMXBean = hikariDataSource.getHikariPoolMXBean();
        if (hikariPoolMXBean != null) {
            hikariPoolMXBean.softEvictConnections();
        }
        log.info("[SecretLeaseContainer]> Update database credentials with the new ones.");
        HikariConfigMXBean hikariConfigMXBean = hikariDataSource.getHikariConfigMXBean();
        hikariConfigMXBean.setUsername(username);
        hikariConfigMXBean.setPassword(password);
    }
}

用户配置类的代码示例 13-12src/main/java/com/apress/users/config/UserConfiguration.java

UserConfiguration 类将帮助识别更新后的凭据并进行轮换。我们来一起回顾一下:

  • @RequiredArgsConstructor:这个注解(在第三章中介绍)来自 Lombok 库,允许我们定义作为构造函数参数的 final 字段,这些字段将由 Spring 进行注入。字段包括 databaseRoleName、SecretLeaseContainer 和 HikariDataSource(这是 Spring Boot 在使用数据库时的默认配置)。
  • 这是在我们需要获取 vault://database/creds/ 时使用的;在这种情况下,值是定义在 application.yaml 文件中的用户角色,以及我们通过 vault CLI 创建的内容。
  • SecretLeaseContainer:这个类是一个基于事件的容器,它从 Vault 请求机密并续订与机密相关的租约。我们正在添加一个租约监听器,以便每当有新的或续订的租约时,从 Vault 接收通知。我们需要请求 SecretLeaseExpiredEvent 和机密模式(RENEW 或 ROTATE);如果是 RENEW,我们需要请求旋转的机密;如果是 ROTATE,我们需要获取新的用户名和密码,并更新数据源。
  • 默认情况下,Spring Boot 使用 HikariDataSource 作为数据源,因此我们需要通过 HikariPoolMXBean 来更新用户名和密码。

然后,如果你再次运行用户应用程序并等待一两分钟,你会看到关于接收 SecretLeaseContainer 事件的日志,接着是通过使用数据源(HikariDataSource)更新来续订旋转凭据的逻辑。这真是太酷了!现在,你知道如何旋转凭据了。

如果您想了解更多关于可以在 Vault 中应用的策略,请访问此链接:https://developer.hashicorp.com/vault/docs/concepts/policies。

接下来,我们需要通过添加一个 API 网关来完成云架构(如图 13-1 所示)。

Spring Cloud Gateway

Spring Cloud Gateway 是一个基于 Spring 生态系统构建的 API 网关,旨在简化 API 路由并为您的微服务架构增添重要功能。我们可以将其定义为

  • 基于 Java 的 API 网关,采用 Spring 框架、Spring Boot 和 Project Reactor 构建而成
  • 作为微服务的统一入口,根据不同标准将请求路由到相应的服务
  • 提供安全性、监控和网关级别的弹性等“跨领域关注点”

Spring Cloud Gateway 的一些主要特点包括

  • 路由:根据 URL 路径、请求头、请求方法等定义路由。
  • 谓词:通过内置或自定义谓词控制哪些请求与特定路由匹配。
  • 过滤器:为请求和响应添加预处理和后处理逻辑。
  • 安全:与 Spring Security 集成,实现身份验证和授权。
  • 监控:跟踪指标并为网关及其下游服务提供健康检查。
  • 弹性:与 Spring Cloud Circuit Breaker 集成,提供容错和回退机制。
  • 发现:与 Spring Cloud DiscoveryClient 集成,自动发现您的微服务。
  • 其他功能:包括速率限制、路径重写、Hystrix 集成等多种功能。

Spring Cloud Gateway 的一些应用场景包括

  • 单一入口点:通过提供统一的 API 接口,简化了对微服务的访问。
  • 安全性:为所有 API 访问提供集中管理的安全措施。
  • 监控:提供 API 流量和微服务健康状况的深入洞察。
  • 弹性:防止服务故障,确保高可用性。
  • 负载均衡:将流量分配到多个微服务实例中。
  • API 管理:让您能够控制访问权限、限制请求速率并收集使用数据。

总的来说,Spring Cloud Gateway 是一个强大的工具,可以帮助您管理和优化微服务架构。它提供灵活的路由、强大的安全性以及有用的监控功能,以便更好地简化您的 API 交互。