Yii框架不提供接口签名验证功能,需自行实现;Bearer Token仅认证身份,无法防止请求重放或篡改,必须结合时间戳、nonce、参数排序与密钥摘要等机制进行签名校验,且应通过ActionFilter在beforeAction中前置执行。

Yii 框架本身不提供接口签名验证(sign)功能,必须自己实现;直接套用 HttpBearerAuth 或 QueryParamAuth 只能做 token 认证,无法校验请求体、时间戳、nonce、签名字段是否被篡改。
为什么不能只靠 Bearer Token?
Bearer Token 解决的是“你是谁”,但不解决“这个请求是不是被中间人重放或篡改过”。比如攻击者截获一次合法请求,稍作修改(如把金额从 100 改成 1000)再原样重发,只要 token 还有效,服务端就会执行——除非你额外加签名机制。
签名验证的核心是:客户端和服务端用约定好的规则(参数排序 + 时间戳 + nonce + 密钥)生成摘要,服务端收到后复现一遍,比对结果是否一致。
- 常见错误是只对 URL 参数签名,忽略 POST body(尤其是 JSON)
- 没校验
timestamp是否超时(建议 ≤ 5 分钟),导致重放攻击可行 - 没检查
nonce是否已存在 Redis(防重复提交),或用时间戳代替 nonce(不安全) - 密钥硬编码在代码里,或通过 HTTP 传密钥(绝对禁止)
怎么在 Yii2 中插入签名验证逻辑?
别改 authenticator 行为类,那是给 token 用的。签名验证属于「请求前置校验」,应放在 beforeAction() 或自定义 Filter 里,且必须在路由解析之后、action 执行之前运行。
推荐做法:写一个 SignatureFilter 类,继承 \yii\base\ActionFilter,在 beforeAction() 中提取并校验签名:
public function beforeAction($action)
{
$request = \Yii::$app->request;
$params = array_merge($request->get(), $request->getBodyParams());
$timestamp = $params['timestamp'] ?? null;
$nonce = $params['nonce'] ?? null;
$sign = $params['sign'] ?? null;
<pre class="brush:php;toolbar:false;">if (!$timestamp || !$nonce || !$sign) {
throw new \yii\web\BadRequestHttpException('Missing required sign params');
}
// 校验时间戳
if (abs(time() - (int)$timestamp) > 300) {
throw new \yii\web\UnauthorizedHttpException('Timestamp expired');
}
// 校验 nonce 是否已用过(Redis key: sign:nonce:{nonce},TTL 300s)
$cacheKey = 'sign:nonce:' . $nonce;
if (\Yii::$app->cache->exists($cacheKey)) {
throw new \yii\web\ForbiddenHttpException('Nonce reused');
}
\Yii::$app->cache->set($cacheKey, 1, 300);
// 拼接待签名字符串(按字典序排序 key,不含 sign 本身)
ksort($params);
unset($params['sign']);
$strToSign = http_build_query($params) . \Yii::$app->params['apiSecret'];
if ($sign !== hash_hmac('sha256', $strToSign, \Yii::$app->params['apiSecret'])) {
throw new \yii\web\UnauthorizedHttpException('Invalid signature');
}
return true;}
- 注意:JSON 请求体需提前 decode 成数组,
$request->getBodyParams()默认只处理application/x-www-form-urlencoded -
apiSecret必须存在config/params.php中,绝不能暴露在 URL 或日志里 - 如果用 Nginx,确保
client_max_body_size足够大,否则 JSON body 会被截断,签名必然失败
签名和 Bearer Token 能不能一起用?
可以,而且推荐:token 管身份,签名管防篡改和重放。两者是正交的,互不干扰。
关键点在于执行顺序和作用域:
-
HttpBearerAuth在authenticator行为中运行,负责设置\Yii::$app->user->identity -
SignatureFilter应配置在behaviors()的authenticator之后(或独立注册),它不依赖用户登录态,甚至可在未登录时运行(比如开放注册接口) - 不要在
findIdentityByAccessToken()里塞签名逻辑——那会污染认证流程,且无法覆盖匿名接口
配置示例:
public function behaviors()
{
return [
'authenticator' => [
'class' => \yii\filters\auth\HttpBearerAuth::class,
],
'signature' => [
'class' => \app\filters\SignatureFilter::class,
],
];
}
容易被忽略的兼容性细节
签名验证看着简单,但线上最容易翻车的是环境和协议层问题:
- Apache 下,
$_SERVER['QUERY_STRING']和parse_str()对空值、+ 号、编码的处理和 Nginx 不一致,建议统一用$request->get()+$request->getBodyParams() - cURL 测试时,如果用
-d '{"a":1}'但没设-H "Content-Type: application/json",Yii 默认不会解析 JSON body,getBodyParams()返回空数组 - 前端 JS 用
fetch发 JSON 请求,body: JSON.stringify(...)后必须手动加headers: {'Content-Type': 'application/json'},否则签名字符串漏掉 body 内容 - 移动端 SDK 如果自动添加
X-Requested-With或其他 header,而你的签名规则没包含这些字段,就会校验失败——签名规则文档必须明确声明参与签名的字段范围
真正难的不是写几行 hash 代码,而是让所有客户端严格遵守同一套参数组装与编码规范,并在服务端稳定还原出来。一旦某端用了不同排序逻辑或 URL 编码方式,签名就永远对不上。


















