安全测试
定义与作用
Spring Security 测试支持是一套专门为安全过滤器链设计的集成测试框架。它允许开发者在不启动完整嵌入式 Servlet 容器的情况下,验证 SecurityFilterChain 中各过滤器的协作行为——包括认证拦截、授权决策、CSRF 校验、Session 管理及安全响应头的写入。核心注解 @AutoConfigureMockMvc 负责将 Spring Security 的完整过滤器链注入到 MockMvc 中,使得测试请求能够像真实请求一样依次经过 DelegatingFilterProxy → FilterChainProxy → 各安全过滤器,最终到达目标控制器。
该测试框架的价值在于:安全规则属于架构约束,必须用自动化测试锁定。一旦 HttpSecurity 配置发生变更,对应的测试必须立即暴露回归缺陷,而非等到生产环境被渗透测试发现。
核心原理
MockMvc 本身是一个基于 DispatcherServlet 的轻量级测试门面,默认只包含 Spring MVC 的过滤器。当测试类上标注 @AutoConfigureMockMvc 时,Spring Boot 测试自动配置会检索容器中名为 springSecurityFilterChain 的 FilterChainProxy Bean,并将其作为首个 FilterRegistrationBean 注册到 MockMvc 的过滤器链中。此后,所有通过 mockMvc.perform() 发起的模拟请求都会按以下顺序执行:
MockMvc构建模拟的HttpServletRequest与HttpServletResponse;DelegatingFilterProxy将请求委托给FilterChainProxy;FilterChainProxy遍历所有SecurityFilterChain,匹配第一个适用的链;- 链内过滤器按固定顺序执行:
SecurityContextPersistenceFilter→CsrfFilter→UsernamePasswordAuthenticationFilter→ExceptionTranslationFilter→FilterSecurityInterceptor等; - 若认证与授权均通过,请求进入
DispatcherServlet并到达 Controller; - 响应反向经过各过滤器的后置处理,最终返回
ResultActions供断言。
由于过滤器链完整参与,测试中可以断言 HTTP 状态码、重定向地址、响应头、SecurityContext 中的 Authentication 对象,甚至验证 CsrfFilter 是否因 Token 缺失而拒绝 POST 请求。
测试流程图
示例一:未认证请求被拦截与登录后放行
场景说明
应用存在 /admin/dashboard 页面,要求用户必须已认证且拥有 ADMIN 角色。测试目标是:验证未认证用户被重定向到登录页,并验证登录后带正确角色的用户可以访问。
操作前配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.antMatchers("/admin/**").hasRole("ADMIN")
.antMatchers("/login").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.defaultSuccessUrl("/admin/dashboard")
.permitAll()
)
.csrf().disable(); // 为简化测试示例先禁用,生产环境不应如此
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("{noop}admin123")
.roles("ADMIN");
}
}
操作后配置(测试类)
@SpringBootTest
@AutoConfigureMockMvc
public class AdminDashboardSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
public void 未认证用户访问管理后台应被重定向到登录页() throws Exception {
mockMvc.perform(get("/admin/dashboard"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("**/login"));
}
@Test
public void 已认证且拥有ADMIN角色的用户应能访问管理后台() throws Exception {
mockMvc.perform(get("/admin/dashboard")
.with(user("admin").roles("ADMIN")))
.andExpect(status().isOk())
.andExpect(content().string(containsString("Dashboard")));
}
}
结果分析
- 第一个测试执行时,
MockMvc发送的 GET 请求到达FilterSecurityInterceptor,由于SecurityContextHolder中不存在Authentication,ExceptionTranslationFilter捕获AccessDeniedException并调用AuthenticationEntryPoint,默认行为为重定向到/login,断言is3xxRedirection()通过。 - 第二个测试通过
MockMvcRequestPostProcessors.user()构建了一个已认证的UsernamePasswordAuthenticationToken并注入SecurityContext。FilterSecurityInterceptor检查Authentication中的ROLE_ADMIN后放行,Controller 正常返回 200。
示例二:POST 请求 CSRF 校验缺失导致 403
场景说明
管理后台提供用户删除接口 POST /admin/users/delete,要求提交表单时必须携带有效的 CSRF Token。测试目标是验证:未携带 CSRF Token 的请求被拒绝,携带正确 Token 的请求成功处理。
操作前配置
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests(auth -> auth
.antMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
)
.csrf(); // 显式启用 CSRF 防护(默认即为开启)
}
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.inMemoryAuthentication()
.withUser("admin")
.password("{noop}admin123")
.roles("ADMIN");
}
}
操作后配置(测试类)
@SpringBootTest
@AutoConfigureMockMvc
public class AdminDeleteUserSecurityTest {
@Autowired
private MockMvc mockMvc;
@Test
public void 未携带CSRF_TOKEN的POST请求应返回403() throws Exception {
mockMvc.perform(post("/admin/users/delete")
.param("userId", "1001")
.with(user("admin").roles("ADMIN")))
.andExpect(status().isForbidden());
}
@Test
public void 携带CSRF_TOKEN的POST请求应成功处理() throws Exception {
mockMvc.perform(post("/admin/users/delete")
.param("userId", "1001")
.with(user("admin").roles("ADMIN"))
.with(csrf())) // 通过 csrf() 处理器自动注入有效 Token
.andExpect(status().isOk())
.andExpect(content().string(containsString("删除成功")));
}
}
结果分析
CsrfFilter默认拦截所有 "state-changing" 请求(POST / PUT / DELETE / PATCH)。第一个测试未调用.with(csrf()),请求中既无_csrf参数也无X-CSRF-TOKEN请求头,CsrfFilter比对失败,直接返回 403 Forbidden。- 第二个测试使用
MockMvcRequestPostProcessors.csrf()处理器,该处理器从HttpSessionCsrfTokenRepository中获取或生成 Token,并将其同时写入 Session 和请求参数。CsrfFilter比对通过,请求继续向下到达FilterSecurityInterceptor,授权通过后进入 Controller 返回 200。 - 关键结论:在 Spring Security 5.x 中,
csrf()默认开启,任何 POST 测试若未显式处理 CSRF,都会收到 403。这是 MockMvc 安全测试中最常见的失败原因。
易错场景:@AutoConfigureMockMvc 遗漏导致安全过滤器未生效
现象
开发者编写了如下测试,但断言失败——未认证用户居然返回了 200 而不是 302:
@SpringBootTest
public class WrongSecurityTest { // 缺少 @AutoConfigureMockMvc
@Autowired
private MockMvc mockMvc;
@Test
public void 未认证访问受保护资源应被重定向() throws Exception {
mockMvc.perform(get("/admin/dashboard"))
.andExpect(status().is3xxRedirection()); // 实际返回 200,断言失败
}
}
根因分析
@AutoConfigureMockMvc 的作用不仅仅是创建 MockMvc 实例,更重要的是将 Spring Security 的 FilterChainProxy 注册到 MockMvc 的过滤器链中。缺少该注解时,MockMvc 内部只有 Spring MVC 的默认过滤器(如 CharacterEncodingFilter),springSecurityFilterChain 根本不会参与请求处理。请求未经 FilterSecurityInterceptor 拦截,直接到达 Controller,因此返回 200。
正确做法
测试类必须显式标注 @AutoConfigureMockMvc。如果开发者需要进一步排除某些自动配置(例如排除 OAuth2 客户端配置),应使用 excludeAutoConfiguration 属性,但绝不能省略该注解本身。
@SpringBootTest
@AutoConfigureMockMvc
public class CorrectSecurityTest {
// 安全过滤器链正常注入,测试断言通过
}
面试考点
Q:MockMvc 安全测试与真实启动 Servlet 容器的测试相比,有何局限?
MockMvc 测试基于
DispatcherServlet的轻量级模拟,不经过真正的嵌入式 Tomcat/Jetty。因此它无法验证:① 容器层自定义 Filter(如 web.xml 中手动注册的 Filter)的交互;② HTTPS 强制跳转(HttpSecurity.requiresChannel()在 MockMvc 中无法真正建立 TLS 连接);③ 分布式 Session(如 Spring Session Redis)的跨实例共享。对于纯 Spring Security 过滤器链逻辑的验证,MockMvc 是最高效的方式;对于容器层或网络层的验证,仍需使用@SpringBootTest(webEnvironment = RANDOM_PORT)配合TestRestTemplate或WebTestClient。
Q:为什么测试中的 user() 处理器与 @WithMockUser 注解都能模拟用户,两者区别是什么?
MockMvcRequestPostProcessors.user()是请求级别的处理器,仅作用于当前mockMvc.perform()请求的SecurityContext,粒度细到单个 HTTP 调用;@WithMockUser是测试方法级别的注解,由WithSecurityContextTestExecutionListener在测试方法执行前将Authentication设置到SecurityContextHolder,在整个测试方法期间都有效(包括被测方法内部可能触发的异步任务或子线程,但子线程继承需额外配置)。在@WebMvcTest切片测试中,推荐user()处理器;在需要方法级别统一身份的集成测试中,推荐@WithMockUser。
版本说明:本教程基于 Spring Security 5.x(对应 Spring Boot 2.x)。
WebSecurityConfigurerAdapter为 5.x 的经典配置基类,测试注解与MockMvc安全处理器的 API 在 5.x 与 6.x 中保持兼容。