签名校验中间件应注册在 $middlewareGroups['api'] 中或按需绑定到具体路由,避免放入全局 $middleware 栈;其实现需校验时间戳、nonce 和签名原文(含 body 或排序后 query),密钥从配置读取,原始请求体用 $request->getContent() 安全获取,并通过 Redis 防重放。

签名校验中间件该放在哪里注册
中间件必须注册在 $middleware、$middlewareGroups 或路由级,但签名校验通常属于「API 请求前置校验」,不该全局启用(比如登录页、静态资源不需要),推荐注册到 $middlewareGroups['api'] 里,或按需绑定到具体路由组:
- 若所有 API 都要校验,往
app/Http/Kernel.php的$middlewareGroups['api']数组末尾加\App\Http\Middleware\VerifyApiSignature::class - 若只对部分接口生效,直接在路由定义中加:
Route::post('/webhook')->middleware(VerifyApiSignature::class) - 别注册进
$middleware全局栈——会拦截/storage、/vendor等非业务路径,导致 404 或签名失败
签名算法实现要点(时间戳 + 非对称加密常见组合)
实际项目中多数用「HMAC-SHA256 + 时间戳 + 随机 nonce」,而不是 RSA 签名(太重)。关键不是“怎么加密”,而是“怎么防重放、防篡改、防伪造”:
- 客户端必须传三个必要 header:
X-Signature(签名值)、X-Timestamp(秒级或毫秒级时间戳)、X-Nonce(一次性随机字符串) - 服务端先校验
X-Timestamp是否在允许偏差内(如 ±300 秒),超时直接拒掉,避免重放攻击 - 签名原文应为:
$timestamp.$nonce.$requestBody(注意:GET 请求无 body,就用$timestamp.$nonce.$query,且 query 必须按 key 字典序拼接) - 密钥不硬编码,从
config('api.secret')或env('API_SECRET')读取,开发环境可设为空字符串跳过校验
中间件里怎么安全地读取原始请求体
Laravel 默认会在第一次调用 $request->all() 或 $request->input() 时解析并缓存 body,但签名校验必须在解析前拿到原始字节流,否则签名原文和实际 body 不一致:
- 用
$request->getContent()获取原始 body(返回 string),不要用$request->body()(不存在)或$request->json()->all()(已解析) - GET 请求没有 body,签名原文应基于
$request->fullUrlWithQuery([])去掉 URL 中的 signature/timestamp/nonce 参数后拼接,再排序 query string - 如果用了
FormRequest或前端发application/x-www-form-urlencoded,需手动urldecode(file_get_contents('php://input'))并规范格式,否则空格变+、特殊字符乱码会导致验签失败
验签失败时怎么返回清晰错误而不暴露细节
直接 return response('Unauthorized', 401) 太笼统,前端无法区分是密钥错、时间超时还是 nonce 重复;但也不能返回 "Invalid signature: HMAC mismatch" 这种提示,等于告诉攻击者校验逻辑:
- 统一返回
401 Unauthorized,body 为{"message": "Invalid request"}(不提 signature、timestamp、nonce 任一关键词) - 记录详细日志到
storage/logs/laravel.log,包含$request->header('X-Timestamp')、$request->header('X-Nonce')、计算出的期望签名、实际签名,仅限 debug 环境输出 - 对高频失败 IP 做简单限流(例如 5 分钟内 10 次失败就
sleep(2)),防暴力试探密钥
最易被忽略的是:没校验 X-Nonce 是否已使用过。建议用 Redis 存 cache()->put("nonce:{$nonce}", true, 300),存在即拒绝,这是防重放的关键一环。


















