签名校验必须放在中间件里,不能在控制器中做;因控制器执行时路由已匹配、参数已解析,恶意请求可能已触发业务逻辑或打日志,中间件是请求进入业务逻辑前的唯一可控拦截点。

签名校验必须放在中间件里,控制器里做等于白做——请求进控制器前,路由已匹配、参数已解析、日志可能已打、业务逻辑甚至已触发。
为什么必须用中间件而不是控制器
中间件是请求进入业务逻辑前的唯一可控拦截点。ThinkPHP 的 ValidateRequest 中间件完成参数解析后,紧接着就该是签名验证;挂太早(比如在 TrustHosts 前)会拿不到原始请求头,挂太晚(比如在控制器里)攻击者已经能构造恶意参数触发数据库查询或写日志。
- 别用
$request->post()或$request->get()单独取参——它们不保证顺序,且对 JSON 请求体返回空数组 - 统一用
$request->param()拿全部参数(含 query + post),但注意:文件字段、raw body 需单独处理 - 如果接口支持 JSON,得先判断
$request->header('content-type')是否含application/json,再用$request->getContent()取原始 body 并合并进签名原文 - 中间件注册位置:在
app/middleware.php的全局中间件数组中,排在AllowCrossDomain之后、业务控制器之前
签名原文怎么拼才不会和客户端对不上
90% 的验签失败源于服务端拼的字符串和客户端不一致。不是简单把参数连起来,而是有固定顺序和清洗规则:
- 取
$request->url(true)得到路径(如/api/v1/user),不带域名、不带 query - 剔除
sign、signature、sign_type等签名相关字段,但保留sign_version(它必须参与签名) - 用
ksort()对剩余参数按 key 字典序排序,再遍历对每个$k和$v单独调用rawurlencode()(不是urlencode()) - 拼成
$k=$v形式,用&连接,末尾不加& - 最后追加
rawurlencode($secretKey),再传给hash_hmac('sha256', $str, $secret) - 中文参数要确保前后端都用 UTF-8,PHP 内部设
mb_internal_encoding('UTF-8'),值还要trim()并清除\r\n和 BOM
timestamp 和 nonce 怎么防重放才靠谱
时间戳校验不是“有没有”,而是“准不准”;nonce 不是“存没存”,而是“过期没过期”:
-
timestamp必须从请求中读(如$request->param('timestamp')),不能用time()直接比——服务器时钟偏差会导致误拒。容错建议:用abs((int)$client_ts - time()) > 300 -
nonce必须存在 Redis,key 形如nonce:{$app_id}:{$nonce},TTL 设为 300 秒(不是永久)。写入即过期,不查旧值 - 别把
timestamp放 URL 里——CDN 或代理可能缓存,导致签名永远失效 - 连续 5 次验签失败,建议用 Redis 计数器临时限流该
app_id或 IP
错误响应和调试该怎么处理
返回信息越少,攻击者越难反推你的逻辑:
- 验签失败一律返回 HTTP 401 + 统一 JSON:
{"code":401,"msg":"Unauthorized"},绝不暴露是 timestamp 超时、nonce 重复还是签名错 - 线上环境禁止记录完整请求参数到日志,尤其要脱敏
password、token、id_card等字段 - 调试阶段可加
debug_sign=1参数,但必须用env('APP_DEBUG')包裹,上线前删掉 - 密钥绝不能硬编码,配置项名要明确,比如
app.aes_secret_key,生产环境从环境变量读取
最易被忽略的是参数编码一致性——客户端用 rawurlencode() 编码 value,服务端也必须用它还原;哪怕一个空格、一个换行、一个隐藏 Unicode 字符,都会让 hash_hmac 结果完全不同。
大量免费API接口:立即使用
涵盖生活服务API、金融科技API、企业工商API、等相关的API接口服务。免费API接口可安全、合规地连接上下游,为数据API应用能力赋能!











