签名验证必须在入口层拦截,即框架路由解析前完成,Laravel应在Kernel.php中间件数组靠前位置添加自定义中间件,原生PHP需在index.php顶部读取原始请求数据并校验X-Timestamp时效性、签名原文组成及hash_hmac('sha256')安全性,同时统一header处理、避免代理截断、配合IP白名单与Redis滑动窗口限流。

签名验证必须在入口层拦截,不能等路由分发后才做
PHP 接口被刷、被重放、被篡改,根本原因往往是签名校验逻辑混在业务代码里,甚至写在控制器方法内部。这时候攻击者绕过验证的成本极低——比如直接调用某个 service 方法,或伪造一个已登录的 session 绕过中间件。
正确做法是把签名验证作为「请求生命周期的第一道门」,在框架路由解析前完成。Laravel 可用 App\Http\Middleware\TrustProxies 后、Kernel.php 的 $middleware 数组靠前位置加自定义中间件;原生 PHP 则应在 index.php 最顶部读取 $_SERVER['REQUEST_METHOD'] 和原始 body(需提前用 file_get_contents('php://input')),再解析 header 中的 X-Signature 和 X-Timestamp。
- 务必校验
X-Timestamp是否在 5 分钟窗口内,防止重放攻击 - 签名原文必须包含:HTTP 方法 + 请求路径 + 排序后的 query string + 排序后的 body JSON 字段(不含空值)+
X-Timestamp - 不要用
$_GET/$_POST拼接参数——它们已被 PHP 自动解码/过滤,会破坏原始签名原文一致性
hash_hmac('sha256', $data, $secret) 是最稳的签名函数,别手写 base64_encode(sha1())
很多老项目还在用 base64_encode(sha1($data . $secret)),这存在两个硬伤:一是 SHA-1 已被证明不安全,二是没密钥混淆机制,攻击者可尝试长度扩展攻击。PHP 原生 hash_hmac() 内置防碰撞性和密钥封装,且支持多种算法,sha256 是当前兼容性与安全性平衡最好的选择。
注意:$secret 必须是服务端强随机生成、不参与传输的密钥,不能和 API Key 混用;每次请求的 $data 字符串必须严格按约定顺序拼接,字段名大小写、空格、换行都要一致。
立即学习“PHP免费学习笔记(深入)”;
- 示例签名生成:
hash_hmac('sha256', $sign_string, $secret, true),第三个参数设为true返回二进制,再用base64_encode()编码传给客户端 - 客户端和服务端必须使用完全相同的字符编码(推荐 UTF-8),否则中文字段会导致签名不一致
- 不要用
md5()或sha1()直接拼密钥——这是教科书级的签名漏洞
curl -H 'X-Signature: xxx' 调用时,header 名称大小写和空格容易出错
PHP 的 getallheaders() 在 Apache 下返回小写 key,在 Nginx + FPM 下可能保留原始大小写。而 $_SERVER 数组里 header 会被转成 HTTP_X_SIGNATURE 形式,下划线替代短横,全大写。如果代码里写 $_SERVER['HTTP-X-SIGNATURE'] 或 getallheaders()['X-signature'],大概率拿不到值。
更隐蔽的问题是:某些代理或 CDN 会自动 strip 掉自定义 header,尤其是带下划线的(如 X_Api_Key)。所以生产环境必须确认网关层是否透传了签名相关 header。
- 统一用
getallheaders()并对 key 做strtolower()处理,再查x-signature和x-timestamp - 测试时用
curl -v看实际发出的请求 header,确认名称和值是否符合预期 - 避免在 header 名中使用下划线,优先用短横线(
X-Signature而非X_Signature)
签名通过后还要校验 IP 白名单和频率限制,单靠签名不够
签名只解决「这个请求是我发的、没被改过」,但不解决「这个人能不能一直发」。见过太多案例:签名逻辑写对了,结果被恶意用户用固定密钥高频刷接口,把数据库拖垮。
IP 白名单适合 B2B 场景(如第三方系统回调),但要注意 NAT 环境下多个客户共用出口 IP;频率限制建议用 Redis 做滑动窗口(redis->zRangeByScore + zRemRangeByScore),而不是简单计数器——后者无法防御突发流量。
- 白名单校验应放在签名之后、业务执行之前,避免无效请求消耗计算资源
- 频率 key 应包含
app_id(如有)+ 客户端 IP + 接口路径,防止某接口被单点打爆 - 不要把限流逻辑写进数据库事务里——Redis 才是唯一靠谱的选择











