签名验证中间件必须在请求体解析前执行,通过$request->getContent()安全读取原始body,按约定顺序拼接签名原文,先校验timestamp和nonce再比对签名,并确保验签在限流之前完成。

签名验证中间件怎么写才不被绕过
签名验证必须在请求体解析前完成,否则 $request->all() 可能触发 Laravel 的自动 JSON 解析或表单解析,导致原始 raw body 丢失,签名计算失效。关键点是直接读取 file_get_contents('php://input'),且不能依赖 $request->input() 或 $request->query() 拼接参数——它们顺序不可控、空值处理不一致、编码不统一。
实操建议:
- 在中间件构造函数里禁用 Laravel 的自动请求解析:设置
$request->enableHttpMethodParameterOverride()不影响,但务必避免调用任何触发getContent()或all()的方法 - 签名原文拼接顺序必须严格按文档约定(如:HTTP 方法 + URI + query string(按 key 字典序)+ raw body(非 decoded)+ timestamp + nonce)
- timestamp 有效性检查要放在签名比对前,防止重放攻击;建议误差窗口 ≤ 300 秒,且服务器时间需 NTP 同步
- nonce 需存入 Redis 并设 TTL(略大于 timestamp 窗口),拒绝已存在的 nonce —— 注意 Redis key 要带用户标识前缀,避免跨账号冲突
Laravel 10+ 中间件里如何安全读取原始 body
Laravel 默认会缓存 php://input 内容,但多次调用 file_get_contents('php://input') 在某些 SAPI(如 PHP-FPM)下返回空字符串。正确做法是只读一次,并手动保存到 request 实例中供后续使用。
实操建议:
- 在中间件开头立即执行:
$rawBody = $request->getContent();(Laravel 封装了兼容逻辑,比直接file_get_contents更可靠) - 若需保留原始 body 给下游控制器使用,需重新绑定 request:
$request = $request->replace(['_raw_body' => $rawBody]);,然后在控制器里通过$request->get('_raw_body')获取 - 不要在中间件里调用
$request->json()或$request->post(),它们会触发内部解析并清空原始流 - GET 请求无 body,签名原文中 body 部分应为空字符串,而非
null或未定义
防刷限流该用 Redis 还是数据库
高频接口防刷必须用 Redis,数据库写入延迟高、连接数瓶颈明显,且难以原子性实现“计数+过期”操作。Laravel 的 RateLimiter 默认基于 cache,但默认驱动(如 file)完全不适用生产环境。
实操建议:
- 强制配置
CACHE_DRIVER=redis,并在config/cache.php中确认redis连接正常;避免误用array或file驱动测试上线 - 限流 key 应包含客户端标识维度:推荐组合为
"rate_limit:{$ip}:{$api_route}:{$user_id}",其中$user_id从 token 解析得来,未登录则用 IP 替代 - 慎用 Laravel 自带的
throttle:60,1—— 它默认以 IP 为 key,无法区分同一 IP 下不同用户,易造成误杀;应自定义RateLimiter::attempt()逻辑 - Redis 原子计数建议用
INCR+EXPIRE组合,而非SETNX,避免竞态条件;Laravel 10.28+ 的Cache::lock()可用于更复杂场景,但增加延迟
签名与限流中间件的执行顺序很关键
必须先验签再限流。如果限流逻辑放在签名验证前,攻击者可构造无效签名反复触发限流计数,导致正常用户被误封;反之,若签名通过后再限流,就能确保只有合法请求参与频率统计。
实操建议:
- 在
app/Http/Kernel.php的$middlewareGroups['api']中,把签名中间件(如EnsureApiSignature)放在ThrottleRequests之前 - 不要在同一个中间件里混写签名和限流逻辑——职责耦合会导致复用困难、测试成本高、错误定位模糊
- 开发时用
dd()或日志记录中间件执行顺序,确认HandleCors、EncryptCookies等前置中间件不干扰原始 body 读取 - 测试时用
curl -X POST -H "Content-Type: application/json" --data '{"a":1}' http://localhost/api/test模拟真实请求,避免用 tinker 或单元测试绕过中间件链


















