这事儿过去好几年了,但我至今记得那个下午。我盯着后台日志里那条奇怪的请求,反复看了五六遍——用户名、金额、收款账户,全都是对的,唯独发起者的 IP 远在另一个大洲。更诡异的是,那个用户坚称自己从未操作过。直到后来我看到了请求头里的 Referer… 操,是空的。一下子全明白了。那是我第一次近距离接触 CSRF,说实话,有点后背发凉。因为这意味着,只要我登录着你的网站,随便点开一个不明链接,我的钱可能就飞了。而你,作为开发者,可能压根儿都没察觉到。
CSRF(Cross-Site Request Forgery),跨站请求伪造。这名字听着挺高大上,拆解开了其实就一句话:挟持用户浏览器,以用户的名义发起恶意请求。 它的核心不在技术有多高深,而在于它巧妙地利用了一个所有浏览器都默认遵守的规则——自动携带 cookie。你登录了 bank.com,浏览器里存着 session cookie。这时候,你点开了钓鱼邮件里的一张图片,或者一个看起来很正常的链接,实际上它可能偷偷发了一个 form 提交到 bank.com 的转账接口。浏览器一看,噢,要请求 bank.com,好嘞,我把 cookie 给你带上。于是,银行后台收到了一个带着你有效 cookie 的请求,它怎么区分这是你的本意还是别人的伪造?没法区分。
咱们来画个图,可能更直观。

说白了,这就是一种“借刀杀人”。攻击者不需要窃取你的密码,他只需要让你在不知情的情况下“签个名”就行。这玩意儿跟 XSS 不一样,XSS 要往页面里注入脚本,CSRF 只关心你发出去的请求。所以,很多开发者把防御重心全放 XSS 上,结果 CSRF 这边城门大开。直到今天,我还能在一些新上线的系统里发现低级 CSRF 漏洞。
Token 不是银弹,但没它真不行
防御 CSRF 的思路其实很直接:让请求包含一个攻击者无法预测、无法获取的随机值。 最主流的就是 CSRF Token。服务器生成一个随机串,发给客户端;客户端后续请求时必须带上它,服务器校验通过才放行。由于同源策略的限制,攻击者没法从其他域读取到你的 Token,自然就构造不出合法请求。
听起来完美,对吧?但落地的时候事情就开始变味了。我记得有一次,团队压测,QPS 刚上 3000,数据库延迟直奔 200ms。一查,好家伙,每个请求都去 Redis 里读一次 Token 做对比。当时的设计是把 Token 存 session 或者 Redis,每次校验都要远程调用。这对于动辄几万并发的系统简直是灾难。后来我们改成了无状态 Token —— 把 Token 和一个内置的 salt 拼起来做一次 HMAC,然后直接放在 cookie 或 header 里。服务器拿到 Token 后重新算一遍 HMAC 对比,完全不用查存储。改造之后,同样的压测场景,数据库 CPU 下降了将近 40%,P99 延迟从 180ms 降到了 12ms。这就是工程上的取舍。有些文章告诉你用 Token 就完了,却不说你该怎么存、怎么比,这就是坑。

还有 SameSite Cookie。这属性一出来,我都快感动哭了。 SameSite=Strict 或 Lax 可以彻底阻断来自第三方站点的请求携带 Cookie,直接从浏览器层面把 CSRF 的一大半攻击路子给封死了。几行配置的事,何乐不为?但是——永远有但是——它不兼容一些老旧的业务场景,比如跨域的支付回调,或者某些需要跨站传递登录态的 OAuth 流程。硬上 SameSite 可能导致功能直接挂掉。所以,SameSite 适合当第一道防线,Token 兜底,别指望单靠一个就能高枕无忧。
我掉进去的三个大坑,你可别再踩了

坑一:跨域部署下的 Token 传递地狱。 前后端分离,前端在 a.com,后端在 b.com。这时候 Token 怎么给?放 cookie 里吧,跨域没法读;放响应头里吧,每次都得自定义 header,还可能触发预检请求。我们的第一个方案是把 Token 注入到页面模板里,前端从 DOM 里取。结果有一天前端重构,把那个隐藏 input 给干掉了——所有 POST 请求全线 403。大半夜爬起来修 bug 的滋味,真不好受。最终我们定了条铁律:统一把 Token 放在名为 X-CSRF-Token 的响应头里,前端拦截器从响应头中读取并存入内存变量,每次请求带上。一定要把 Token 的存取逻辑收敛到一个地方,不然迟早出事。
坑二:Token 泄露。 你以为 Token 是安全的?天真了。如果你的站点存在 XSS,攻击者可以直接读到页面上的 Token,甚至读完你的 cookie。那 CSRF Token 形同虚设。所以,防御 CSRF 的前提是你得先防住 XSS。但我们不能假设系统完全没有 XSS,所以还得加一层——Token 绑定用户行为或 IP。比如把用户 ID 和时间戳混入 Token 生成签名,或者签发 Token 时绑定客户端的 IP 段,校验时发现 IP 漂移就做二次挑战。虽然会牺牲一点用户体验,但在安全这件事上,适度的麻烦是必须的。
坑三:GET 请求的误伤。 很多教程告诉你“只保护写操作”,所以只给 POST/PUT/DELETE 加 Token。但你想过没,有些敏感操作恰恰是 GET 触发的?比如通过 GET 参数完成密码重置、邮件退订。攻击者可以利用 img 标签发起 GET 请求,虽然无法读取响应,但副作用已经发生了。我们吃过一次亏:一个内部管理接口用 GET 导出用户数据,因为没有 Token 保护,被爬虫把整份通讯录拖走了。后来我们强制:所有带有副作用的请求,一律禁用 GET,改用 POST,并增加 CSRF Token 校验。 RESTful 的纯洁性?在安全面前,该妥协就妥协。
写在最后的真实想法
我见过太多项目,安全设计挂在 PPT 里,代码里却只剩一句“TODO: 加固 CSRF”。CSRF 这东西,说穿了不是什么高深的技术,但它像一面镜子,照出一个团队对基础的敬畏程度。你仔细琢磨它的防御逻辑,会觉得特别有“工程美感”——Token 的无状态设计、SameSite 的浏览器侧拦截、二次确认的 UX 权衡,每个环节都体现着架构师对安全与性能、可用性的平衡。别等到出事了才去补,那会儿你面对的就不只是代码漏洞,而是信任的崩塌。