thinkphp 5.1 接口签名必须在中间件统一验签,不可放在控制器;需先读取原始请求体、清洗参数、剔除非业务字段,再按规则拼接签名原文,最后用 hash_hmac + hash_equals 严格校验并防重放。

ThinkPHP 5.1 实现接口签名必须绕过控制器验签的常见陷阱,直接在中间件统一拦截所有请求,否则恶意参数已触发库存扣减或日志写入,验签形同虚设。
签名验证必须放在中间件里
执行 php think make:middleware CheckApiSign 创建中间件;
在 app/middleware.php 中注册该中间件,并确保它排在 think\middleware\ParseToken 之后、业务控制器之前;
中间件 handle() 方法开头就提取 sign 字段,若不存在或为空,立即返回 json(['code'=>401,'msg'=>'sign invalid']) 并终止流程;
【关键】控制器里写验签等于把锁装在屋里——恶意参数已触发日志写入、库存扣减甚至数据库事务,验签失去意义。
统一获取并清洗原始参数
第一步:调用 $raw = $request->getInput() 一次性读取 php://input 流;若为空,则 fallback 到 file_get_contents('php://input');
第二步:根据 $request->header('content-type') 分支处理:
含 application/json → json_decode($raw, true);
否则 → parse_str($raw, $body);
第三步:合并参数:$params = array_merge($request->get(), $body ?: []);,再对每个值执行 trim(str_replace(["\r","\n"], '', (string)$v)) 清除隐藏字符;
第四步:剔除非业务字段:isset($params['sign']) && unset($params['sign']);,同样处理 signature、timestamp、nonce;漏掉 isset() 判断会触发 Notice 报错。
构造签名原文的三要素
方法一:基础拼接(适用于 GET/简单表单)
对 $params 执行 ksort($params, SORT_STRING);
遍历拼接 rawurlencode($k).'='.rawurlencode($v),用 & 连接;
追加 ×tamp=xxx&nonce=yyy(注意:timestamp 必须是客户端传入的 Unix 秒级时间戳);
最后拼上密钥:$data = $query_string . '×tamp=' . $ts . '&nonce=' . $nonce . '&key=' . config('api.sign_key');。
方法二:带 HTTP 方法和路径(防接口挪用)
取 $method = strtoupper($request->method()) 和 $uri = parse_url($request->url(), PHP_URL_PATH);
签名原文格式:$method . "\n" . $uri . "\n" . $query_string . "\n";
【关键】换行符必须是 \n,不能是 \r\n,否则哈希值错位。
计算与比对签名
第一步:用 hash_hmac('sha256', $data, config('api.sign_key'), true) 计算服务端签名;
第二步:对结果做 base64_encode() 或 bin2hex()(取决于客户端约定),常见为 strtoupper(bin2hex(...));
第三步:严格比对客户端传入的 sign 值——使用 hash_equals() 防时序攻击,禁止用 ===;
第四步:校验 timestamp 是否在允许窗口内(如 ±300 秒),超时则拒绝;
第五步:检查 nonce 是否已在 Redis 中存在,存在则拒绝(防重放),并立即写入 setex nonce:xxx 300 1。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











