CSRF 防御的几种主流策略
CSRF 攻击靠的是浏览器自动带 Cookie 这个“信任漏洞”,让你在不知情时替攻击者发请求。这篇不贴代码,只讲原理:从 Token 验证(随机 Token 和指定算法 Token 两种思路)说起,再聊 SameSite、二次确认、Referer 校验这些配套策略,帮你理清怎么挡住它。
聊防御之前,得先搞清楚 CSRF 到底是怎么坑人的,不然防起来都是瞎防。
CSRF 全称跨站请求伪造。它坑的不是你的密码,而是你已经登录的这个身份。
这篇我们不贴代码,只把原理和防御思路讲透。搞懂了为什么能防住,具体代码怎么写反而是水到渠成的事。
CSRF 到底是怎么发生的
先讲清楚攻击的套路,防御才有的放矢。
关键在浏览器的一个“老实”习惯:只要你访问某个网站,浏览器就会自动把这个网站的 Cookie 带上,不管这个请求是从哪触发的。
我们把场景摆出来。
假设我在银行网站登录了,浏览器里存着银行的登录 Cookie。这时候我没退出,又顺手点开了一个乱七八糟的网站。
这个坏网站的页面里,藏了一个偷偷指向银行转账接口的请求。
浏览器一看是往银行发的请求,二话不说,把银行的 Cookie 老老实实带上了。
银行服务器收到请求,一验 Cookie,身份没问题啊,是本人,转吧。
钱就这么没了。
整个过程里我啥也没干,就点了个网页。这就是 CSRF 最阴的地方:它不需要偷你的密码,它是借用你已经登录的身份,替你发请求。
所以问题的核心是:服务器分不清这个请求到底是我在银行页面上主动发的,还是那个坏网站偷偷替我发的。
防御的所有招数,本质上都在回答同一个问题:怎么确认这个请求真的是用户自己有意发的。
Token 验证 主流打法
最主流、最有效的防御,就是 Token 验证。
思路特别朴素:既然坏网站能借用 Cookie,那我就再加一道 Cookie 之外的暗号。
这个暗号(Token)由服务器生成,放在正规页面里。用户正常操作时,请求会带上这个 Token;而坏网站拿不到这个暗号,它伪造的请求里就没有,服务器一看没暗号,直接拒。
为什么坏网站拿不到?因为它受同源策略限制,读不到银行页面里的内容,自然也偷不走这个 Token。这是 Token 方案能立住的根基。
Token 具体怎么生成,有两种常见思路。
随机 Token
第一种,纯随机。
服务器在用户打开页面时,生成一串随机字符串当 Token,一份塞进页面,一份存在服务器端(比如会话 Session 里)。
用户提交请求时把 Token 带上,服务器拿请求里的 Token 跟自己存的那份一比,对得上就放行,对不上就拒。
它的好处是简单、够安全,随机串猜不出来。
代价是服务器得存这份 Token,还得跟会话绑定。用户多了、接口多了,这个存储和比对是有开销的,分布式环境下还得考虑 Token 存哪、怎么共享。
指定算法 Token
第二种,用算法算出来,不用存。
服务器不再随机生成、也不落地保存,而是拿一些已知信息——比如用户 ID、会话标识、时间戳——再加上一个只有服务器知道的密钥,通过固定算法(常见的是带密钥的哈希,比如 HMAC)算出一个 Token。
校验的时候,服务器拿请求里带的那些信息,用同样的算法和同样的密钥再算一遍,看结果跟请求带来的 Token 对不对得上。
对得上,说明这个 Token 确实是服务器用密钥签出来的,是合法的。
对不上,要么信息被人动过,要么是伪造的,拒掉。
这种方式最大的好处是服务器不用存 Token,算力换存储,天然适合分布式和无状态的场景。密钥只在服务器手里,攻击者没有密钥就算不出合法 Token。
再叠一个时间戳进去,还能顺手做过期控制,Token 超时自动失效,就算漏出去一个也拖不了多久。
两种思路怎么选?
- 系统简单、单机为主 → 随机 Token 够用,实现直白
- 分布式、追求无状态、不想维护 Token 存储 → 指定算法 Token 更合适
没有绝对的好坏,看你的系统形态。
其他配套策略
Token 是主力,但防御从来不是单点作战。下面几招可以叠加上去,多一层保险。
SameSite 属性
这招是从源头掐。
前面说 CSRF 的病根是浏览器跨站自动带 Cookie。那能不能让浏览器“跨站时别带 Cookie”?
可以。给 Cookie 设置 SameSite 属性就是干这个的。
它告诉浏览器:这个 Cookie 只有在同站请求时才带上,跨站过来的请求不许带。
这样一来,那个坏网站发起的跨站请求根本带不上银行 Cookie,CSRF 直接从根上被掐断。
现在主流浏览器都默认给了比较严的 SameSite 策略,这也是这些年 CSRF 没那么猖獗的一个重要原因。
不过它是浏览器行为,老旧浏览器可能不支持,所以更适合当加固层,不建议单独当唯一防线。
二次确认
对特别敏感的操作,比如转账、改密码、删数据,可以在真正执行前再确认一次。
比如要求重新输入密码、输入短信验证码、或者做个滑块之类的交互。
道理很简单:CSRF 能替你发一个请求,但它没法替你输入只有你知道的密码,也收不到你手机上的验证码。
加一道用户必须亲自参与的关卡,伪造请求就过不去了。
代价是牺牲一点体验,所以只用在真正高危的操作上,不能处处都来这么一下,不然用户得烦死。
Referer 校验
还有一招是查请求的来路。
HTTP 请求头里通常带着 Referer(或 Origin),标明这个请求是从哪个页面发出来的。
服务器可以检查:这个请求的来路是不是我自己的域名?如果是从一个陌生的外部网站过来的,那就很可疑,拒掉。
思路是对的,但我个人不太建议拿它当主力。
因为 Referer 这东西不太可靠:有些浏览器或隐私设置会把它去掉,有些场景本来就没有 Referer,一刀切拒绝容易误伤正常用户。
所以它更适合当个辅助信号,跟 Token 配合着用,而不是唯一的判断依据。
怎么搭配才稳
讲了这么多招,实际落地不是挑一个用,而是叠起来。
我自己的排序大概是这样:
- Token 验证打底:这是主力防线,随机 Token 或算法 Token 按系统形态选一个
- SameSite 加固:给 Cookie 设上,从浏览器层面再掐一道,成本极低
- 敏感操作上二次确认:转账改密这类高危动作,加一道用户亲自参与的关卡
- Referer/Origin 当辅助:作为额外信号参考,别单独依赖
安全这事儿从来没有银弹,靠的是层层设防。单独哪一层都可能有缝,但几层叠一块儿,攻击者要同时突破就难了。
小结
绕回最开始那句话:CSRF 的病根,是服务器分不清请求到底是不是用户有意发的。
所有防御,都是在想办法帮服务器把这件事分清楚。
把 Token 当主力、其他几招当加固,层层叠上去,CSRF 基本就翻不起浪了。
原理搞明白了,具体怎么写就看你的框架顺手怎么来了。那就动手加固起来吧。
