Spring Boot3中MockMVC测试OAuth2资源服务器401状态码异常问题
我正在开发基于Spring Boot 3的REST API应用,使用Keycloak作为OAuth2身份提供商,依赖org.springframework.boot:spring-boot-starter-oauth2-resource-server。实际运行时认证授权功能符合预期,但编写WebMvcTest时遇到异常:当用户未提供JWT进行认证时,期望返回401(未认证)状态码,实际却得到403(禁止访问)。
已成功通过的测试用例:
- 用户提供包含预期声明的JWT → 返回200 ✔
- 用户提供不包含预期声明的JWT → 返回403 ✔
相关代码
测试类代码
@WebMvcTest(CustomerController.class) @ImportAutoConfiguration(classes = {RequestInformationExtractor.class}) @ContextConfiguration(classes = SecurityConfiguration.class) @Import({TestConfiguration.class, CustomerController.class}) public class PartnerControllerTest { @Autowired private WebApplicationContext context; private MockMvc mockMvc; @BeforeEach public void setup() { mockMvc = MockMvcBuilders .webAppContextSetup(context) .apply(springSecurity()) .build(); } // 运行成功 @Test void shouldReturnListOfCustomers() throws Exception { mockMvc.perform( post("/search") .contentType(MediaType.APPLICATION_JSON) .content("{" + "\"searchKeyword\": \"Mustermann\"" + "}") .with(jwt() .authorities( new SimpleGrantedAuthority("basic") ))) .andExpect(status().isOk()); } // 失败:期望401但得到403 @Test void shouldReturn401WithoutJwt() throws Exception { mockMvc.perform( post("/search") .contentType(MediaType.APPLICATION_JSON) .content("{" + "\"searchKeyword\": \"Mustermann\"" + "}")) .andExpect(status().isUnauthorized()); } // 运行成功 @Test void shouldReturn403() throws Exception { mockMvc.perform( post("/search") .contentType(MediaType.APPLICATION_JSON) .content("{" + "\"searchKeyword\": \"Mustermann\"" + "}") .with(jwt())) .andExpect(status().isForbidden()); } }
测试配置类
@org.springframework.boot.test.context.TestConfiguration public class TestConfiguration { @Bean public JwtDecoder jwtDecoder() { SecretKey secretKey = new SecretKeySpec("dasdasdasdfgsg9423942342394239492349fsd9fsd9fsdfjkldasd".getBytes(), JWSAlgorithm.HS256.getName()); NimbusJwtDecoder jwtDecoder = NimbusJwtDecoder.withSecretKey(secretKey).build(); return jwtDecoder; } }
安全配置类
@Configuration @EnableWebSecurity public class SecurityConfiguration { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(STATELESS)) .authorizeHttpRequests((authz) -> authz .requestMatchers("/actuator/**").permitAll() .anyRequest().hasAuthority("Basic") ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); return http.build(); } @Bean public JwtAuthenticationConverter jwtAuthenticationConverter() { JwtGrantedAuthoritiesConverter grantedAuthoritiesConverter = new JwtGrantedAuthoritiesConverter(); grantedAuthoritiesConverter.setAuthoritiesClaimName("groups"); grantedAuthoritiesConverter.setAuthorityPrefix(""); JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter(); jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(grantedAuthoritiesConverter); return jwtAuthenticationConverter; } }
原因分析
这并非MockMvc模拟流程的问题,而是Spring Security默认逻辑导致:当请求无认证信息时,AnonymousAuthenticationFilter会自动创建匿名用户(AnonymousAuthenticationToken),该用户无任何权限。后续authorizeHttpRequests中的anyRequest().hasAuthority("Basic")规则会触发授权检查,因匿名用户缺少必要权限,故返回403。
实际运行时,OAuth2资源服务器的BearerTokenAuthenticationFilter会优先拦截无JWT的请求并返回401,但MockMvc测试环境中过滤器优先级与匿名认证的交互导致了差异。
解决方案
在安全配置中显式禁用匿名认证,这样无JWT的请求会被OAuth2资源服务器过滤器直接识别为未认证,返回401:
修改SecurityConfiguration的filterChain方法,添加.anonymous(AbstractHttpConfigurer::disable):
@Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(STATELESS)) .anonymous(AbstractHttpConfigurer::disable) // 禁用匿名认证 .authorizeHttpRequests((authz) -> authz .requestMatchers("/actuator/**").permitAll() .anyRequest().hasAuthority("Basic") ) .oauth2ResourceServer(OAuth2ResourceServerConfigurer::jwt); return http.build(); }
修改后,无JWT的测试用例会返回预期的401状态码。
内容的提问来源于stack exchange,提问作者LucasMoody

