ThinkPHP5.1接口签名校验核心是中间件统一验证timestamp、nonce和sign,关键在于时间窗口控制、nonce防重放及签名算法一致性。使用HMAC-SHA256,参数按ASCII升序拼接后加secret签名,配合Redis缓存nonce并强制HTTPS。

ThinkPHP5.1 实现接口签名校验,核心是统一在请求入口或中间件中验证 timestamp、nonce 和 sign 三要素,防止重放和篡改。关键不在加密多复杂,而在时间窗口控制、随机数防重用、签名算法一致性这三点。
一、约定签名规则(客户端与服务端必须一致)
推荐使用 HMAC-SHA256 签名方式,安全且 TP5.1 原生支持。规则如下:
- 所有请求参数(除
sign外)按字段名 ASCII 升序排列,拼成key1=value1&key2=value2形式(URL 编码不参与拼接,值保持原始) - 拼接固定密钥(如
app_secret),格式为:sorted_params&secret=your_app_secret - 对拼接字符串做
hash_hmac('sha256', $string, $secret),转小写即为 sign - 必须携带
timestamp(秒级时间戳)、nonce(长度 6–16 位随机字符串,建议 UUID 截取)
二、在中间件中统一校验(推荐做法)
新建中间件 app/http/middleware/CheckSign.php:
namespace app\http\middleware;
<p>use think\Request;
use think\Response;</p><p>class CheckSign
{
// 配置密钥和时间窗口(单位:秒)
protected $secret = 'your_app_secret_here';
protected $timeWindow = 300; // 5分钟内有效</p><pre class="brush:php;toolbar:false;">public function handle(Request $request, \Closure $next)
{
$params = $request->param();
$timestamp = $params['timestamp'] ?? 0;
$nonce = $params['nonce'] ?? '';
$sign = $params['sign'] ?? '';
// 基础检查
if (!$timestamp || !$nonce || !$sign || !is_numeric($timestamp)) {
return json(['code' => 400, 'msg' => 'Missing required sign fields'])->code(400);
}
// 时间有效性
$now = time();
if (abs($now - $timestamp) > $this->timeWindow) {
return json(['code' => 401, 'msg' => 'Request expired'])->code(401);
}
// nonce 防重放:存入缓存(如 Redis),设置过期时间略大于 timeWindow
$cacheKey = 'sign_nonce_' . md5($nonce);
if (cache($cacheKey)) {
return json(['code' => 401, 'msg' => 'Nonce reused'])->code(401);
}
cache($cacheKey, true, $this->timeWindow + 60);
// 构造待签名字符串
unset($params['sign']);
ksort($params);
$str = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$expectedSign = hash_hmac('sha256', $str . '&secret=' . $this->secret, $this->secret);
if (!hash_equals($expectedSign, $sign)) {
return json(['code' => 401, 'msg' => 'Invalid signature'])->code(401);
}
return $next($request);
}}
立即学习“PHP免费学习笔记(深入)”;
然后在 app/http/middleware.php 中注册:
return [
'check_sign' => \app\http\middleware\CheckSign::class,
];并在路由或控制器中启用(如全局应用):
// route/route.php
Route::group('api', function () {
Route::rule('user/info', 'api.User/info');
})->middleware('check_sign');三、客户端调用示例(PHP)
确保和服务端完全一致的拼接逻辑:
$params = [
'uid' => 123,
'nickname' => '张三',
'timestamp' => time(),
'nonce' => substr(md5(uniqid()), 0, 12),
];
<p>// 排序 + 拼接(注意:值不 urlencode!)
ksort($params);
$str = http_build_query($params, '', '&', PHP_QUERY_RFC3986);
$sign = hash_hmac('sha256', $str . '&secret=your_app_secret_here', 'your_app_secret_here');</p><p>$params['sign'] = $sign;</p><p>// 发起请求
file_get_contents('<a href="https://www.php.cn/link/3ccbfa49e9dba252180ef3c8db7d5d5d">https://www.php.cn/link/3ccbfa49e9dba252180ef3c8db7d5d5d</a>?' . http_build_query($params));四、增强建议(可选但实用)
-
AppID 隔离:在签名中加入
app_id字段,不同客户端用不同 secret,便于权限隔离和密钥轮换 - 签名白名单:对登录、注册等免签接口,在中间件中通过路由名或 action 名跳过校验
- 日志记录异常:对签名校验失败的请求,记录 IP、nonce、timestamp、URI,辅助排查攻击或调试问题
- HTTPS 强制:签名校验必须配合 HTTPS,否则明文传输 sign、timestamp、nonce 失去意义
不复杂但容易忽略:nonce 缓存要设比 timeWindow 略长的过期时间;参数排序必须严格按 key 名 ASCII 排;所有空格、换行、编码差异都会导致签名不一致。



















