如何为Spring Security的SecurityConfig编写单元测试?
关于Spring Security配置的测试策略
要不要写单元测试?
没有绝对的必须,得看你的场景:
- 如果安全规则逻辑简单(比如只有几个路径匹配、基础权限配置),集成测试完全足够——毕竟集成测试能真实模拟请求、Token验证的全流程,覆盖端到端的安全逻辑。
- 如果安全规则复杂(比如大量路径匹配、自定义权限逻辑、多个Validator组合),单元测试能快速验证配置细节,比如路径写错、权限字符串拼写错误这类低级问题,不用每次跑全量集成测试,节省时间。
Stack Overflow上的“无需单元测试”建议,本质是说这类配置的核心逻辑最终要靠集成测试验证,但单元测试可以作为补充,提前拦截小问题。
如果要写,怎么实现?
Spring Security提供了专门的测试支持,主要分两类场景:
1. 测试端点的权限控制(最实用)
用@WebMvcTest配合Spring Security的测试工具,模拟不同身份的请求,验证端点的访问规则是否符合预期。
示例代码:
@WebMvcTest @Import(SecurityConfig.class) class SecurityConfigMvcTest { @Autowired private MockMvc mockMvc; // 测试GET /data/actuator/** 允许匿名访问 @Test void actuatorEndpoint_AllowsAnonymous() throws Exception { mockMvc.perform(get("/data/actuator/health")) .andExpect(status().isOk()); } // 测试PUT /data/** 没有正确权限时返回403 @Test void dataPutEndpoint_DeniesWithoutWriteScope() throws Exception { mockMvc.perform(put("/data/user/1") .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_data:read")))) .andExpect(status().isForbidden()); } // 测试PUT /data/** 有正确权限时允许访问 @Test void dataPutEndpoint_AllowsWithWriteScope() throws Exception { mockMvc.perform(put("/data/user/1") .with(jwt().authorities(new SimpleGrantedAuthority("SCOPE_data:write")))) .andExpect(status().isOk()); // 可根据实际控制器返回调整断言 } // 测试其他端点需要认证,匿名访问返回401 @Test void otherEndpoints_RequiresAuthentication() throws Exception { mockMvc.perform(get("/data/user/1")) .andExpect(status().isUnauthorized()); } }
2. 测试JwtDecoder的验证逻辑
针对你自定义的JwtDecoder配置,验证Token的issuer、audience是否被正确校验:
@ExtendWith(MockitoExtension.class) class SecurityConfigDecoderTest { @Mock private NimbusJwtDecoder nimbusJwtDecoder; @InjectMocks private SecurityConfig securityConfig; private final String testAudience = "test-audience"; private final String testIssuer = "https://test-issuer.com/"; @Test void jwtDecoder_ValidatesAudienceAndIssuer() { try (MockedStatic<JwtDecoders> mockedDecoders = Mockito.mockStatic(JwtDecoders.class)) { mockedDecoders.when(() -> JwtDecoders.fromOidcIssuerLocation(testIssuer)) .thenReturn(nimbusJwtDecoder); JwtDecoder decoder = securityConfig.jwtDecoder(); // 验证decoder被设置了正确的Validator组合 ArgumentCaptor<OAuth2TokenValidator<Jwt>> validatorCaptor = ArgumentCaptor.forClass(OAuth2TokenValidator.class); verify(nimbusJwtDecoder).setJwtValidator(validatorCaptor.capture()); OAuth2TokenValidator<Jwt> validator = validatorCaptor.getValue(); assertTrue(validator instanceof DelegatingOAuth2TokenValidator); DelegatingOAuth2TokenValidator<Jwt> delegatingValidator = (DelegatingOAuth2TokenValidator<Jwt>) validator; List<OAuth2TokenValidator<Jwt>> validators = delegatingValidator.getValidators(); assertTrue(validators.stream().anyMatch(v -> v instanceof JwtIssuerValidator)); assertTrue(validators.stream().anyMatch(v -> v instanceof AudienceValidator)); } } // 单独测试自定义的AudienceValidator @Test void audienceValidator_RejectsInvalidAudience() { AudienceValidator validator = new AudienceValidator(testAudience); Jwt invalidJwt = Jwt.withTokenValue("test-token") .header("alg", "HS256") .claim("aud", "wrong-audience") .build(); OAuth2TokenValidatorResult result = validator.validate(invalidJwt); assertTrue(result.hasErrors()); // 根据你的Validator实现调整断言 } }
3. 直接测试SecurityFilterChain的配置
如果想验证HttpSecurity的配置细节(比如是否禁用CSRF、是否启用CORS),可以直接实例化SecurityConfig,调用filterChain方法:
@ExtendWith(MockitoExtension.class) class SecurityConfigFilterChainTest { @InjectMocks private SecurityConfig securityConfig; @Test void filterChain_ConfiguresCorrectRules() throws Exception { HttpSecurity http = Mockito.mock(HttpSecurity.class); AuthorizedUrl authorizedUrl = Mockito.mock(AuthorizedUrl.class); ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry interceptRegistry = Mockito.mock(ExpressionUrlAuthorizationConfigurer.ExpressionInterceptUrlRegistry.class); // 模拟链式调用 when(http.csrf()).thenReturn(Mockito.mock(CsrfConfigurer.class)); when(http.csrf().disable()).thenReturn(http); when(http.authorizeRequests()).thenReturn(interceptRegistry); when(interceptRegistry.antMatchers(HttpMethod.GET, "/data/actuator/**")).thenReturn(authorizedUrl); when(authorizedUrl.permitAll()).thenReturn(interceptRegistry); // 继续模拟其他链式调用... SecurityFilterChain chain = securityConfig.filterChain(http); // 验证配置是否被正确调用 verify(http.csrf()).disable(); verify(interceptRegistry).antMatchers(HttpMethod.GET, "/data/actuator/**"); verify(authorizedUrl).permitAll(); // 验证其他规则... } }
总结
- 优先用集成测试覆盖端到端的安全流程,这是验证安全配置是否生效的最可靠方式。
- 单元测试作为补充,重点测试容易出错的细节:比如路径匹配是否正确、自定义Validator逻辑、权限字符串是否拼写正确。
- 如果你的配置非常简单,完全可以只依赖集成测试,不用写单元测试。
内容的提问来源于stack exchange,提问作者Jonathan Hagen
相关产品推荐
相关产品推荐

