精通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 交互。