1. 前言

在前面学习的章节中,相信大家一定看过一个配置.csrf()。回忆一下之前使用Spring Security默认页登录的时候,该配置Spring Security默认开启,主要用于CSRF防护。如果你现在还不了解什么是CSRF防护,没关系,通过本章节,博主带着大家一起深入学习这个知识点~


2. CSRF攻击原理

跨站请求伪造(CSRF)是一种利用受信任用户的身份,诱使用户在已登录的应用中执行非预期操作的攻击手段。

当用户在某个站点(如银行)登录并持有有效Session Cookie后,攻击者可通过精心构造的请求(例如隐藏在图片或表单中的POST请求)在用户不知情的情况下向该站点发起请求,并携带用户的Cookie,从而完成诸如转账、修改邮箱等敏感操作。

2.1 攻击原理图解

用户访问了A站点,获得了Session或Cookie后,
用户不经意间访问到了恶意网站,此刻恶意网站伪造对A站点的危险请求

CSRF攻击原理图

2.2 攻击示例

下面示例展示了一个最常见的CSRF攻击场景:用户登录了https://bank.com后,攻击者在自己的网站https://evil.com上放置如下HTML片段:

<!-- 在恶意页面上渲染时,立即向 bank.com 发起转账请求 -->
<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模板中集成

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    
    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -> csrf
                // 可自定义CsrfTokenRepository,例如 CookieCsrfTokenRepository.withHttpOnlyFalse()
            )
            .authorizeHttpRequests(auth -> auth
                .anyRequest().authenticated()
            )
            .formLogin(withDefaults());
        return http.build();
    }
}

在Thymeleaf页面中添加隐藏字段,设置Token:

<!-- Thymeleaf 模板:form.html -->
<form th:action="@{/transfer}" method="post">
    <!-- 输出 CSRF 隐藏字段 -->
    <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:

// 自定义CSRF令牌处理器
public class SpaCsrfTokenRequestHandler extends CsrfTokenRequestAttributeHandler {
    
    @Override
    public void handle(HttpServletRequest request, 
                      HttpServletResponse response, 
                      Supplier<CsrfToken> csrfToken) {
        // 将CSRF Token暴露给前端JavaScript
        CsrfToken token = csrfToken.get();
        if (token != null) {
            response.setHeader(token.getHeaderName(), token.getToken());
        }
    }
}

// SecurityConfig.java
@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();
    }
}

前端演示代码:

// 初始化时获取CSRF Token
fetch('/csrf', { credentials: 'include' })
  .then(res => {
    const token = res.headers.get('X-CSRF-TOKEN');
    axios.defaults.headers.common['X-CSRF-TOKEN'] = token;
  });

// 所有POST请求自动携带Token
axios.interceptors.request.use(config => {
  if (['post', 'put', 'delete'].includes(config.method.toLowerCase())) {
    config.headers['X-CSRF-TOKEN'] = getCSRFToken();  
  }
  return config;
});

❸ 自定义令牌存储策略

// 使用Redis存储CSRF令牌(分布式场景)
@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验证:

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的跨站请求:

@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的状态修改接口都构成威胁。本文从攻击原理入手,详细介绍了同步令牌双重提交CookieSameSite属性等防护方案,并给出了对应代码供小伙伴们参考!

最后修改:2026 年 03 月 12 日
如果觉得我的文章对你有用,请随意赞赏