ThinkPHP需手动实现签名+时间戳+nonce校验防重放,因单独用timestamp或nonce存在窗口期重放、并发漏判、存储非原子等问题;必须HTTPS前提下,固定字段排序签名、Redis原子存取nonce、中间件前置校验。

ThinkPHP 本身不内置防重放机制,必须手动实现签名+时间戳+nonce校验,否则任何带业务参数的接口(尤其是登录、支付、修改密码)都可被截获后无限重放。
为什么 timestamp 和 nonce 单独用没用
只传 timestamp 和 nonce 并在服务端检查“是否超时”“是否已存在”,无法阻止重放——攻击者截到一次合法请求,立刻重发,只要时间仍在窗口内、nonce 还没写入 Redis,校验就直接通过。
- 常见错误:用
$_GET['timestamp']做简单大小比较,没做绝对值容差 - 更危险的是:把
nonce存 MySQL 或文件里,没加唯一索引或原子写入,导致并发时漏判 -
uniqid()不够安全,应配合random_bytes(16)或bin2hex(openssl_random_pseudo_bytes(16))
ThinkPHP 接口签名必须覆盖哪些字段
签名字符串必须包含且仅包含参与校验的字段,且顺序固定,否则客户端和服务端算出的 sign 必然不一致。
- 必含字段:
timestamp、nonce、所有业务参数(如user_id、amount)、api_key(用于标识调用方) - 排序规则:按 key 字典序升序,例如
api_key=xxx&amount=100&nonce=abc×tamp=1717023456&user_id=123 - 每个 value 必须
urlencode(),空值也要编码成%20或%27,不能跳过 - 签名算法必须用
hash_hmac('sha256', $data_string, $secret_key),禁用md5()或裸sha1()
如何在 ThinkPHP 6/8 中安全落地 nonce 校验
不能靠 session 或本地数组存 nonce,必须用 Redis 的原子操作保证全局唯一性和自动过期。
立即学习“PHP免费学习笔记(深入)”;
- 校验逻辑必须放在中间件(如
app/middleware/ApiSignCheck.php)中,在路由分发前执行 - Redis 写入必须用
SET nonce:abc123 1 EX 300 NX,NX确保只在 key 不存在时写入,避免竞态 - 如果 Redis 不可用,应直接拒绝请求(
abort(401)),不能降级为内存缓存或跳过 - 不要把
nonce和用户 ID 拼成 key(如nonce:uid123:abc),会破坏全局去重能力
HTTPS 是前提,不是可选项
哪怕签名再严,只要走 HTTP,攻击者就能在中间人位置拿到完整请求体,包括 timestamp、nonce、sign,然后原样重放。所有防重放措施默认失效。
- 确认 Nginx/Apache 已强制跳转 HTTPS,且
$_SERVER['HTTPS'] === 'on'或$_SERVER['HTTP_X_FORWARDED_PROTO'] === 'https' - ThinkPHP 配置中启用
'url_https' => true,避免生成 HTTP 链接 - 前端 Vue/React 发送请求时,
axios.defaults.baseURL必须是https://开头 - 禁用
http://回调地址、Webhook 地址、OAuth redirect_uri
真正难的不是拼出那个 hash_hmac() 调用,而是确保从客户端生成、传输、服务端解析、排序、编码、存储 nonce、验证时间窗,每一步都无歧义、无时差、无降级。少一环,整套就形同虚设。



















