php安全csrf token必须用bin2hex(random_bytes(32))生成并存入$_session,输出时htmlspecialchars转义,校验用hash_equals防时序攻击,通过后立即重置。

PHP表单防CSRF攻击,核心就一条:token必须绑定当前会话、一次性使用、恒定时间校验。用time()、rand()或md5(uniqid())生成的值可被预测,等于没设防。
怎么生成真正安全的CSRF token
PHP原生不提供csrf_token()函数,你得自己写,且必须满足密码学安全要求:
- 用
bin2hex(random_bytes(32))生成——random_bytes()是唯一被PHP官方标记为“cryptographically secure”的随机源 - 存进
$_SESSION['csrf_token'],不能放$_COOKIE、URL参数、localStorage或JS变量里 - 每次调用
session_regenerate_id(true)(如登录成功后)后,必须立刻重新生成token,否则旧session数据还在但ID已换,token实际失效 - 建议封装成
ensure_csrf_token()函数,在每个需要表单的页面开头显式调用,别依赖“自动初始化”
表单里怎么输出token才不翻车
很多人直接写<input value="<?php echo $_SESSION['csrf_token']; ?>">,这会引发属性注入——如果token里含双引号或>,HTML结构就崩了,甚至执行JS。
- 必须用
htmlspecialchars()转义:<input type="hidden" name="csrf_token" value="<?php echo htmlspecialchars($_SESSION['csrf_token'] ?? ''); ?>"> - 严禁用JS动态填充value,比如
document.querySelector('[name=csrf_token]').value = token——XSS一触发,token立刻泄露 - 多个form共用一个token可以,但校验通过后必须立刻重置
$_SESSION['csrf_token'],不是unset()后不管 - AJAX请求要从DOM读隐藏字段值,别从全局变量或单独API接口取——那等于开了个token获取通道
校验时为什么必须用hash_equals()
用===或==比较token会暴露时序差异:攻击者发大量请求测响应时间,就能逐字节爆破出token。尤其bin2hex()生成的字符串长度固定,更容易被利用。
- 校验逻辑必须严格:先检查
$_POST['csrf_token']是否存在且非空,再检查$_SESSION['csrf_token']是否存在且非空 - 然后用
hash_equals($_SESSION['csrf_token'], $_POST['csrf_token'])——它恒定时间,不依赖字符串内容 - 校验通过后,必须立即重置:
$_SESSION['csrf_token'] = bin2hex(random_bytes(32));,防止重放 - 失败时统一返回HTTP 400或跳转错误页,别返回403/419并附带提示文本——会暴露校验是否触发
多标签页和长页面提交失败不是bug
用户开两个tab编辑同一页面,或表单挂了20分钟再提交,token大概率已失效。这不是实现问题,而是设计使然:CSRF防护的本质就是“一次一验”。
- session生命周期别设太短,
ini_set('session.gc_maxlifetime', 1440)是底线(24分钟不够用) - 不要指望“永远有效”,该兜体验就兜:前端检测到400响应后可自动刷新页面+重载token,而不是让用户手动F5
- ThinkPHP等框架的“多tab友好”签名token是特例,靠URL路径+时间戳+会话ID拼接签名实现,但牺牲了一次性——纯PHP项目别照搬
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











