签名校验必须在中间件中实现,不能放控制器里;因中间件是tp8请求生命周期中唯一可统一拦截、不侵入业务逻辑且能确保所有接口被覆盖的环节,控制器校验易重复或遗漏,导致重放攻击风险。

签名校验必须在中间件里做,不能放控制器
TP8 的请求生命周期中,中间件是唯一能统一拦截所有接口、且不侵入业务逻辑的位置。如果把校验逻辑写在控制器里,要么重复写一堆 checkSign(),要么漏掉某个新接口——重放攻击就发生在漏掉的地方。
推荐新建中间件 app/middleware/ApiSignatureCheck.php,并在 app/middleware.php 中全局注册(或按路由分组绑定):
return [
'api' => [
\app\middleware\ApiSignatureCheck::class,
],
];
注意:中间件顺序很重要,它必须在 ValidatePostSize 之后、业务控制器之前执行,否则可能读不到完整请求体。
签名算法要固定参数顺序和编码规则
TP8 默认用 input() 获取参数,但这个方法对 GET/POST/JSON 混合请求行为不一致。必须统一用 $request->param()(合并后)+ 手动提取 sign、timestamp、nonce,再构造待签名字符串。
常见坑点:
- 参数 key 必须字典序排序(用
ksort()),否则客户端和服务端算出的签名永远不一致 - 空值、
null、0都要转成字符串''或保留原值?建议统一转空字符串,避免 PHP 自动类型转换干扰哈希结果 - URL 编码必须严格:用
rawurlencode()而非urlencode(),因为后者会把空格转成+,而标准签名要求空格转%20
示例核心逻辑片段:
$params = $request->param();
unset($params['sign']);
ksort($params);
$toSign = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$expectedSign = hash_hmac('sha256', $toSign, config('api.sign_key'));
时间戳和随机串(nonce)必须查重且限时
仅校验 timestamp 是否在 ±5 分钟内是不够的——攻击者可以截获一次合法请求,反复重放同一套 timestamp+nonce+sign。必须把 nonce 存起来做去重。
推荐做法:
-
timestamp校验:取当前时间与请求时间差绝对值 ≤ 300 秒(硬性要求,拒绝过期请求) -
nonce校验:用 Redis 存setex api:nonce:{$nonce} 300 1,若设置失败(返回0)说明已存在,直接拒掉 - 不要用数据库存 nonce:高并发下写入延迟和锁竞争会导致误判或性能瓶颈
Redis key 命名建议带前缀和过期时间对齐,避免残留;同时注意 nonce 长度至少 16 位随机字符串(用 bin2hex(random_bytes(16))),防止被暴力枚举。
错误响应要统一且不泄露细节
签名失败时,别返回 "sign error: invalid timestamp" 这类提示——攻击者会靠响应差异判断你校验了哪一步。所有失败都走同一个错误码和消息,比如 401 Unauthorized + {"code":401,"msg":"Unauthorized"}。
另外,TP8 的异常处理机制默认会输出调试信息,线上环境务必确认 app_debug 为 false,且中间件里用 throw new HttpException(401, 'Unauthorized') 而非 exit/json,确保走统一异常渲染流程。
容易被忽略的一点:如果接口支持 CORS,签名中间件必须在 Cors 中间件之后运行,否则预检请求(OPTIONS)会被拦住——但 OPTIONS 本身不该签名校验,所以要在中间件里加 if ($request->method() === 'OPTIONS') return $next($request);。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











