bigywater.tech
Spring · 106

CSRF:令牌怎么生成与校验

CSRF 令牌每次刷新都不一样为什么还能通过校验?哪些请求方法需要令牌?单页应用读 cookie 再放进请求头为什么会 403?登录之后旧页面的令牌还有效吗?这篇在 Spring Boot 4.0.8、Spring Security 7 上逐个实测。

96 字符,每次不同
默认令牌的长度,同一个会话里连续取两次得到不同的值,都能通过校验
403 vs 200
Cookie 方案下把 cookie 里的原始令牌放进请求头:默认处理器 403,明文处理器 200
登录后作废
登录之前拿到的令牌在登录之后 POST 会 403

默认令牌

实测 · 本机执行输出
  令牌的请求头名 = X-CSRF-TOKEN,请求参数名 = _csrf
  同一个会话里连续取两次令牌:第 1 次 bBaOabKGfpXpHWoxAVZOEfgaqcm2YKB25W3hJJHSPtw1YXZkXXe2WNHlT6zEKVsHN3t6JJt4hKjUUphbgQiEFangXO8BAhcC(长度 96);第 2 次 8D9OUiYnsRzgawct71MfSU-Dlm8t4ZrbHtKzCnMlsjwNqPnjwV52Y0VEgCXNXzYb2X4rfCzhuw5P06L2erfWO0sX0A85y5iF(长度 96);两次相同:False
  各种带法 POST /post:
    不带令牌 → (403, '{"timestamp":"2026-10-20T04:15:56.910Z","status":403,"error":"Forbidden","path":"/post"}')
    用请求头 X-CSRF-TOKEN 带第 1 次的令牌 → (200, 'posted')
    用请求头带第 2 次的令牌(同一会话里另一个掩码) → (200, 'posted')
    用请求头带一个随便写的令牌 → (403, '{"timestamp":"2026-10-20T04:15:56.925Z","status":403,"error":"Forbidden","path":"/post"}')
    用表单参数 _csrf 带令牌 → (200, 'posted')
  不同的 HTTP 方法(不带令牌):GET /who → 200;POST /post → 403;DELETE /item → 403;HEAD /who → 200;OPTIONS /who → 200

每次取到的值不同但都有效;POST、DELETE 等需要令牌,GET、HEAD、OPTIONS 不需要。

登录之后

实测 · 本机执行输出
  登录页里的令牌 ZFkMvKn5xGXZ…(登录用的);登录成功 → HTTP 302
  登录之后用登录页那个旧令牌 POST /post → (403, '{"timestamp":"2026-10-20T04:15:58.648Z","status":403,"error":"Forbidden","path":"/post"}')
  登录之后取到新令牌再 POST /post → (200, 'posted')

登录会换掉令牌,登录前缓存的旧令牌作废。

Cookie 方案

实测 · 本机执行输出
  CookieCsrfTokenRepository.withHttpOnlyFalse() + 默认的请求处理器:Cookie XSRF-TOKEN = befb7652-11b…(长度 36);/csrf 接口给出的令牌长度 96
    把 Cookie 里的值原样放进 X-XSRF-TOKEN 请求头 POST /post → (403, '{"timestamp":"2026-10-20T04:16:00.330Z","status":403,"error":"Forbidden","path":"/post"}')
    用 /csrf 接口给出的令牌放进 X-XSRF-TOKEN 请求头 → (200, 'posted')
  CookieCsrfTokenRepository.withHttpOnlyFalse() + CsrfTokenRequestAttributeHandler(明文):Cookie XSRF-TOKEN = c7311397-f98…(长度 36);/csrf 接口给出的令牌长度 36
    把 Cookie 里的值原样放进 X-XSRF-TOKEN 请求头 POST /post → (200, 'posted')
    用 /csrf 接口给出的令牌放进 X-XSRF-TOKEN 请求头 → (200, 'posted')

cookie 里是原始令牌,默认处理器不认,要换成明文处理器或用 SPA 专用处理器。

放行与关闭

实测 · 本机执行输出
  csrf.ignoringRequestMatchers("/api/**"):POST /api/post 不带令牌 → (200, 'api posted');POST /post 不带令牌 → (403, '{"timestamp":"2026-10-20T04:16:03.719Z","status":403,"error":"Forbidden","path":"/post"}')
  csrf.disable():POST /post 不带令牌 → (200, 'posted')

ignoringRequestMatchers 按路径放行;disable 全部关闭。

分步演示

CSRF
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动(Spring Security 7)。用户 alice / pw,表单登录;客户端是 Python http.client,手动带 Cookie;令牌从登录页 HTML 中解析,或由我写的 /csrf 接口(返回当前请求的 CsrfToken)取得;不同的 SecurityFilterChain 由 -Dx106.cfg 切换
    • 没有验证:BREACH 攻击的具体防护原理(只观察到令牌每次不同);SameSite cookie 对 CSRF 的补充作用;SpaCsrfTokenRequestHandler 本身(用的是明文处理器);多页面、多标签同时使用时的令牌表现;Origin / Referer 校验;WebFlux 里的 CSRF;第 107 篇会讲 CORS 与 Security 的交互
    京ICP备2026067502号-1京公网安备11010502063487号