必须放在中间件,不能在控制器中执行;因控制器已进入业务逻辑层,路由匹配和参数解析完成,恶意请求可能已触发日志或数据库操作,而中间件是请求进入业务前的唯一可控拦截点。

签名验证该放在哪个环节执行
ThinkPHP 的 API 签名验证必须放在路由调度前,否则控制器里做就晚了——攻击者可能绕过业务逻辑直接触发敏感操作。推荐在 app/middleware/SignVerifyMiddleware.php 中实现,然后全局或按路由组注册。
注意:不能放在 app/common.php 或控制器的 initialize() 方法里,前者无请求上下文,后者已进入业务层,防御滞后。
- 全局注册:在
app/middleware.php中加入'app\middleware\SignVerifyMiddleware' - 按路由组注册:在路由定义中用
->middleware(SignVerifyMiddleware::class) - 务必排除登录、注册等无需签名的接口,否则新用户连不上
签名算法怎么写才防重放和篡改
标准做法是拼接非空参数(含 timestamp、nonce、appid)、按字典序排序后,用密钥 HMAC-SHA256 加密。关键点不是“怎么算”,而是“哪些字段必须参与且不可被绕过”。
常见错误是只对 GET 参数签名,忽略 POST body;或者没校验 timestamp 有效期(建议 ≤ 300 秒),导致重放攻击有效。
立即学习“PHP免费学习笔记(深入)”;
-
timestamp必须是当前 Unix 时间戳,服务端需校验与本地时间差 ≤ 300 秒 -
nonce需存入 Redis(带 TTL),防止重复使用,key 可设为"nonce:{$appid}:{$nonce}" - 所有请求参数(GET + POST JSON/body)都需合并解析后再排序拼接,不能漏掉
Content-Type: application/json下的原始 body - 签名字符串示例:
appid=abc&nonce=xyz×tamp=1717023456&v=1.0(注意无空格、无编码、小写键名)
如何安全地获取和比对签名值
签名通常放在 Header(如 X-Signature)或 query string(如 ?sign=xxx),但 Header 更规范。千万别从 $_GET 或 $request->param() 直接取 sign 字段——它可能被业务参数污染。
ThinkPHP 的 $request->header('X-Signature') 是唯一可信入口,且必须在中间件早期就提取,避免后续逻辑修改 header 或 request 对象。
- 先调用
$request->header('X-Signature')获取签名,再手动解析参数(不要用$request->param()) - POST JSON 请求需用
$request->getContent()读原始 body,再json_decode($raw, true)合并到参数数组 - 比对时用
hash_equals($expected, $provided)防时序攻击,ThinkPHP 8.x 已内置,低版本需自行引入 - 密钥不硬编码,从配置读取:
config('api.sign_key'),生产环境应由 Env 注入
调试时常见的 401 错误怎么快速定位
返回 401 Unauthorized 但没说明原因,是签名中间件最烦人的体验。根本原因是中间件里抛异常后未携带上下文信息,而 ThinkPHP 默认会吞掉 detail。
解决办法是在中间件中捕获异常时,主动记录日志并返回结构化错误响应,而不是依赖默认异常页。
- 在中间件 catch 块中加
Log::error("Sign verify failed: {$msg}", ['raw' => $request->param(), 'header' => $request->header()]) - 返回响应统一用
json(['code' => 401, 'msg' => 'Invalid signature'], 401) - 前端调试时,用
curl -H "X-Signature: xxx" "http://api.test/v1/user?appid=test×tamp=1717023456&nonce=abc"手动构造最直观 - 注意开发环境开启
app_debug = true,否则Log::error可能不写磁盘
签名验证真正的复杂点不在算法本身,而在于参数来源的完整性、时间同步的容忍度、以及密钥轮换时的灰度兼容——这些往往要等上线后才暴露。



















