使用SpaCsrfTokenRequestHandler时MockMvc的csrf() API测试失效问题
问题分析与解决方案
问题核心
使用Spring Boot推荐的SpaCsrfTokenRequestHandler实现双重提交Cookie模式的CSRF防护时,MockMvc的csrf() API无法适配该场景:手动设置xsrf-token Cookie和x-xsrf-token Header的测试能正常通过,但调用with(csrf().asHeader())的两种写法均触发InvalidCsrfTokenException。
原因拆解
MockMvc默认的CsrfRequestPostProcessor是为Spring Security传统CSRF配置设计的,和SpaCsrfTokenRequestHandler存在两处关键不匹配:
- 默认使用的Header名称是
X-CSRF-TOKEN,而非SpaCsrfTokenRequestHandler要求的x-xsrf-token - 仅会将生成的CSRF Token放入Header或请求参数,不会同步设置对应的
xsrf-tokenCookie,违背双重提交Cookie模式「Cookie与Header值必须一致」的核心要求
有效测试代码(手动实现)
mockMvc .perform(post("/api/some-endpoint") .cookie(new Cookie("xsrf-token", "abc")) .header("x-xsrf-token", "abc") .contentType(MediaType.APPLICATION_JSON_VALUE) .content("Some body content") .andExpect(status().is2xxSuccessful()) .andReturn();
无效代码及异常
无效代码1
mockMvc .perform(post("/api/some-endpoint") .cookie(new Cookie("xsrf-token", "abc")) .with(csrf().asHeader()) .contentType(MediaType.APPLICATION_JSON_VALUE) .content("Some body content") .andExpect(status().is2xxSuccessful()) .andReturn();
异常信息:
org.springframework.security.web.csrf.InvalidCsrfTokenException: Invalid CSRF Token 'J68ItVNNwwdMhD-qLrdkGN8ox2fAk73IO4md1BNIGvIl-IFlQZY8hGp59GRhtAbOG5pQKe0Z6galoInlX-z-7XJxfsoVy7AD' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.
无效代码2
mockMvc .perform(post("/api/some-endpoint") .with(csrf().asHeader()) .contentType(MediaType.APPLICATION_JSON_VALUE) .content("Some body content") .andExpect(status().is2xxSuccessful()) .andReturn();
异常信息:
org.springframework.security.web.csrf.InvalidCsrfTokenException: Invalid CSRF Token '1SKNwUSrMfPyT9Wp4A1hP8zjY-31exqNdqGvz4avElN50TTQthDp8HOcAZDffu2cgSBVC__UTtTCQnigEJmcquTKczdLtwHg' was found on the request parameter '_csrf' or header 'X-CSRF-TOKEN'.
解决方案
方案1:自定义CSRF请求处理器
编写适配SpaCsrfTokenRequestHandler的MockMvc请求处理器,自动生成匹配的Cookie和Header:
private RequestPostProcessor spaCsrf() { return request -> { String csrfToken = UUID.randomUUID().toString(); // 设置SpaCsrfTokenRequestHandler要求的Cookie request.setCookies(new Cookie("xsrf-token", csrfToken)); // 设置对应的Header request.addHeader("x-xsrf-token", csrfToken); return request; }; }
使用方式:
mockMvc .perform(post("/api/some-endpoint") .with(spaCsrf()) .contentType(MediaType.APPLICATION_JSON_VALUE) .content("Some body content") .andExpect(status().is2xxSuccessful()) .andReturn();
方案2:复用手动配置逻辑并封装
将手动设置Cookie和Header的逻辑封装成工具方法,避免重复代码:
private MockHttpServletRequestBuilder withSpaCsrf(MockHttpServletRequestBuilder builder) { String csrfToken = UUID.randomUUID().toString(); return builder.cookie(new Cookie("xsrf-token", csrfToken)) .header("x-xsrf-token", csrfToken); }
使用方式:
mockMvc .perform(withSpaCsrf(post("/api/some-endpoint")) .contentType(MediaType.APPLICATION_JSON_VALUE) .content("Some body content") .andExpect(status().is2xxSuccessful()) .andReturn();
方案3:全局配置MockMvc适配SpaCsrfTokenRequestHandler
通过自定义CsrfTokenRepository,让MockMvc默认适配SpaCsrf的命名规则:
@Bean public MockMvc mockMvc(WebApplicationContext context) { CsrfTokenRepository csrfTokenRepository = CookieCsrfTokenRepository.withHttpOnlyFalse(); csrfTokenRepository.setCookieName("xsrf-token"); csrfTokenRepository.setHeaderName("x-xsrf-token"); return MockMvcBuilders.webAppContextSetup(context) .apply(SecurityMockMvcConfigurers.springSecurity()) .defaultRequest(get("/").with(csrf().csrfTokenRepository(csrfTokenRepository))) .build(); }
配置完成后,直接使用with(csrf().asHeader())即可自动匹配正确的Cookie和Header名称。
内容的提问来源于stack exchange,提问作者jden
相关产品推荐
相关产品推荐

