Spring Boot
Spring Boot 是一个开源的 Java 基础框架,旨在简化 Spring 应用的创建和开发过程。它由 Pivotal 团队(现为 VMware)开发,是 Spring 平台和第三方库的集成,提供了一个快速且广泛接受的方式来构建 Spring 应用。Spring Boot 使得设置和配置 Spring 应用变得简单,主要通过约定优于配置的原则,减少了项目的样板代码。

在 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