Spring集成测试:Mock BearerTokenAuthentication与OAuth2问题排查
问题背景
Web层集成测试时,/getBooks端点依赖BearerTokenAuthentication入参和@AuthenticationPrincipal OidcUser。生产环境通过自定义JwtBearerTokenAuthenticationConverter将JWT转换为BearerTokenAuthentication存入安全上下文,但使用c4_soft.springaddons的@WithJwt注解模拟认证时,安全上下文里的认证对象是JwtAuthenticationToken,导致类型不匹配报错:
jakarta.servlet.ServletException: Request processing failed: java.lang.IllegalStateException: Current user principal is not of type [org.springframework.security.oauth2.server.resource.authentication.BearerTokenAuthentication]: JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@a795d843, Credentials=[PROTECTED], Authenticated=true, Details=null, Granted Authorities=[]]
原因分析
@WithJwt注解的默认逻辑是直接将解析后的JWT封装为JwtAuthenticationToken存入安全上下文,不会经过你配置的JwtAuthenticationConverter——这个转换器仅在OAuth2资源服务器的过滤器链处理真实请求时生效,而@WithJwt是跳过过滤器链直接设置认证对象的。
解决方案
方案一:自定义@WithJwt的认证对象生成逻辑
利用c4_soft.springaddons的扩展能力,实现AuthenticationFactory<Jwt>接口,复用你的自定义转换器逻辑生成BearerTokenAuthentication:
@Component public class BearerTokenAuthFactory implements AuthenticationFactory<Jwt> { @Override public Authentication create(Jwt jwt) { // 复用生产环境的转换器 JwtBearerTokenAuthenticationConverter converter = new JwtBearerTokenAuthenticationConverter(); return converter.convert(jwt); } }
然后在测试方法的@WithJwt注解中指定该工厂:
@Test @WithJwt(value = "jwt.json", authenticationFactory = BearerTokenAuthFactory.class) public void testGetBooks() throws Exception { 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()); }
方案二:测试前手动转换认证对象
在测试前置方法中,将@WithJwt生成的JwtAuthenticationToken转换为BearerTokenAuthentication:
@BeforeEach void adjustAuthentication() { Authentication currentAuth = SecurityContextHolder.getContext().getAuthentication(); if (currentAuth instanceof JwtAuthenticationToken jwtAuth) { JwtBearerTokenAuthenticationConverter converter = new JwtBearerTokenAuthenticationConverter(); BearerTokenAuthentication bearerAuth = (BearerTokenAuthentication) converter.convert(jwtAuth.getToken()); SecurityContextHolder.getContext().setAuthentication(bearerAuth); } }
注意:确保
@BeforeEach在@WithJwt之后执行,可通过调整测试框架的执行顺序(如使用@Order)保证逻辑生效。
方案三:模拟真实请求流程(推荐)
放弃@WithJwt,转而在MockMvc请求中携带Bearer Token,让请求走完整的安全过滤器链,自然触发自定义转换器的逻辑:
@Test public void testGetBooks() throws Exception { // 构建Mock Jwt对象,匹配jwt.json中的内容 Jwt mockJwt = Jwt.withTokenValue("test-token") .header("alg", "HS256") .claim("sub", "test-user") // 添加jwt.json中的其他声明 .build(); when(jwtDecoder.decode(anyString())).thenReturn(mockJwt); ResponseEntity<List<Book>> responseEntity = ResponseEntity.ok(List.of(sampleBooks)); when(feignClient.getBooks()).thenReturn(responseEntity); this.mockMvc.perform(MockMvcRequestBuilders.get("/books") .header("Authorization", "Bearer test-token") .with(SecurityMockMvcRequestPostProcessors.csrf())) .andExpect(status().isOk()); }
这种方式完全模拟生产环境的请求链路,测试结果更可信,同时也能自动处理OidcUser的转换(只要你的转换器逻辑包含这部分)。
内容的提问来源于stack exchange,提问作者rwd762

