乐闻世界logo
搜索文章和话题

在 Spring Boot 中如何实现 CSRF 防护?

2月19日 17:52

在 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,可以自定义存储方式。

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

java
public 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 验证器

java
public 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(); } }
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

javascript
fetch('/api/data', { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-CSRF-TOKEN': getCsrfToken() }, body: JSON.stringify(data) });

2. 多标签页 Token 失效

原因:会话过期或 Token 不匹配 解决:确保所有标签页使用同一个会话

3. 文件上传失败

原因:文件上传无法使用表单 Token 解决:使用请求头或预签名 URL

最佳实践

  1. 使用默认配置:Spring Security 默认配置已经足够安全
  2. 使用 Cookie 存储 Token:便于前端获取
  3. 为 AJAX 请求添加 Token:确保所有请求都包含 Token
  4. 定期更新 Token:降低 Token 泄露风险
  5. 配合其他防护措施:如 SameSite Cookie、Origin 验证
  6. 测试 CSRF 保护:确保防护机制正常工作

总结

Spring Boot 通过 Spring Security 提供了完善的 CSRF 保护机制。默认配置已经足够安全,可以根据需要自定义 Token 存储方式和验证逻辑。前端需要确保所有请求都包含有效的 CSRF Token,特别是 AJAX 请求。配合其他防护措施可以构建更强大的安全防护体系。

标签:CSRFSpring Boot