PHP 如何防止 CSRF
打开电脑准备点外卖时,突然弹出一个陌生的“确认订单”弹窗,你随手点了“确定”,结果发现自己悄悄订阅了每月自动扣费的无用服务——这种被骗的经历,其实和一种叫CSRF的网络攻击有关。今天我们就用最通俗的话,聊聊PHP开发的网站,怎么防住这种“隐身的恶意操作”。
先搞懂:CSRF到底是什么?
不用记复杂定义,你就把它想成“冒你的名做坏事”。举个生活化的例子:你登录了自己的银行网站(这是“信任你的网站”),同时浏览器还开了个被黑客控制的钓鱼页面。钓鱼页面悄悄藏了一行代码,自动向银行网站发了“转1000元到黑客账户”的请求——因为银行网站会默认“你已经登录,发过来的请求就是你本人操作”,根本不会怀疑这是钓鱼页偷偷发的。

对PHP开发的网站来说,CSRF的逻辑完全一样:只要你登录后打开了黑客的链接,对方就能用你的身份悄悄操作你的账号,比如改密码、发私信、买东西,根本不需要偷你的密码。
PHP防CSRF的核心:给请求加个“专属暗号”
既然攻击是“冒充你的请求”,那PHP防CSRF的思路就很简单:给每个真实请求加一个只有你和网站知道的“专属暗号”,黑客不知道这个暗号,就算发了请求,网站也会直接拒绝。
这个暗号在技术里叫“CSRF令牌”,实现起来特别简单,举个大家都懂的例子:
你去小区超市买东西,老板怕有人拿假的“提货条”领东西,就给每个真提货条盖了一个只有他才有的“私人印章”。PHP里的CSRF令牌,就是这个“私人印章”——只有网站生成的请求,才会带这个印章,没印章的直接当假的拦下来。
PHP实现CSRF防护的两个实用方法(都是能直接用的)
表单提交:给每一个表单加隐藏的令牌字段
平时你填的注册、登录、下单表单,都可以加一个隐藏的令牌,PHP会把这个令牌存在你的会话(就是你登录后网站存的临时身份信息)里,提交表单时,PHP会对比表单里的令牌和会话里的令牌,不一样就拒绝。
举个代码的简化例子(不用怕,就是几行能看懂的逻辑):
- 登录页面的表单里,PHP会自动生成一个令牌:
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));,然后把这个令牌藏在表单的<input type="hidden" name="csrf_token" value="刚才生成的那个值">里,你提交表单时会一起发过去。 - 提交后PHP对比:
if ($_POST['csrf_token'] !== $_SESSION['csrf_token']) { die("请求无效,请重试"); }——黑客就算想伪造表单,也拿不到会话里的令牌,直接被拦。
接口请求:给每个请求头加令牌(适合APP或Ajax)
现在很多网站不用表单,用Ajax接口发请求(比如你点“点赞”不跳转页面,是悄悄发了个请求),这时候可以把令牌放到请求的头信息里,而不是表单里。
比如你用JavaScript发点赞请求时,PHP会先在你登录时生成令牌,你发请求时把这个令牌放到Headers里,比如X-CSRF-Token:刚才的令牌值,PHP拿到后对比请求头里的令牌和会话里的,对了才执行操作。这种方法适合不需要跳转的交互,比表单更灵活。
要注意的两个细节,不然防护会失效
很多PHP开发者用CSRF防护时,容易踩两个坑,一定要避开:
第一个是令牌重复用:有人生成一个令牌就一直用,这就像老板只盖一个章,有人偷了章就能随便伪造提货条。正确的做法是每次提交表单后,都重新生成新令牌,旧令牌立刻失效。
第二个是令牌存在地方不对:有人把令牌存在URL参数里,比如“下单.php?token=xxxx”,这就像把印章印在提货条外面,路过的人都能看到,黑客直接偷了用。令牌必须存在会话($_SESSION)里,会话是浏览器和网站单独存的,黑客拿不到。
其实CSRF防护一点都不复杂,核心就是“给每个请求加个只有你知道的暗号”,PHP只要用上会话和简单的令牌生成,就能轻松挡住大部分CSRF攻击。毕竟网络安全的核心,就是不让黑客有“冒充你”的机会。