Java Spring Mock Bearer Token 认证与 OAuth2 集成测试方案
2024-12-15 19:43:09
Java Spring 集成测试中 Mock Bearer Token 认证与 OAuth2 的问题及解决方案
在 Java Spring 应用中,对 Web 层进行集成测试时,经常会遇到模拟 Bearer Token 认证的场景。当控制器方法参数包含 BearerTokenAuthentication
和 @AuthenticationPrincipal OidcUser
时,如何正确地 Mock 这些参数并保证测试的准确性,是一个常见的问题。本文将深入分析该问题,并提供多种解决方案。
问题分析
问题的核心在于 Spring Security 上下文中用户 Principal 类型与控制器期望的 BearerTokenAuthentication
类型不匹配。具体表现为测试过程中抛出 java.lang.IllegalStateException: Current user principal is not of type [org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication]
异常。
出现这个问题的原因可能包括:
- 自定义 JwtAuthenticationConverter 未生效: 虽然配置了自定义
JwtBearerTokenAuthenticationConverter
,但测试过程中可能未被正确应用。 - Mock 方式不兼容: 使用
@WithJwt
注解进行 Mock 时,可能只生成了JwtAuthenticationToken
,而不是BearerTokenAuthentication
。 - 测试配置缺失: Spring Boot 测试环境的配置可能与实际运行环境存在差异,导致认证机制行为不一致。
解决方案
针对上述原因,可以采用以下解决方案:
方案一:使用 SecurityContextHolder 手动设置 Authentication
直接构建 BearerTokenAuthentication
对象并设置到 Spring Security 上下文中。这种方式最为直接,可以精确控制认证对象。
代码示例:
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
// ... 其他导入
@Test
public void testGetBooks() throws Exception {
// 构建 BearerTokenAuthentication 对象
Jwt jwt = Jwt.withTokenValue("mock-token")
.header("alg", "none")
.claim("sub", "user123")
.build();
BearerTokenAuthentication authentication = new BearerTokenAuthentication(jwt, List.of(), jwt);
SecurityContext securityContext = SecurityContextHolder.createEmptyContext();
securityContext.setAuthentication(authentication);
SecurityContextHolder.setContext(securityContext);
ResponseEntity<List<Book>> responseEntity = ResponseEntity.ok(List.of(sampleBooks));
when(feignClient.getBooks()).thenReturn(responseEntity);
this.mockMvc.perform(MockMvcRequestBuilders.get("/books")
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(status().isOk());
}
操作步骤:
- 在测试方法中,使用
Jwt.withTokenValue()
方法构建一个模拟 Jwt 对象。 - 使用
BearerTokenAuthentication
构造函数,传入 Jwt 对象、授权信息和凭据(通常也是 Jwt 对象),创建一个BearerTokenAuthentication
实例。 - 创建空的
SecurityContext
,并将构建好的BearerTokenAuthentication
对象设置进去。 - 将
SecurityContext
设置到SecurityContextHolder
中。 - 执行 MockMvc 请求。
额外安全建议: 在生产环境中,应该使用安全的密钥和签名算法。 Mock JWT 仅用于测试目的,切勿在生产环境中使用 Mock JWT。
方案二:替换 JwtAuthenticationConverter 使用 Mockito 替换 JwtAuthenticationConverter 并指定返回自定义 BearerTokenAuthentication
对象。
代码示例:
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication;
// ... 其他导入
@SpringBootTest
@AutoConfigureMockMvc
public class BookApiTest {
// ... 其他成员变量
@MockBean
JwtAuthenticationConverter jwtAuthenticationConverter;
// ... 其他方法
@Test
@WithJwt("jwt.json")
public void testGetBooks() throws Exception {
Jwt jwt = Jwt.withTokenValue("mock-token")
.header("alg", "none")
.claim("sub", "user123")
.build();
BearerTokenAuthentication authentication = new BearerTokenAuthentication(jwt, List.of(), jwt);
when(jwtAuthenticationConverter.convert(any(Jwt.class))).thenReturn(authentication);
ResponseEntity<List<Book>> responseEntity = ResponseEntity.ok(List.of(sampleBooks));
when(feignClient.getBooks()).thenReturn(responseEntity);
this.mockMvc.perform(MockMvcRequestBuilders.get("/books")
.with(SecurityMockMvcRequestPostProcessors.csrf()))
.andExpect(status().isOk());
}
}
操作步骤:
- 使用
@MockBean
注解 MockJwtAuthenticationConverter
。 - 在测试方法中,使用
Jwt.withTokenValue()
构建模拟 Jwt 对象。 - 构建
BearerTokenAuthentication
对象。 - 使用
Mockito.when
指定JwtAuthenticationConverter.convert
方法的返回值,改为 Mock 的BearerTokenAuthentication
。 - 执行 MockMvc 请求。
额外安全建议: 测试过程中使用的 JWT 模拟数据不应该包含敏感信息。
方案三: 使用 @Import 替换 security configuration 来引入专门用于测试的安全配置。该配置可以移除 OAuth2 的配置,只启用基本的认证。
代码示例:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
@Import(BookApiTest.TestSecurityConfig.class)
public class BookApiTest {
// ... 成员变量与之前相同
@Configuration
@EnableWebSecurity
public static class TestSecurityConfig{
@Bean
@Primary
public SecurityFilterChain securityFilterChainTest(HttpSecurity http) throws Exception {
http.csrf(AbstractHttpConfigurer::disable)
.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll());
return http.build();
}
}
// ... 测试方法同方案二
}
操作步骤:
- 创建一个测试安全配置 TestSecurityConfig
- @ActiveProfiles("test") 注解,使得 test 的配置仅在测试环境下启用.
- @Import 引入专门为测试准备的安全配置 TestSecurityConfig,覆盖原有安全配置.
- 移除了 Jwt decoder 及 Authentication convert 相关的代码. 这样 Spring security 上下文中便不会有这些组件. 可以测试 controller 本身的功能
- 在测试的安全配置中 http.authorizeHttpRequests(authorize -> authorize.anyRequest().permitAll()) 配置绕过了权限认证,以保证单元测试专注于测试业务逻辑.
额外安全建议: 分离测试环境与生产环境的安全配置。 保证测试环境的配置简单化. 生产环境的配置复杂化及安全性最大化。
总结
本文针对 Java Spring 集成测试中 Mock Bearer Token 认证与 OAuth2 的问题,提出了三种解决方案。开发者可以根据具体情况选择最合适的方案。在选择方案时,需要综合考虑测试的精度、代码的复杂度和可维护性等因素。通过合理运用 Mock 技术,可以有效地提高集成测试的效率和准确性,确保应用的安全性和可靠性。
相关资源
- Spring Security 官方文档: https://docs.spring.io/spring-security/reference/index.html
- Spring Boot Test 官方文档: https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-testing
- Mockito 官方文档: https://site.mockito.org/
希望以上解决方案能帮助您解决 Java Spring 集成测试中遇到的 Bearer Token 认证 Mock 问题。