ThinkPHP 8.0 接口签名验证需在中间件开头读取原始请求体并禁用自动解析,参数归一化要求键小写、字典序排序、值不 urldecode,拼接为 key1=value1&key2=value2 格式;严格校验 timestamp(±5分钟)和 nonce(Redis 去重),失败统一返回 401 且日志脱敏。

ThinkPHP 8.0 实现接口签名验证,核心是**统一原始请求体、严格参数归一化、签名逻辑前置到中间件**。90% 的验签失败不是算法问题,而是服务端没拿到客户端签名时用的那串原始字节——尤其是 JSON 请求体被框架自动解析后,php://input 就空了。
必须在中间件开头读原始 body
TP8.0 默认对 application/json 请求自动调用 json_decode 并清空输入流。一旦你调过 $request->param() 或 $request->post(),原始 JSON 字符串就不可恢复,签名哈希必然不一致。
- 在自定义中间件
handle()方法最开头,只执行一次:$raw = file_get_contents('php://input'); - 同时禁用自动解析:在
config/app.php中设'json_decode' => false,或路由定义加['json' => false] - 若需兼容表单和 JSON,先判断
$request->header('content-type'),再分别用parse_str($raw, $body)或json_decode($raw, true)
签名原文拼接要完全镜像客户端
服务端拼出来的字符串,必须和客户端签名时用的字节流一模一样。任何空格、大小写、排序、编码差异都会导致验签失败。
- 参与字段:所有非空业务参数 +
appid+timestamp+nonce+v(版本号),剔除sign、signature等签名字段 - 键名强制小写,用
ksort($params, SORT_STRING)按字典序升序排列 - 值不做
urldecode—— 客户端传什么字节,你就用什么字节拼 - 拼接格式严格为
key1=value1&key2=value2,无空格、无换行、无末尾 &;POST body 原样追加(哪怕为空字符串)
时间戳与 nonce 必须严格校验
签名只防篡改,不防重放。没有时间窗口和唯一性控制,攻击者截包改个时间戳就能无限重放。
立即学习“PHP免费学习笔记(深入)”;
- 时间戳容错建议:接收后只接受
abs($client_ts - time()) <= 300(5 分钟内),不校验毫秒级精度 - nonce 存 Redis,key 形如
sign:nonce:{$appid}:{$nonce},TTL 设为 300 秒,写入即过期 - 连续 5 次验签失败,建议对该
appid或 IP 做临时限流(Redis 计数器)
错误响应与日志要安全可控
验签失败时暴露细节,等于帮攻击者反推你的逻辑。
- 一律返回 HTTP 401 状态码 + 统一 JSON:
{"code":401,"msg":"Unauthorized"},不提示“时间超时”或“签名不匹配” - 不要记录完整请求参数到日志,至少脱敏
password、token、id_card等字段 - 调试阶段可临时加
debug_sign=1参数,输出服务端构造的签名原文,方便和客户端比对



















