最新Spring Security实战教程(十一):CSRF攻防实战
1. 前言
在前面学习的章节中,相信大家一定看过一个配置.csrf()。回忆一下之前使用Spring Security默认页登录的时候,该配置Spring Security默认开启,主要用于CSRF防护。如果你现在还不了解什么是CSRF防护,没关系,通过本章节,博主带着大家一起深入学习这个知识点~
2. CSRF攻击原理
跨站请求伪造(CSRF)是一种利用受信任用户的身份,诱使用户在已登录的应用中执行非预期操作的攻击手段。
当用户在某个站点(如银行)登录并持有有效Session Cookie后,攻击者可通过精心构造的请求(例如隐藏在图片或表单中的POST请求)在用户不知情的情况下向该站点发起请求,并携带用户的Cookie,从而完成诸如转账、修改邮箱等敏感操作。
2.1 攻击原理图解
用户访问了A站点,获得了Session或Cookie后,
用户不经意间访问到了恶意网站,此刻恶意网站伪造对A站点的危险请求

2.2 攻击示例
下面示例展示了一个最常见的CSRF攻击场景:用户登录了https://bank.com后,攻击者在自己的网站https://evil.com上放置如下HTML片段:
1 2
| <img src="https://bank.com/transfer?amount=1000&to=attacker" />
|
3. Spring Security防御机制解析
3.1 同步令牌模式(Synchronizer Token Pattern)
同步令牌模式是Spring Security的默认方案,服务器在渲染每个需要保护的表单页面时,向用户Session中存入一个随机生成的Token,并在表单中以隐藏字段输出;提交时,服务器验证该字段与Session中的Token是否一致,若不匹配则拒绝请求。此模式能有效防止CSRF攻击,因为攻击者无法从第三方域读取到该随机Token。
核心防御流程

- 服务端生成随机Token(每个Session唯一)
- Token嵌入HTML表单的隐藏字段或HTTP头
- 客户端提交请求时必须携带有效Token
- 服务端校验Token合法性
3.2 双重提交Cookie(Double Submit Cookie)
服务器在首次响应页面时,通过Set-Cookie设置一个随机Token,同时在页面中通过脚本将该Token读出并写入一个请求头(或隐藏表单字段)。服务器接收请求后,比较Cookie中的Token与请求中携带的Token是否一致。由于浏览器同源策略不能让第三方域读取Cookie,攻击者无法同步两个值。
3.3 SameSite Cookie属性
浏览器支持在Set-Cookie响应头中声明SameSite属性,用来限制Cookie在跨站请求时是否发送。设置为Strict或Lax模式,可从源头上阻止大部分CSRF请求。
- SameSite=Strict:绝不在第三方请求中发送该Cookie;
- SameSite=Lax:仅允许在“安全”的跨站GET导航中发送。
4. 实战代码示例
这里我们将针对上述三种防护机制,进行相关代码演示。
4.1 在Spring Security中启用CSRF防护
Spring Security默认开启CSRF保护,采用的是同步令牌模式。下面展示如何在单体、前后分离中集成。
❶ Thymeleaf模板中集成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf ) .authorizeHttpRequests(auth -> auth .anyRequest().authenticated() ) .formLogin(withDefaults()); return http.build(); } }
|
在Thymeleaf页面中添加隐藏字段,设置Token:
1 2 3 4 5 6 7
| <form th:action="@{/transfer}" method="post"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <input type="number" name="amount" /> <button type="submit">Transfer</button> </form>
|
在控制器中,Spring Security自动会在每次POST请求时校验表单中的${_csrf.token}与Session中的令牌是否匹配,不匹配则抛出InvalidCsrfTokenException。
❷ 前后端分离适配方案
下面演示在前后分离中的适配,前端在请求前初始化时获取CSRF Token:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| public class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler { @Override public void handle(HttpServletRequest request, HttpServletResponse response, Supplier<CsrfToken> csrfToken) { CsrfToken token = csrfToken.get(); if (token != null) { response.setHeader(token.getHeaderName(), token.getToken()); } } }
@Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler()) ) return http.build(); } }
|
前端演示代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| fetch('/csrf', { credentials: 'include' }) .then(res => { const token = res.headers.get('X-CSRF-TOKEN'); axios.defaults.headers.common['X-CSRF-TOKEN'] = token; });
axios.interceptors.request.use(config => { if (['post', 'put', 'delete'].includes(config.method.toLowerCase())) { config.headers['X-CSRF-TOKEN'] = getCSRFToken(); } return config; });
|
❸ 自定义令牌存储策略
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| @Bean public CsrfTokenRepository redisCsrfTokenRepository(RedisTemplate<String, String> redisTemplate) { return new CsrfTokenRepository() { @Override public CsrfToken generateToken(HttpServletRequest request) { return new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", UUID.randomUUID().toString()); } @Override public void saveToken(CsrfToken token, HttpServletRequest request, HttpServletResponse response) { String sessionId = request.getSession().getId(); if (token == null) { redisTemplate.delete(sessionId); } else { redisTemplate.opsForValue().set(sessionId, token.getToken(), 30, MINUTES); } } @Override public CsrfToken loadToken(HttpServletRequest request) { String sessionId = request.getSession().getId(); String token = redisTemplate.opsForValue().get(sessionId); return token != null ? new DefaultCsrfToken("X-CSRF-TOKEN", "_csrf", token) : null; } }; }
|
4.2 双重Cookie验证
实际上在我们日常开发中,使用Spring Security同步令牌方案基本能满足我们大部分需求,这里就简单演示一下双重Cookie验证:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40
| public class DoubleCookieCsrfFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { CsrfToken token = (CsrfToken) request.getAttribute(CsrfToken.class.getName()); if (requiresValidation(request)) { String headerToken = request.getHeader(token.getHeaderName()); String cookieToken = getCookieValue(request, "CSRF-TOKEN"); if (!token.getToken().equals(headerToken) || !token.getToken().equals(cookieToken)) { response.sendError(HttpStatus.FORBIDDEN.value()); return; } } filterChain.doFilter(request, response); } private boolean requiresValidation(HttpServletRequest request) { return "POST".equalsIgnoreCase(request.getMethod()) || "PUT".equalsIgnoreCase(request.getMethod()) || "DELETE".equalsIgnoreCase(request.getMethod()); } private String getCookieValue(HttpServletRequest request, String cookieName) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { if (cookieName.equals(cookie.getName())) { return cookie.getValue(); } } } return null; } }
|
4.3 SameSite Cookie策略
限制cookie的跨站请求:
1 2 3 4 5 6 7 8 9 10 11 12
| @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()) .csrfTokenRequestHandler(new CsrfTokenRequestAttributeHandler()) ) .sessionManagement(session -> session .sessionCookiePolicy(cookie -> cookie.sameSite(SameSite.STRICT)) ); return http.build(); }
|
结语
CSRF攻击凭借“利用用户身份”的特点,对任何依赖Cookie的状态修改接口都构成威胁。本文从攻击原理入手,详细介绍了同步令牌、双重提交Cookie、SameSite属性等防护方案,并给出了对应代码供小伙伴们参考!