前言

Github:https://github.com/HealerJean

博客:http://blog.healerjean.com

一、CSRF 是什么

CSRFCross-Site Request Forgery,跨站请求伪造)是一种利用用户身份发起非自愿请求的攻击方式。攻击者通过诱导用户访问恶意网站或点击链接,以用户的名义向目标网站发送请求,从而完成攻击者期望的操作(如转账、修改密码、添加管理员等)。

1、攻击原理

  1. 用户在浏览器中登录了受信任的网站 A(如银行网站);
  2. 登录成功后,浏览器保存了网站 A 的 Cookie
  3. 用户在未退出网站 A 的前提下,访问了攻击者控制的网站 B
  4. 网站 B 中嵌入了指向网站 A 的请求(如 <img src="http://bank.example/transfer?to=hacker&amount=1000000">);
  5. 浏览器自动携带网站 ACookie 发送请求;
  6. 网站 A 认为请求合法,执行了转账操作。

2、攻击特点

  • 利用用户身份发起请求;
  • 请求看似合法,难以追踪;
  • 不需要窃取用户凭证,仅需诱导用户访问恶意页面;
  • 通常通过 GET 请求实现,但也可用于 POST 请求。

二、CSRF 攻击示例

假设银行网站存在 CSRF 漏洞,攻击者构造如下 HTML 页面:

  • 当用户访问该页面时,浏览器会向银行网站发起请求,自动携带用户 Cookie,完成转账操作。
<img src="http://bank.example/transfer?to=hacker&amount=1000000" style="display:none;" />

1、受害者 A 在银行有一笔存款,通过对银行的网站发送请求 http://bank.example/withdraw?account=A&amount=1000000&for=B 可以使A1000000 的存款转到 B的账号下。通常情况下,该请求发送到网站后,服务器会先验证该请求是否来自一个合法的 session,并且该 session 的用户 A已经成功登陆。

2、黑客 C 自己在该银行也有账户,他知道某个 URL 可以把钱进行转帐操作。A 可以自己发送一个请求给银行:http://bank.example/withdraw?account=A&amount=1000000&for=C。但是这个请求来自 C而非 A,他不能通过安全认证,因此该请求不会起作用。

3、这时,C 想到使用 CSRF 的攻击方式,他先自己做一个网站,在网站中放入如下代码: src=”http://bank.example/withdraw?account=A&amount=1000000&for=C ”,并且通过广告等诱使 Bob 来访问他的网站。

4、当 A 访问该网站时,上述 url 就会从 A的浏览器发向银行,而这个请求会附带 A 浏览器中的 cookie 一起发向银行服务器。大多数情况下,该请求会失败,因为他要求 A 的认证信息。但是,如果 A 当时恰巧刚访问他的银行后不久,他的浏览器与银行网站之间的 session 尚未过期,浏览器的 cookie 之中含有 A 的认证信息。这时,悲剧发生了,这个 url 请求就会得到响应,钱将从 A 的账号转移到C 的账号,而 A 当时毫不知情。等以后 A 发现账户钱少了,即使他去银行查询日志,他也只能发现确实有一个来自于他本人的合法请求转移了资金,没有任何被攻击的痕迹。而 C 则可以拿到钱后逍遥法外。

三、防御 CSRF 的方法

方法 安全性 实现难度 适用场景 是否推荐 描述 优点 缺点
Referer 验证 GET 请求、非敏感操作 ❌(仅辅助) 检查请求来源是否为可信域名 实现简单,对现有系统影响小 Referer 可伪造、可为空、HTTPS 跳转不带 Referer
Token 验证(参数) 通用场景 ✅ 推荐 在请求中加入随机 Token,服务器校验 安全性高,广泛使用 需要前后端配合,Token 管理复杂
Token 验证(Header) 前后端分离项目 ✅ 推荐 Token 放入自定义 Header(如 X-CSRF-Token 适用于前后端分离架构 仅适用于 Ajax 请求,兼容性要求高
验证码 高风险操作 ✅ 辅助推荐 强制用户交互,防止自动请求 安全性高 降低用户体验,不适合频繁操作

1、验证 Referer

1)适用场景:

  • 简单页面请求(如 GET 请求);
  • 已有系统快速接入;
  • 非敏感操作。

2)问题与限制:

  • Referer 可伪造(某些浏览器支持伪造);
  • HTTPSHTTP 跳转不带 Referer;
  • 用户隐私设置可禁用 Referer;
  • 直接输入地址访问时 Referer 为空。

3)示例

@Slf4j
public class RefererInterceptor extends HandlerInterceptorAdapter {

  private Set<String> whiteList = new HashSet<>(Arrays.asList("example.com", "trusted.com"));

  @Override
  public boolean preHandle(HttpServletRequest request, 
                           HttpServletResponse response, 
                           Object handler) throws Exception {
      String method = request.getMethod();
      String referer = request.getHeader("Referer");

      // 仅对 POST 请求强制校验 Referer
      if ("POST".equalsIgnoreCase(method)) {
          if (StringUtils.isBlank(referer)) {
              log.warn("Missing Referer header in POST request");
              response.sendError(HttpServletResponse.SC_FORBIDDEN, "Missing Referer");
              return false;
          }

          try {
              URL refererUrl = new URL(referer);
              String currentHost = request.getServerName();

              if (!currentHost.equals(refererUrl.getHost()) && !whiteList.contains(refererUrl.getHost())) {
                  log.warn("Invalid Referer: {}", referer);
                  response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid Referer");
                  return false;
              }
          } catch (MalformedURLException e) {
              log.warn("Invalid Referer URL: {}", referer);
              response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid Referer");
              return false;
          }
      }

      return true;
  }
}

2、Token 验证(推荐)

1)实现方式:

  • 服务端生成 Token(随机字符串)
  • Token 存入 SessionCookie
  • 前端请求时携带 Token(参数或 Header
  • 服务端校验 Token 是否匹配

2)示例:

前端示例

<form action="/transfer" method="POST">
    <input type="hidden" name="csrfToken" th:value="${csrfToken}" />
    <input type="text" name="to" />
    <input type="submit" value="转账" />
</form>

后端 Token 生成(Java):

String csrfToken = UUID.randomUUID().toString();
session.setAttribute("csrfToken", csrfToken);
String token = request.getParameter("csrfToken");
String sessionToken = (String) request.getSession().getAttribute("csrfToken");

if (!token.equals(sessionToken)) {
    throw new ForbiddenException("Invalid CSRF token");
}

3、自定义 Header Token

1)实现方式:

  • 前端在每次请求中加入 X-CSRF-Token: <token>
  • 后端拦截请求并验证 Token
  • Token 可通过接口获取或从 Cookie 中读取。

2)示例:

前端设置

axios.defaults.headers.common['X-CSRF-Token'] = localStorage.getItem('csrfToken');

后端校验逻辑(Spring Boot

@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    if ("POST".equalsIgnoreCase(request.getMethod())) {
        String token = request.getHeader("X-CSRF-Token");
        String sessionToken = (String) request.getSession().getAttribute("csrfToken");

        if (token == null || !token.equals(sessionToken)) {
            response.sendError(HttpServletResponse.SC_FORBIDDEN, "Invalid CSRF Token");
            return false;
        }
    }
    return true;
}

4、验证码(辅助手段)

1)使用场景:

  • 高风险操作(如修改密码、转账);
  • 防止自动化攻击;
  • 用户交互确认。

2) 缺点:

  • 降低用户体验;
  • 不适用于频繁操作;
  • 无法完全替代 Token 验证。

四、实践建议

  1. 优先使用 Token 验证机制,后端存储到 redis
  2. 对敏感操作强制 Token 校验
  3. 避免使用 GET 请求进行状态变更操作
  4. 前后端分离项目使用自定义 Header + Token
  5. 高风险操作结合验证码
  6. 禁用不必要的跨域请求
  7. 定期更新 Token,防止 Token 被泄露
  8. 使用 SameSite Cookie 属性(推荐 SameSite=StrictLax
  9. 启用 CSRF 防护中间件(如 Spring Security CSRF Protection

1、Spring Security 中的 CSRF 防护

Spring Security 默认启用了 CSRF 防护机制,适用于基于 Session 的认证。

启用方式(Spring Boot):

@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf()
            .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
            .and()
            .authorizeRequests()
            .anyRequest().authenticated();
    }
}

前端使用方式:

// 从 Cookie 中读取 XSRF-TOKEN
const csrfToken = getCookie('XSRF-TOKEN');

function getCookie(name) {
    const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'));
    return match ? match[2] : null;
}

axios.defaults.headers.common['X-XSRF-Token'] = csrfToken;

ContactAuthor