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

如何为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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.02 15:20:22