签名验证必须用 file_get_contents('php://input') 而非 $_POST,因后者会自动解析并丢失原始请求体格式,导致 JSON 等场景哈希计算错误;需在中间件禁用 input 自动解析,统一获取 raw body 后处理。

签名验证为什么必须用 file_get_contents('php://input') 而不是 $_POST
因为接口签名通常要求对原始请求体(raw body)做哈希,尤其是 POST JSON 请求——$_POST 会自动解析并丢弃原始格式,还可能触发 Content-Type 不匹配导致为空。一旦签名计算基于错误数据,验证必然失败。
实操要点:
- 所有签名验证逻辑必须在
App\Http\middleware\Authenticate或自定义中间件中,且放在input()之前调用 - ThinkPHP 6+ 默认启用
input自动解析,需手动禁用:在中间件里加$this->app->http->setRequest($request->withInput(false)) - 获取原始数据统一用:
file_get_contents('php://input'),再json_decode($raw, true)(若为 JSON) - 注意:GET 请求无 raw body,签名字段应全部从
$_GET或$request->param()提取,但需约定参数顺序(如按 key 字典序拼接)
sign 字段怎么生成才不被绕过
常见错误是只拼接参数值,忽略 key、排序、分隔符或时间戳校验。攻击者只要重放旧请求 + 修改业务参数就能 bypass。
安全拼接规则:
- 参与签名的字段必须显式声明(如
['appid', 'timestamp', 'nonce', 'data']),禁止用array_diff_key($_POST, ['sign' => ''])这类动态排除 - 所有 key 按字典升序排列,value 强制转字符串(
(string)$v),空值统一作''(不是null或undefined) - 拼接格式固定为:
key1=value1&key2=value2&...&keyN=valueN&key=secret,末尾加密密钥,不可前置 - 必须校验
timestamp:与服务器当前时间差 ≤ 300 秒,且拒绝已用过的nonce(建议 Redis 存 5 分钟)
ThinkPHP 6 中间件里怎么写验证逻辑
别在控制器里做签名检查——容易漏掉、难复用、无法拦截非法请求体。中间件才是正确位置。
示例代码片段(放在 app/middleware/ApiSignCheck.php):
public function handle($request, \Closure $next)
{
$raw = file_get_contents('php://input');
$params = $request->isGet() ? $request->param() : json_decode($raw, true) ?: [];
<pre class="brush:php;toolbar:false;">// 必须字段检查
foreach (['appid', 'timestamp', 'nonce', 'sign'] as $k) {
if (!isset($params[$k])) {
return json(['code' => 400, 'msg' => 'missing '.$k]);
}
}
// 时间戳 & nonce 校验
if (abs(time() - (int)$params['timestamp']) > 300) {
return json(['code' => 401, 'msg' => 'timestamp expired']);
}
if ($this->app->cache->get('nonce_'.$params['nonce'])) {
return json(['code' => 401, 'msg' => 'nonce reused']);
}
$this->app->cache->set('nonce_'.$params['nonce'], 1, 300);
// 签名生成(按 key 排序)
ksort($params);
$str = http_build_query(array_filter($params, function($k) { return $k !== 'sign'; }, ARRAY_FILTER_USE_KEY)) . '&key=your_secret';
if (!hash_equals($params['sign'], md5($str))) {
return json(['code' => 403, 'msg' => 'sign invalid']);
}
return $next($request);}
注意:http_build_query 会自动 URL 编码,和前端保持一致;hash_equals 防时序攻击;array_filter 第三个参数必须显式传 ARRAY_FILTER_USE_KEY,否则删错字段。
调试时 sign 总是不一致?先查这三处
90% 的签名失败不是算法问题,而是环境或协议细节偏差。
- 前端是否把 JSON 字符串又套了一层 JSON?比如
JSON.stringify({data: JSON.stringify(obj)})→ 后端json_decode一次得到字符串,需再 decode 才是数组 - 时间戳单位是不是搞错了?PHP
time()是秒,前端用Date.now()是毫秒,必须除以 1000 并floor - 密钥有没有多空格、换行或 BOM?尤其从配置文件读取时,用
trim(file_get_contents(...))更稳妥
最有效的排查方式:在中间件开头打日志,把拼出的 $str 和实际 $params['sign'] 全打出来对比,别猜。
php免费学习视频:立即使用
踏上前端学习之旅,开启通往精通之路!从前端基础到项目实战,循序渐进,一步一个脚印,迈向巅峰!











