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

Spring Security 6.5下JUnit测试OAuth2资源服务器Register API遇401问题

问题

使用JUnit(Spring Security 6.5)测试Register API时遇到异常:API在Postman中正常运行,但JUnit测试返回401而非预期的200。

测试代码

@WebMvcTest(AuthenticationController.class)
class AuthenticationControllerTest {

    @Autowired
    private MockMvc mockMvc;

    // ... 其他Mock及依赖

    @Test
    void testRegister() throws Exception {
        String username = "example";
        String password = "exPw123";
        String email = "example@gmail.com";

        AuthenticationRequest request = new AuthenticationRequest(username, password, email);
        UserDto userDto = new UserDto(username, email);
        User user = Mockito.mock(User.class);

        Mockito.when(user.getDto()).thenReturn(userDto);
        Mockito.when(authenticationService.register(any(), any(), any(), any())).thenReturn(user);

        mockMvc.perform(post("/api/auth/v1/register")
                .with(csrf())
                .header("role", "learner")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.email").value(email))
                .andExpect(jsonPath("$.username").value(username));
    }
}

SecurityFilterChain配置

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http
            .csrf(csrf -> csrf.disable())
            .securityMatcher("/**")
            .authorizeHttpRequests(auth -> {
                auth.requestMatchers("/api/auth/**", "/swagger-ui/*", "/v3/api-docs*/**", "/swagger-resources/*", "/swagger-ui.html", "/").permitAll();
                auth.requestMatchers(HttpMethod.OPTIONS, "/**").permitAll();
                auth.requestMatchers("/learner/**").hasAnyRole("LEARNER", "ADMIN");
                auth.requestMatchers("/admin/**").hasAnyRole("ADMIN");
                auth.anyRequest().authenticated();
            })
            .oauth2ResourceServer(
                    oauth2 -> oauth2.jwt(
                            jwtConfigurer -> jwtConfigurer
                                    .jwtAuthenticationConverter(jwtAuthenticationConverter())))
            .addFilterBefore(jwtCookieFilter, UsernamePasswordAuthenticationFilter.class)
            .sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
            .headers(headers -> headers.frameOptions(frameOptions -> frameOptions.disable()));

    return http.build();
}

问题细节

  • 给测试类添加@AutoConfigureMockMvc(addFilters = false)可通过测试,但不符合真实场景要求
  • 明明配置了/api/auth/**为permitAll,测试仍返回401
  • 尝试添加@WithMockUser或模拟JWT后,状态码变为200,但响应体为空

测试日志

401情况

MockHttpServletResponse:
           Status = 401
    Error message = Unauthorized
          Headers = [WWW-Authenticate:"Basic realm="Realm"", X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

添加@WithMockUser/@WithMockJWT后的200情况

MockHttpServletResponse:
           Status = 200
    Error message = null
          Headers = [X-Content-Type-Options:"nosniff", X-XSS-Protection:"0", Cache-Control:"no-cache, no-store, max-age=0, must-revalidate", Pragma:"no-cache", Expires:"0", X-Frame-Options:"DENY"]
     Content type = null
             Body = 
    Forwarded URL = null
   Redirected URL = null
          Cookies = []

分析与解决

1. 401错误的根源

从401日志里的WWW-Authenticate:"Basic realm="Realm""能看出,测试触发了默认Basic认证,而非你配置的OAuth2 Resource Server逻辑。原因是@WebMvcTest默认只加载控制器和Spring MVC核心组件,你的SecurityFilterChain、jwtCookieFilter等自定义安全Bean没被加载到测试上下文。

Spring Security在找不到自定义SecurityFilterChain时,会启用默认安全规则——要求所有请求走Basic认证,这就是配置了permitAll仍返回401的原因。

解决方法

给测试类添加@Import注解,导入你的安全配置类(即包含securityFilterChain方法的类),让测试上下文加载自定义安全规则:

@WebMvcTest(AuthenticationController.class)
@Import(SecurityConfig.class) // 替换为你的安全配置类实际名称
class AuthenticationControllerTest {
    // ... 原有代码
}

如果jwtCookieFilter有依赖Bean(比如JWT解析工具),用@MockBean模拟这些依赖,避免测试时因缺少Bean报错。

2. 添加@WithMockUser后响应体为空

这是因为你模拟的authenticationService.register返回的是Mockito mock的User对象,控制器序列化UserDto时出现了问题:要么UserDto没有可被Jackson访问的属性,要么mock逻辑没有正确返回可序列化的UserDto实例。

解决方法

  • 方法一:返回真实对象替代mock
    直接让register方法返回真实的User实例,确保其getDto()方法返回可序列化的UserDto:
    UserDto userDto = new UserDto(username, email);
    // 假设User类有对应的构造方法和正确的getDto实现
    Mockito.when(authenticationService.register(any(), any(), any(), any())).thenReturn(new User(username, password, email));
    
  • 方法二:确保mock的UserDto可被序列化
    检查UserDto类是否有public getter方法,或添加Lombok的@Data注解,保证Jackson能正确读取属性:
    // UserDto类示例
    @Data
    public class UserDto {
        private String username;
        private String email;
    
        public UserDto(String username, String email) {
            this.username = username;
            this.email = email;
        }
    }
    

3. 额外优化点

  • 测试里的.with(csrf())可以删除,因为你的安全配置已经禁用了CSRF(csrf -> csrf.disable()),添加后属于冗余操作。
  • 精准匹配register方法的参数:比如when(authenticationService.register(eq(username), eq(password), eq(email), eq("learner"))),避免因参数不匹配导致mock逻辑不生效。

内容的提问来源于stack exchange,提问作者phuc luu

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 01:12:03