验签必须在中间件中实现,且需全局注册于app/middleware.php中并确保执行顺序靠前;原始参数须统一通过getInput()获取并规范拼接;签名原文需ksort+rawurlencode后追加密钥,用hash_hmac计算、hash_equals比对;防重放需校验timestamp±300秒,并用SET...NX+EX原子操作存储nonce。

ThinkPHP 接口签名验证必须放在中间件里,不能写在控制器中。因为验签是请求合法性第一道防线,一旦进入控制器,路由已匹配、参数已解析、日志可能已记录、数据库查询甚至短信发送都可能触发——攻击者靠一次非法请求就能消耗资源、留下痕迹,验签就失去意义。
中间件位置与注册规范
中间件类需在 app/middleware.php 中全局注册,且顺序关键:
- 必须带完整命名空间,如 \app\middleware\CheckSign::class(开头反斜杠不可省)
- 应排在 \think\middleware\AllowCrossDomain::class 之后、所有业务中间件和控制器之前
- 若注册靠后(比如跨域或日志中间件之后),非法请求可能已写入日志或被错误响应,防御失效
原始参数统一读取与拼接
90% 的验签失败源于客户端与服务端参数不一致。核心是服务端必须 100% 还原客户端签名时的原始输入:
- 一律用 $request->getInput() 或 file_get_contents('php://input') 获取原始请求体,禁用自动解析:
$this->app->http->setRequest($request->withInput(false)) - 按 Content-Type 分流处理:JSON 请求用
json_decode($input, true);表单用parse_str($input, $params) - 显式合并 GET 与解析后的 body:array_merge($request->get(), $parsed_body)
- 剔除
sign、signature、timestamp、nonce等非业务字段后再参与签名
签名原文构造与安全比对
签名不是简单拼接,而是严格可复现的字符串:
立即学习“PHP免费学习笔记(深入)”;
- 对剩余参数执行 ksort($params, SORT_STRING),确保 ASCII 字典序升序
- 每个 value 单独 rawurlencode()(不是 urlencode,空格不转 +)
- 拼成
k1=v1&k2=v2格式,末尾不加 &,再追加 rawurlencode($secretKey) - 签名计算统一用 hash_hmac('sha256', $str, $secret),比对必须用 hash_equals() 防时序攻击
防重放关键控制点
仅验签不够,必须结合时间戳与 nonce 才能防重放:
- timestamp 必须校验:与服务器当前时间差 ≤ 300 秒(±5 分钟),超时即拒
-
nonce 必须 Redis 原子存取:
SET nonce:{$app_id}:{$nonce} 1 EX 300 NX(NX 保证唯一,EX 保证自动过期) - 严禁分两步操作:
SET + EXPIRE在高并发下会覆盖 TTL,导致去重失效 - 建议 key 命名含 app_id,便于多客户端隔离与排查



















