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

Spring Boot

Spring Boot 是一个开源的 Java 基础框架,旨在简化 Spring 应用的创建和开发过程。它由 Pivotal 团队(现为 VMware)开发,是 Spring 平台和第三方库的集成,提供了一个快速且广泛接受的方式来构建 Spring 应用。Spring Boot 使得设置和配置 Spring 应用变得简单,主要通过约定优于配置的原则,减少了项目的样板代码。
Spring Boot
在 Spring Boot 中如何实现 CSRF 防护?在 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 ```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(); } } ``` ### 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 ```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 请求。配合其他防护措施可以构建更强大的安全防护体系。
服务端 · 2月19日 17:52