防重放令牌必须用hash_hmac()带密钥生成,结构为timestamp|user_id|nonce,nonce单次有效且redis原子校验,时间偏差±300秒内有效,敏感字段需aes-256-cbc加密,密钥严禁硬编码,header推荐x-api-token避免nginx透传问题。

PHP用hash_hmac()生成防重放的加密令牌
直接用md5()或sha1()拼接时间戳+用户ID生成令牌,等于没加密——攻击者截获一次就能重放。必须带密钥、带时效、带唯一性约束。hash_hmac()是PHP内置安全选择,比手写base64_encode(sha1($key.$data, true))更可靠。
- 密钥必须存于环境变量或配置文件,**绝不能硬编码在代码里**;示例:
$_ENV['API_TOKEN_KEY'] - 令牌结构建议:
timestamp|user_id|nonce(如1717023456|u_8821|a7f3e9),其中nonce用bin2hex(random_bytes(3))生成,单次有效 - 生成后立即把
nonce存入Redis(带过期,比如EX 300),验证时先查是否存在并DEL,防止重复提交 - 时间戳偏差需校验:服务端当前时间与令牌中时间差超过±300秒即拒收,防拖库重放
限流逻辑必须和令牌验证耦合,不能分两步走
很多实现先验令牌再查Redis限流计数,中间存在竞态窗口——攻击者可并发发10个合法令牌绕过限制。正确做法是把「令牌有效性」和「当前用户请求次数」合并为一个原子操作。
- 用Redis的
EVAL执行Lua脚本:先GET令牌对应nonce确认未使用,再INCR该用户+时间窗口的计数器,最后EXPIRE整个key;失败则返回nil - PHP调用示例:
$redis->eval($lua_script, 3, 'rate:u_8821:20240530', 'nonce:a7f3e9', 'token:1717023456|u_8821|a7f3e9') - 不要用
file_get_contents()或cURL去调第三方限流服务——网络延迟会让原子性失效,且增加单点故障
openssl_encrypt()不是必须,但对敏感字段要加密而非仅签名
如果令牌里要携带权限等级、设备指纹等敏感信息(而不仅是防重放),仅hash_hmac()签名不够——攻击者虽不能篡改,但能解码看到明文。此时必须加密。
- 选
openssl_encrypt()而非mcrypt(已废弃)或自研异或加密;算法固定用AES-256-CBC,IV必须随机且随密文一起传输(base64后拼在密文末尾) - 密钥仍来自环境变量,且**每次加密必须用新IV**:
$iv = random_bytes(openssl_cipher_iv_length('AES-256-CBC')) - 解密失败(返回
false)或解密后JSON结构非法,一律按非法令牌处理,不抛异常、不打日志细节(防侧信道)
前端传令牌时别踩Authorization头格式坑
很多前端开发者习惯写Authorization: Bearer xxx,但你的PHP后端若用getallheaders()手动解析,会因Apache/Nginx配置差异漏掉该头——尤其Nginx默认不透传Authorization。
立即学习“PHP免费学习笔记(深入)”;
- 最稳方案:前端改用自定义头,如
X-Api-Token: xxx,服务端用$_SERVER['HTTP_X_API_TOKEN']直接取(注意下划线转大写) - 若坚持用
Authorization,Nginx需加配置:fastcgi_pass_request_headers on;+underscores_in_headers on;(后者慎开,可能影响其他逻辑) - 别在
$_GET或$_POST里传令牌——URL会被CDN、代理、浏览器历史记录留存,日志里也容易泄露











