在 Spring Boot 中实现 CSRF 防护有多种方式,Spring Security 提供了内置的 CSRF 保护机制。
Spring Security CSRF 保护概述
Spring Security 默认启用 CSRF 保护,它通过以下方式工作:
- 生成 CSRF Token
- 将 Token 存储在服务器会话中
- 在表单中自动添加 Token
- 验证请求中的 Token
配置方式
1. 使用默认配置(推荐)
Spring Security 默认启用 CSRF 保护,无需额外配置。
java@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers("/public/**").permitAll() .anyRequest().authenticated() .and() .formLogin() .loginPage("/login") .permitAll(); } }
2. 自定义 CSRF Token 存储
默认使用 HttpSessionCsrfTokenRepository,可以自定义存储方式。
使用 Cookie 存储
java@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .and() .authorizeRequests() .anyRequest().authenticated(); } }
自定义 Token Repository
javapublic class CustomCsrfTokenRepository implements CsrfTokenRepository { @Override public CsrfToken generateToken(HttpServletRequest request) { String token = UUID.randomUUID().toString(); return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { // 自定义存储逻辑 request.getSession().setAttribute("_csrf", token); } @Override public CsrfToken loadToken(HttpServletRequest request) { // 自定义加载逻辑 return (CsrfToken) request.getSession().getAttribute("_csrf"); } } @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(new CustomCsrfTokenRepository()) .and() .authorizeRequests() .anyRequest().authenticated(); } }
3. 禁用 CSRF 保护(不推荐)
某些情况下可能需要禁用 CSRF 保护(如 REST API)。
java@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .disable() .and() .authorizeRequests() .anyRequest().authenticated(); } }
4. 部分禁用 CSRF 保护
只为特定路径禁用 CSRF 保护。
java@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .ignoringAntMatchers("/api/**", "/public/**") .and() .authorizeRequests() .antMatchers("/api/**").permitAll() .anyRequest().authenticated(); } }
前端集成
1. Thymeleaf 模板
Spring Security 自动在 Thymeleaf 模板中添加 CSRF Token。
html<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Login</title> </head> <body> <form th:action="@{/login}" method="post"> <!-- CSRF Token 自动添加 --> <input type="text" name="username" placeholder="Username"> <input type="password" name="password" placeholder="Password"> <button type="submit">Login</button> </form> </body> </html>
2. JSP 模板
jsp<%@ taglib prefix="sec" uri="http://www.springframework.org/security/tags" %> <form action="/login" method="post"> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}"/> <input type="text" name="username" placeholder="Username"> <input type="password" name="password" placeholder="Password"> <button type="submit">Login</button> </form>
3. AJAX 请求
javascript// 获取 CSRF Token function getCsrfToken() { const metaTag = document.querySelector('meta[name="_csrf"]'); const headerName = document.querySelector('meta[name="_csrf_header"]'); return { token: metaTag ? metaTag.getAttribute('content') : '', headerName: headerName ? headerName.getAttribute('content') : 'X-CSRF-TOKEN' }; } // 发送 AJAX 请求 function sendAjaxRequest(url, data) { const { token, headerName } = getCsrfToken(); return fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', [headerName]: token }, body: JSON.stringify(data) }); } // 使用示例 sendAjaxRequest('/api/data', { name: 'John' }) .then(response => response.json()) .then(data => console.log(data));
4. 在 HTML 中添加 Meta 标签
html<!DOCTYPE html> <html> <head> <meta name="_csrf" th:content="${_csrf.token}"/> <meta name="_csrf_header" th:content="${_csrf.headerName}"/> <title>My App</title> </head> <body> <!-- 页面内容 --> </body> </html>
高级配置
1. 自定义 CSRF Token 生成器
java@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenRepository(csrfTokenRepository()) .and() .authorizeRequests() .anyRequest().authenticated(); } @Bean public CsrfTokenRepository csrfTokenRepository() { HttpSessionCsrfTokenRepository repository = new HttpSessionCsrfTokenRepository(); repository.setHeaderName("X-CSRF-TOKEN"); repository.setParameterName("_csrf"); return repository; } }
2. 自定义 CSRF Token 验证器
javapublic class CustomCsrfTokenValidator implements CsrfTokenValidator { @Override public boolean validateToken(HttpServletRequest request, CsrfToken token) { // 自定义验证逻辑 String requestToken = request.getHeader(token.getHeaderName()); return token.getToken().equals(requestToken); } } @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { http .csrf() .csrfTokenValidator(new CustomCsrfTokenValidator()) .and() .authorizeRequests() .anyRequest().authenticated(); } }
3. 配置 SameSite Cookie
java@Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public CookieSerializer cookieSerializer() { DefaultCookieSerializer serializer = new DefaultCookieSerializer(); serializer.setCookieName("SESSION"); serializer.setCookiePath("/"); serializer.setDomainNamePattern("^.+?\\.(\\w+\\.[a-z]+)$"); serializer.setSameSite("Lax"); serializer.setUseHttpOnlyCookie(true); serializer.setUseSecureCookie(true); return serializer; } }
测试 CSRF 保护
1. 单元测试
java@RunWith(SpringRunner.class) @SpringBootTest @AutoConfigureMockMvc public class CsrfProtectionTest { @Autowired private MockMvc mockMvc; @Test public void testCsrfProtection() throws Exception { mockMvc.perform(post("/transfer") .contentType(MediaType.APPLICATION_JSON) .content("{\"amount\":100}")) .andExpect(status().isForbidden()); } @Test public void testWithValidCsrfToken() throws Exception { MvcResult result = mockMvc.perform(get("/csrf")) .andReturn(); String csrfToken = result.getResponse().getContentAsString(); mockMvc.perform(post("/transfer") .contentType(MediaType.APPLICATION_JSON) .header("X-CSRF-TOKEN", csrfToken) .content("{\"amount\":100}")) .andExpect(status().isOk()); } }
2. 集成测试
java@RunWith(SpringRunner.class) @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureMockMvc public class CsrfIntegrationTest { @Autowired private TestRestTemplate restTemplate; @Test public void testCsrfWithRestTemplate() { HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_JSON); // 没有 CSRF Token HttpEntity<String> request = new HttpEntity<>("{\"amount\":100}", headers); ResponseEntity<String> response = restTemplate.postForEntity("/transfer", request, String.class); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.FORBIDDEN); } }
常见问题
1. AJAX 请求 403 错误
原因:缺少 CSRF Token 解决:在请求头中添加 CSRF Token
javascriptfetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': getCsrfToken() }, body: JSON.stringify(data) });
2. 多标签页 Token 失效
原因:会话过期或 Token 不匹配 解决:确保所有标签页使用同一个会话
3. 文件上传失败
原因:文件上传无法使用表单 Token 解决:使用请求头或预签名 URL
最佳实践
- 使用默认配置:Spring Security 默认配置已经足够安全
- 使用 Cookie 存储 Token:便于前端获取
- 为 AJAX 请求添加 Token:确保所有请求都包含 Token
- 定期更新 Token:降低 Token 泄露风险
- 配合其他防护措施:如 SameSite Cookie、Origin 验证
- 测试 CSRF 保护:确保防护机制正常工作
总结
Spring Boot 通过 Spring Security 提供了完善的 CSRF 保护机制。默认配置已经足够安全,可以根据需要自定义 Token 存储方式和验证逻辑。前端需要确保所有请求都包含有效的 CSRF Token,特别是 AJAX 请求。配合其他防护措施可以构建更强大的安全防护体系。