You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Spring集成测试:Mock BearerTokenAuthentication与OAuth2问题排查

集成测试中@WithJwt生成JwtAuthenticationToken而非BearerTokenAuthentication的问题解决

问题背景

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.16 21:03:16