必须在中间件中实现签名与时间戳校验,不可放在控制器里;需统一提取原始参数、归一化处理(trim+去\r\n+rawurlencode)、按键字典序排序拼接,并用hash_hmac('sha256', $str, $secret)比对小写sign,同时校验timestamp±60秒及redis去重防重放。

在ThinkPHP 8.0中实现API接口的签名与时间戳校验,必须绕过控制器层,在请求进入业务逻辑前完成安全拦截——否则恶意参数可能已触发日志记录、数据库写入或短信发送等副作用。
注册全局安全拦截器
打开 app/middleware.php 文件,在中间件数组末尾追加自定义拦截器类名:
【注意:必须放在所有业务中间件之前,且不能放在控制器里做验签】
将 app\middleware\ApiSecurity::class 插入到 'api' => [...] 数组最前端,确保它最先执行。
这一步不可跳过,否则验签失败时业务逻辑可能已被执行。
构造签名原文的三步法则
服务端签名验证失败,90%源于客户端与服务端拼接规则不一致。请严格按以下顺序处理参数:
- 第一步:调用
$request->getInput()获取原始请求体; - 第二步:根据
Content-Type头判断解析方式——是application/json就用json_decode($input, true),否则用parse_str($input, $body); - 第三步:显式合并
array_merge($request->get(), $parsed_body),再剔除sign、sign_version、timestamp、token四个字段。
剔除前务必确认这些键真实存在于数组中,否则会漏掉关键参数导致签名不一致。
所有参数值需先 trim(),再 str_replace(["\r", "\n"], '', $v),最后对每个值单独执行 rawurlencode() ——不是 urlencode(),后者会把空格转成 +,而客户端多用 %20。
时间戳校验与防重放
方法一:直接比对
从参数中取出 timestamp 字段,转换为整型后与当前时间戳比较:abs(time() - (int)$params['timestamp']) > 60,超过60秒即拒绝。
方法二:Redis去重(推荐)
将 md5($params['token'] . $params['timestamp']) 作为key,写入Redis并设置过期时间为61秒;若该key已存在,则说明该时间戳已被使用过,立即中断请求。
【Redis key必须包含token,否则不同用户的时间戳会互相干扰】
签名算法实现
服务端必须使用与客户端完全一致的哈希算法和拼接顺序:
将清洗后的参数按键名升序排列 → 拼接成 key1=value1&key2=value2&... 形式 → 在末尾追加 &secret=your_32bit_secret_key → 使用 hash_hmac('sha256', $string, $secret) 计算签名。
客户端传来的 sign 值需全部转为小写后再比对,避免大小写差异导致验签失败。











