本文目录导读:

我来为你详细介绍Java CSRF防护的完整案例开发。
什么是CSRF攻击
CSRF(Cross-Site Request Forgery)跨站请求伪造,攻击者诱导用户点击链接,在用户不知情的情况下执行恶意操作。
完整的CSRF防护实现案例
1 项目结构
src/main/java/com/example/csrf/
├── config/
│ └── SecurityConfig.java
├── controller/
│ └── TransferController.java
├── util/
│ └── CSRFTokenUtil.java
├── filter/
│ └── CSRFValidationFilter.java
└── Application.java
2 核心工具类 - Token生成与管理
package com.example.csrf.util;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
public class CSRFTokenUtil {
private static final String CSRF_TOKEN_KEY = "csrf_token";
private static final SecureRandom random = new SecureRandom();
/**
* 生成CSRF Token
*/
public static String generateToken() {
byte[] tokenBytes = new byte[32];
random.nextBytes(tokenBytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes);
}
/**
* 存储Token到Session
*/
public static void storeToken(HttpSession session, String token) {
Map<String, String> tokenMap = (Map<String, String>) session.getAttribute(CSRF_TOKEN_KEY);
if (tokenMap == null) {
tokenMap = new HashMap<>();
session.setAttribute(CSRF_TOKEN_KEY, tokenMap);
}
// 存储在session中,key可以是token哈希值或唯一标识
tokenMap.put(token, token);
}
/**
* 验证Token
*/
public static boolean validateToken(HttpServletRequest request, String token) {
HttpSession session = request.getSession(false);
if (session == null) {
return false;
}
Map<String, String> tokenMap = (Map<String, String>) session.getAttribute(CSRF_TOKEN_KEY);
if (tokenMap == null || !tokenMap.containsKey(token)) {
return false;
}
// 验证成功后移除已使用的token(一次性)
tokenMap.remove(token);
return true;
}
/**
* 从请求中获取Token
*/
public static String getTokenFromRequest(HttpServletRequest request) {
// 先从Header获取
String token = request.getHeader("X-CSRF-Token");
if (token == null || token.isEmpty()) {
// 再从请求参数获取
token = request.getParameter("_csrf");
}
return token;
}
}
3 CSRF过滤器
package com.example.csrf.filter;
import com.example.csrf.util.CSRFTokenUtil;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Set;
public class CSRFValidationFilter implements Filter {
// 排除CSRF验证的路径
private static final Set<String> EXCLUDED_PATHS = new HashSet<>(
Arrays.asList("/login", "/logout", "/api/public", "/error")
);
// 需要CSRF保护的HTTP方法
private static final Set<String> PROTECTED_METHODS = new HashSet<>(
Arrays.asList("POST", "PUT", "DELETE", "PATCH")
);
@Override
public void init(FilterConfig filterConfig) throws ServletException {
// 初始化操作
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse,
FilterChain filterChain) throws IOException, ServletException {
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
String requestURI = request.getRequestURI();
String method = request.getMethod();
// 检查是否需要CSRF保护
if (shouldValidateCSRF(requestURI, method)) {
String token = CSRFTokenUtil.getTokenFromRequest(request);
if (token == null || !CSRFTokenUtil.validateToken(request, token)) {
// Token验证失败
response.setStatus(HttpServletResponse.SC_FORBIDDEN);
response.setContentType("application/json");
response.getWriter().write("{\"error\":\"CSRF token validation failed\"}");
return;
}
}
filterChain.doFilter(request, response);
}
private boolean shouldValidateCSRF(String requestURI, String method) {
// 排除特定路径
for (String excludedPath : EXCLUDED_PATHS) {
if (requestURI.startsWith(excludedPath)) {
return false;
}
}
// 仅对修改操作进行验证
return PROTECTED_METHODS.contains(method.toUpperCase());
}
@Override
public void destroy() {
// 清理操作
}
}
4 安全配置
package com.example.csrf.config;
import com.example.csrf.filter.CSRFValidationFilter;
import com.example.csrf.util.CSRFTokenUtil;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Configuration
public class SecurityConfig implements WebMvcConfigurer {
/**
* 注册CSRF过滤器
*/
@Bean
public FilterRegistrationBean<CSRFValidationFilter> csrfFilterRegistration() {
FilterRegistrationBean<CSRFValidationFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new CSRFValidationFilter());
registration.addUrlPatterns("/*");
registration.setName("csrfValidationFilter");
registration.setOrder(1);
return registration;
}
/**
* 生成并注入CSRF Token到响应
*/
public static void injectCSRFToken(HttpServletRequest request, HttpServletResponse response) {
HttpSession session = request.getSession();
String token = CSRFTokenUtil.generateToken();
CSRFTokenUtil.storeToken(session, token);
// 在响应头中添加Token
response.setHeader("X-CSRF-Token", token);
// 也可以在Cookie中设置(但需要注意Cookie的安全性)
// Cookie cookie = new Cookie("csrf_token", token);
// cookie.setHttpOnly(true);
// cookie.setSecure(true);
// response.addCookie(cookie);
}
}
5 控制器示例
package com.example.csrf.controller;
import com.example.csrf.config.SecurityConfig;
import com.example.csrf.util.CSRFTokenUtil;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Controller
public class TransferController {
/**
* 转账页面 - 生成并传递CSRF Token
*/
@GetMapping("/transfer")
public String transferPage(HttpServletRequest request,
HttpServletResponse response,
Model model) {
// 生成并注入CSRF Token
SecurityConfig.injectCSRFToken(request, response);
// 从session获取当前token
String token = (String) request.getSession().getAttribute("csrf_token");
model.addAttribute("csrfToken", token);
return "transfer";
}
/**
* 转账操作 - 需要CSRF Token验证
*/
@PostMapping("/transfer")
@ResponseBody
public String processTransfer(@RequestParam String amount,
@RequestParam String toAccount,
HttpServletRequest request) {
// 实际业务逻辑
return "Transfer completed successfully";
}
/**
* 获取新的CSRF Token(用于AJAX请求)
*/
@GetMapping("/api/csrf-token")
@ResponseBody
public String getCsrfToken(HttpServletRequest request,
HttpServletResponse response) {
SecurityConfig.injectCSRFToken(request, response);
String token = (String) request.getSession().getAttribute("csrf_token");
return token;
}
}
6 前端HTML模板
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">转账页面</title>
<script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
<h2>转账</h2>
<!-- 表单方式 -->
<form id="transferForm" action="/transfer" method="POST">
<input type="hidden" name="_csrf" th:value="${csrfToken}" />
<div>
<label>金额:</label>
<input type="text" name="amount" required />
</div>
<div>
<label>收款账户:</label>
<input type="text" name="toAccount" required />
</div>
<button type="submit">提交转账</button>
</form>
<!-- AJAX方式 -->
<button onclick="submitTransfer()">AJAX转账</button>
<script>
// 设置全局AJAX CSRF Token
$(document).ready(function() {
var csrfToken = $('input[name="_csrf"]').val();
$(document).ajaxSend(function(e, xhr, options) {
if (options.type.toUpperCase() === 'POST' ||
options.type.toUpperCase() === 'PUT' ||
options.type.toUpperCase() === 'DELETE') {
xhr.setRequestHeader('X-CSRF-Token', csrfToken);
}
});
});
function submitTransfer() {
$.ajax({
url: '/transfer',
type: 'POST',
data: {
amount: 1000,
toAccount: '123456789'
},
success: function(response) {
alert('转账成功');
// 刷新Token
refreshCSRFToken();
},
error: function(xhr) {
if (xhr.status === 403) {
alert('CSRF验证失败');
refreshCSRFToken();
}
}
});
}
function refreshCSRFToken() {
$.get('/api/csrf-token', function(token) {
$('input[name="_csrf"]').val(token);
});
}
</script>
</body>
</html>
7 Spring Boot入口
package com.example.csrf;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
使用Spring Security内置CSRF防护
如果使用Spring Security,可以直接使用其内置的CSRF防护:
package com.example.csrf.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
public class SpringSecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.ignoringRequestMatchers("/api/public/**")
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
)
.authorizeHttpRequests(authz -> authz
.requestMatchers("/login", "/logout", "/api/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
return http.build();
}
}
最佳实践建议
1 Token安全性
- 使用强随机数生成器
- Token应具有足够长度(至少32字节)
- 限制Token有效期
- 每次使用后刷新Token
2 Cookie安全配置
Cookie cookie = new Cookie("csrf_token", token);
cookie.setHttpOnly(true); // 防止XSS读取
cookie.setSecure(true); // 仅HTTPS传输
cookie.setSameSite("Strict"); // 防止跨站请求
cookie.setPath("/");
cookie.setMaxAge(3600); // 1小时过期
3 双重验证
// 验证Referer和Origin头部
public boolean validateReferer(HttpServletRequest request) {
String referer = request.getHeader("Referer");
String origin = request.getHeader("Origin");
if (referer != null && !referer.startsWith("https://yourdomain.com")) {
return false;
}
if (origin != null && !origin.equals("https://yourdomain.com")) {
return false;
}
return true;
}
4 日志记录
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CSRFValidationFilter extends OncePerRequestFilter {
private static final Logger logger = LoggerFactory.getLogger(CSRFValidationFilter.class);
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
try {
// CSRF验证逻辑
if (!validateCSRF(request)) {
logger.warn("CSRF validation failed for request: {} from IP: {}",
request.getRequestURI(),
request.getRemoteAddr());
response.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF validation failed");
return;
}
filterChain.doFilter(request, response);
} catch (Exception e) {
logger.error("CSRF filter error", e);
}
}
}
测试CSRF防护
package com.example.csrf;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
@SpringBootTest
@AutoConfigureMockMvc
public class CSRFProtectionTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testTransferWithoutCSRFToken() throws Exception {
// 测试无CSRF Token的请求应被拒绝
mockMvc.perform(post("/transfer")
.param("amount", "1000")
.param("toAccount", "123456"))
.andExpect(status().isForbidden());
}
@Test
public void testTransferWithInvalidCSRFToken() throws Exception {
// 测试无效CSRF Token应被拒绝
mockMvc.perform(post("/transfer")
.header("X-CSRF-Token", "invalid-token")
.param("amount", "1000")
.param("toAccount", "123456"))
.andExpect(status().isForbidden());
}
@Test
public void testTransferWithValidCSRFToken() throws Exception {
// 测试有效CSRF Token应成功
// 这里需要先获取有效的Token
String token = fetchCSRFToken();
mockMvc.perform(post("/transfer")
.header("X-CSRF-Token", token)
.param("amount", "1000")
.param("toAccount", "123456"))
.andExpect(status().isOk());
}
private String fetchCSRFToken() throws Exception {
// 从/token接口获取有效的CSRF Token
var result = mockMvc.perform(get("/api/csrf-token"))
.andExpect(status().isOk())
.andReturn();
return result.getResponse().getContentAsString();
}
}
这个完整的案例涵盖了CSRF防护的主要实现方式,包括自定义实现和Spring Security集成方案,建议根据项目需求选择合适的实现方式。