12.3 使用 Spring Security 和 OAuth2 保护 API(响应式)
保护像 Order Service 这样的响应式 Spring Boot 应用程序与我们为 Catalog Service 所做的类似。Spring Security 在两个技术栈之间提供直观且一致的抽象,这使得从一个技术栈切换到另一个技术栈变得容易。
在本节中,我将指导您将 Order Service 配置为 OAuth2 资源服务器,启用 JWT 身份验证,并为 Web 端点定义安全策略。
12.3.1 将 Spring Boot 保护为 OAuth2 资源服务器
包含 Spring Security 和对资源服务器的 OAuth2 支持的 Spring Boot starter 依赖对于命令式和响应式应用程序都是相同的。在 Order Service 项目(order-service)中,打开 build.gradle 文件并添加新依赖。添加新依赖后,请记住刷新或重新导入 Gradle 依赖。
清单 12.16 添加 Spring Security OAuth2 资源服务器依赖
dependencies {
...
implementation 'org.springframework.boot:spring-boot-starter-oauth2-resource-server'
}
接下来,我们将配置 Spring Security 和 Keycloak 之间的集成。
配置 Spring Security 和 Keycloak 之间的集成
将 Spring Security 与 Keycloak 集成的策略将与我们在 Catalog Service 中所做的类似。打开 Order Service 项目(order-service),并使用以下配置更新 application.yml 文件。
清单 12.17 将 Order Service 配置为 OAuth2 资源服务器
spring:
security:
oauth2:
resourceserver:
jwt:
# OAuth2 不强制 Access Token 使用数据格式,因此我们必须明确说明我们的选择。在本例中,我们要使用 JWT
issuer-uri: http://localhost:8080/realms/PolarBookshop
# 提供有关特定领域的所有相关 OAuth2 端点信息的 Keycloak URL
Order Service 和 Keycloak 之间的集成现已建立。接下来,我们将定义必要的安全策略来保护应用程序端点。
定义 JWT 身份验证的安全策略
对于 Order Service 应用程序,我们要强制执行以下安全策略:
- 所有请求应要求身份验证。
- 应用程序应配置为 OAuth2 资源服务器并使用 JWT 身份验证。
- 处理 JWT 身份验证的流程应该是无状态的。
这与我们在 Catalog Service 中所做的有两个主要区别:
- 响应式语法与其命令式对应部分略有不同,特别是强制执行 JWT 身份验证(无状态)的部分。
- 我们没有从 Access Token 中提取用户角色,因为端点没有根据用户角色的特殊要求。
在 Order Service 项目中,在新的 com.polarbookshop.orderservice.config 包中创建一个 SecurityConfig 类。然后使用 ServerHttpSecurity 提供的 DSL 来构建配置了所需安全策略的 SecurityWebFilterChain。
清单 12.18 为 Order Service 配置安全策略和 JWT 身份验证
@EnableWebFluxSecurity
// 启用 Spring WebFlux 对 Spring Security 的支持
public class SecurityConfig {
@Bean
SecurityWebFilterChain filterChain(ServerHttpSecurity http) {
return http
.authorizeExchange(exchange -> exchange
.anyExchange().authenticated()
// 所有请求都需要身份验证
)
.oauth2ResourceServer(
ServerHttpSecurity.OAuth2ResourceServerSpec::jwt)
// 启用使用基于 JWT 的默认配置的 OAuth2 资源服务器支持(JWT 身份验证)
.requestCache(requestCacheSpec ->
requestCacheSpec.requestCache(NoOpServerRequestCache.getInstance()))
// 每个请求必须包含 Access Token,因此无需在请求之间保持会话缓存活动。我们希望它是无状态的
.csrf(ServerHttpSecurity.CsrfSpec::disable)
// 由于身份验证策略是无状态的,不涉及基于浏览器的客户端,因此我们可以安全地禁用 CSRF 保护
.build();
}
}
让我们检查这是否正常工作。首先,我们需要运行备份服务(Polar UI、Keycloak、Redis、RabbitMQ 和 PostgreSQL)。打开终端窗口,导航到保存 Docker Compose 配置的文件夹(polar-deployment/docker)并运行以下命令:
$ docker-compose up -d polar-ui polar-keycloak polar-redis polar-rabbitmq polar-postgres
然后在 JVM 上运行 Edge Service、Catalog Service 和 Order Service(从每个项目运行 ./gradlew bootRun)。最后,打开浏览器窗口并访问 http://localhost:9000。
由于 Order Service 没有根据用户角色的特殊要求,您可以使用 Isabelle(isabelle/password)或 Bjorn(bjorn/password)登录。然后从目录中选择一本书,并提交订单。由于您已通过身份验证,因此允许您创建订单。完成后,您可以访问订单页面查看所有已提交的订单。
"等一下!您说的所有已提交的订单是什么意思?"我很高兴您问了。目前,每个人都可以看到所有用户提交的订单。别担心!在本章的后面,我们将修复这个问题。
不过,在我们这样做之前,我们需要讨论如何测试新的 Order Service 安全策略。停止正在运行的应用程序(Ctrl-C)和容器(docker-compose down)。下一节将向您展示如何在响应式应用程序中测试安全性。
12.3.2 使用 Spring Security 和 Testcontainers 测试 OAuth2
测试受保护的响应式 Spring Boot 应用程序与测试命令式应用程序类似。在开始之前,我们需要添加对 Spring Security Test 和 Testcontainers Keycloak 的新依赖。添加 JUnit5 支持的 Testcontainers 依赖已经存在。打开 build.gradle 文件并按如下所示进行更新。添加新依赖后,请记住刷新并重新导入 Gradle 依赖。
清单 12.19 添加依赖以测试 Spring Security 和 Keycloak
ext {
...
set('testKeycloakVersion', "2.3.0")
// Testcontainers Keycloak 的版本
}
dependencies {
...
testImplementation 'org.springframework.security:spring-security-test'
testImplementation 'org.testcontainers:junit-jupiter'
testImplementation "com.github.dasniko:testcontainers-keycloak:${testKeycloakVersion}"
// 在 Testcontainers 之上提供 Keycloak 测试实用程序
}
我们可以使用 @SpringBootTest 和 Testcontainers Keycloak 实现完整的集成测试。由于设置与 Catalog Service 相同,我不会在这里介绍这些测试,但您可以在本书附带的仓库中找到它们(Chapter12/12-end/order-service/src/test)。确保您更新了这些集成测试,否则应用程序构建将失败。
在本节中,我们将测试端点受保护时响应式应用程序的 Web 切片,就像我们为 Catalog Service 所做的那样。
使用 @WebFluxTest 和 Spring Security 测试受保护的 REST 控制器
我们已经在 OrderControllerWebFluxTests 中使用 @WebFluxTest 为 Web 切片编写了自动测试。让我们现在看看如何更新它们以考虑安全性。
作为 OAuth2 Access Token 验证的一部分,Spring Security 依赖 Keycloak 提供的公钥来验证 JWT 签名。在内部,该框架配置了一个 ReactiveJwtDecoder Bean 来使用这些密钥解码和验证 JWT。在 Web 切片测试的上下文中,我们可以提供一个模拟的 ReactiveJwtDecoder Bean,这样 Spring Security 就会跳过与 Keycloak 的交互(这将通过完整的集成测试进行验证)。
清单 12.20 使用切片测试在 Web 层验证安全策略
@WebFluxTest(OrderController.class)
@Import(SecurityConfig.class)
// 导入应用程序安全配置
class OrderControllerWebFluxTests {
@Autowired
WebTestClient webClient;
@MockBean
OrderService orderService;
@MockBean
// 模拟 ReactiveJwtDecoder,这样应用程序就不会尝试调用 Keycloak 并获取公钥来解码 Access Token
ReactiveJwtDecoder reactiveJwtDecoder;
@Test
void whenBookNotAvailableThenRejectOrder() {
var orderRequest = new OrderRequest("1234567890", 3);
var expectedOrder = OrderService.buildRejectedOrder(
orderRequest.isbn(), orderRequest.quantity());
given(orderService.submitOrder(
orderRequest.isbn(), orderRequest.quantity()))
.willReturn(Mono.just(expectedOrder));
webClient
.mutateWith(SecurityMockServerConfigurers
.mockJwt()
// 使用具有 "customer" 角色的用户的模拟 JWT 格式 Access Token 修改 HTTP 请求
.authorities(new SimpleGrantedAuthority("ROLE_customer")))
.post()
.uri("/orders/")
.bodyValue(orderRequest)
.exchange()
.expectStatus().is2xxSuccessful()
.expectBody(Order.class).value(actualOrder -> {
assertThat(actualOrder).isNotNull();
assertThat(actualOrder.status()).isEqualTo(OrderStatus.REJECTED);
});
}
}
打开终端窗口,导航到 Order Service 根文件夹,然后按如下方式运行新添加的测试:
$ ./gradlew test --tests OrderControllerWebFluxTests
与往常一样,您可以在本书附带的源代码仓库中找到更多测试示例(Chapter12/12-end/order-service)。