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

Spring Boot3中MockMVC测试OAuth2资源服务器401状态码异常问题

问题:Spring Boot 3 OAuth2资源服务器MockMvc测试无JWT时返回403而非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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 18:50:23