CSRF
跨站请求伪造(Cross-Site Request Forgery,CSRF)是一种利用用户已认证身份在目标网站上执行非本意操作的攻击方式。Spring Security 默认通过 CsrfFilter 生成并校验 CSRF Token,是防御此类攻击的核心机制。
定义与作用
一句话定义:攻击者诱导已登录用户在不知情的情况下向目标站点发送恶意请求,浏览器自动携带目标站点 Cookie,使服务器误以为是用户的合法操作。
| 对比项 | 正常请求 | CSRF 攻击请求 |
|---|---|---|
| 发起方 | 用户本人 | 攻击者构造的第三方页面 |
| 用户意图 | 主动操作 | 完全不知情 |
| Cookie 携带 | 是(用户主动) | 是(浏览器自动携带) |
| 服务端视角 | 合法用户 | 无法区分合法性 |
Spring Security 的 CsrfFilter 位于过滤器链第 6 位(在 CorsFilter 之后、LogoutFilter 之前),负责生成、存储和校验 CSRF Token。默认对除 GET / HEAD / TRACE / OPTIONS 外的所有请求进行 Token 校验。
攻击原理与防护流程
核心原理:服务器在渲染表单时生成一个随机 CSRF Token,绑定到当前用户会话;客户端提交写操作时必须携带该 Token;CsrfFilter 比对请求中的 Token 与存储的 Token,不匹配则拒绝请求。由于浏览器的同源策略,第三方域名无法读取目标站点的 Token 值,从而阻断攻击。
核心配置:Token 生成与存储
Spring Security 5.x 默认使用 HttpSessionCsrfTokenRepository(Token 存入 Session),SPA 场景常用 CookieCsrfTokenRepository(Token 存入 Cookie 供前端读取)。
1. HttpSession 存储(默认,传统表单场景)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf() // 默认开启,无需显式调用
.csrfTokenRepository(new HttpSessionCsrfTokenRepository()) // 显式声明(默认即此)
.and()
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll();
}
}
Thymeleaf 表单自动集成:Spring 模板会自动将 Token 注入隐藏字段,无需手动编写。
<form action="/transfer" method="post">
<!-- Thymeleaf 自动渲染 _csrf 隐藏字段 -->
<input type="hidden" name="_csrf" value="d4d4f2a1-..." />
<input type="text" name="to" placeholder="收款人" />
<input type="number" name="amount" placeholder="金额" />
<button type="submit">转账</button>
</form>
2. Cookie 存储(SPA / 前后端分离场景)
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// withHttpOnlyFalse() 使前端 JS 可读取 Cookie 中的 XSRF-TOKEN
.and()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/**").authenticated();
}
}
前端 Axios 自动携带:浏览器对名为 XSRF-TOKEN 的 Cookie 有原生机制,Axios 会自动将其值写入 X-XSRF-TOKEN 请求头提交。
// 前端无需额外配置,Axios 默认读取 XSRF-TOKEN Cookie 并发送 X-XSRF-TOKEN Header
axios.post('/api/transfer', { to: 'alice', amount: 100 });
完整示例一:传统表单登录应用开启 CSRF 防护
场景说明
银行后台管理系统使用 Spring MVC + Thymeleaf 表单页面,用户登录后可在页面上执行转账操作。攻击者构建恶意页面诱导已登录管理员点击,试图在用户不知情的情况下发起转账。
操作前配置(无防护,Spring Security 默认已开启)
@EnableWebSecurity
public class BankSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/transfer").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll()
.and()
.logout()
.logoutSuccessUrl("/login");
// 未显式配置 csrf(),默认已开启 HttpSession 存储
}
}
攻击测试:攻击者页面构造如下:
<!-- evil.com/attack.html -->
<form action="https://bank.com/transfer" method="POST" id="hackForm">
<input type="hidden" name="to" value="hacker" />
<input type="hidden" name="amount" value="10000" />
</form>
<script>document.getElementById('hackForm').submit();</script>
管理员已登录 bank.com,访问 evil.com 后表单自动提交。由于请求未携带 _csrf 参数,CsrfFilter 拦截返回 403 Forbidden,转账失败。
操作后配置(显式自定义 Token 存储与 Header 名)
@EnableWebSecurity
public class BankSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(new HttpSessionCsrfTokenRepository())
.and()
.authorizeRequests()
.antMatchers("/login").permitAll()
.antMatchers("/transfer").hasRole("ADMIN")
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.defaultSuccessUrl("/dashboard")
.permitAll();
}
}
结果分析:CsrfFilter 在管理员访问 /transfer 页面时生成 Token 存入 Session,Thymeleaf 表单渲染 _csrf 隐藏字段。正常提交携带该字段,校验通过;恶意页面无法预知 Token 值,提交即被 403 拒绝。攻击被阻断。
完整示例二:SPA 应用使用 CookieCsrfTokenRepository
场景说明
公司使用 Vue 前端 + Spring Boot 后端分离架构,所有 API 位于 /api/**。前端通过 Cookie 维持 Session,需要让前端能够获取 CSRF Token 并随 AJAX 请求发送。
操作前配置(仅禁用 CSRF,危险状态)
@EnableWebSecurity
public class SpaSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // 错误做法:完全关闭 CSRF 防护
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers("/api/**").authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
}
}
风险分析:SPA 应用虽然主要是 AJAX 调用,但用户登录后 Cookie 仍然有效。攻击者可构造简单表单或自动提交脚本,向 /api/transfer 等写接口发送 POST 请求,由于 CSRF 被禁用,服务端直接执行操作,攻击成功。
操作后配置(正确启用 Cookie 存储)
@EnableWebSecurity
public class SpaSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf()
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
// Cookie 名为 XSRF-TOKEN,前端可读
.and()
.cors()
.and()
.authorizeRequests()
.antMatchers("/api/public/**").permitAll()
.antMatchers(HttpMethod.OPTIONS, "/api/**").permitAll() // 预检放行
.antMatchers("/api/**").authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
}
}
前端 Vue 配置:
// main.js
import axios from 'axios';
axios.defaults.withCredentials = true; // 允许携带 Cookie
axios.defaults.xsrfCookieName = 'XSRF-TOKEN';
axios.defaults.xsrfHeaderName = 'X-XSRF-TOKEN';
结果分析:后端登录成功后,CsrfFilter 将 Token 写入 XSRF-TOKEN Cookie。前端每次 AJAX 请求自动从 Cookie 中读取并写入 X-XSRF-TOKEN Header 发送。恶意站点因同源策略无法读取 XSRF-TOKEN Cookie,无法构造合法 Header,请求在 CsrfFilter 层被 403 拒绝。
易错场景:禁用 CSRF 后 API 仍被攻击
面试高频考点:很多开发者认为"SPA 用 JWT 就不存在 CSRF"或"REST API 不需要 CSRF 防护",这是错误认知。
错误场景还原:
@EnableWebSecurity
public class ApiSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable() // ❌ 错误:即使是无状态 Session,如果用了 Cookie 会话认证就仍有风险
.authorizeRequests()
.antMatchers("/api/**").authenticated()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED);
}
}
问题分析:
- 若使用
SessionCreationPolicy.IF_REQUIRED或ALWAYS,Spring 仍会创建JSESSIONIDCookie 维持会话; - 攻击者诱导用户访问恶意页面,自动提交 POST 请求到
/api/deleteAccount; - 浏览器自动携带
JSESSIONIDCookie,CsrfFilter已被禁用,请求直达 Controller; - 服务端认证通过,账户被删除,攻击成功。
正确做法:如果确实使用 Session + Cookie 认证,绝不能全局禁用 CSRF。仅在无状态 Token 认证(如 JWT 放在 Header)的场景下,才可安全禁用 CSRF:
http.csrf().disable(); // 仅当使用 JWT / OAuth2 无状态认证且完全不依赖 Cookie 会话时安全
面试追问:CookieCsrfTokenRepository.withHttpOnlyFalse() 为什么设置 HttpOnly=false?
答:因为前端 JavaScript 需要读取 Cookie 中的 Token 值,以便将其写入请求 Header 或参数中。如果
HttpOnly=true,浏览器会禁止 JS 读取,SPA 无法获取 Token 完成校验,导致所有写请求被 403 拒绝。但这也意味着 Token 存在被 XSS 脚本读取的风险,因此必须同时做好 XSS 防护,确保前端无脚本注入漏洞。
核心类速查
| 类 | 说明 |
|---|---|
CsrfFilter | 核心过滤器,生成与校验 CSRF Token |
CsrfTokenRepository | Token 存储接口,决定 Token 存储位置 |
HttpSessionCsrfTokenRepository | 默认实现,Token 存入 HttpSession |
CookieCsrfTokenRepository | Token 存入 Cookie,支持 SPA 读取 |
LazyCsrfTokenRepository | 延迟生成 Token,优化性能 |
CsrfToken | Token 抽象,包含 getToken() 和 getHeaderName() / getParameterName() |