接口签名校验失败:多数问题不在算法,而在签名原串
签名报错的现场通常是这样的:同一个接口,抓包看参数一个不少,密钥也换了三遍,服务端还是回 401 或者 SignatureFailure。难点不在写 HMAC 还是 RSA,而在双方对「签名原串」的理解不一致——排序、编码、时间戳、字段名,差一个字符结果就完全不同。
前提与不适用场景
可以照这套流程走的情况:
- 服务端给了具体错误,比如 401、
AuthFailure.SignatureFailure、验签失败,不是笼统的「请求失败」 - 有接口文档能确定签名串的构造规则,或者能抓到一条已知可用的请求当参考值
- 能改代码打日志、能重放请求
不太适用的情况:
- 对方只回「失败」,不给错误码、不给文档,抓包也拿不到可重放的请求,只能靠猜
- sign 依赖运行时上下文(WASM、设备指纹、
performance.timeOrigin之类),静态复刻的成本往往高于直接跑原环境 - 有官方文档的云厂商接口,先别急着猜算法,先对着文档逐行核
先分清是「算错」还是「没对上」
微信支付的说法很直接:出现 401 或签名错误,就是签名计算错误(官方排查文档)。不用怀疑网络层、证书链、IP 白名单,那些会报别的错。所以排查方向只有一个:把你的签名原串和服务端期望的原串对齐。
对齐的手段是双端打印中间值,具体放在后面「通用做法」里说。
微信支付 V3:六处最常见的签名错误
按官方文档的顺序,这几处按出错频率排下来差不太多:
- 服务商模式用错私钥。服务商必须用自己的商户 API 私钥,不是子商户私钥。
- 私钥文件选错。要的是
apiclient_key.pem。常见错法是误用apiclient_cert.pem——这两个文件名太像了;另一种是误用平台证书wechatpay.pem里的公钥。 - 三者不对应。私钥、商户号
mchid、商户 API 证书序列号serial_no必须一一对应。序列号可以用 openssl 从证书里看:
openssl x509 -in apiclient_cert.pem -noout -serial
Authorization 头的格式:
WECHATPAY2-SHA256-RSA2048 mchid=..,nonce_str=..,signature=..,timestamp=..,serial_no=..
- 头里的 nonce_str / timestamp 和算签名时用的不一致。这两个值在 header 和签名串里各出现一次,值必须相同。拼装顺序写反、或者生成两份随机串,都会挂。
- 签名串格式。这一条错得最多:
\n是换行符本身,不是反斜杠加字母 n- 第一行的请求方法必须大写,
GET/POST/PUT,小写不行 - 一共 5 行,每行以换行结尾;GET 请求第五行是空行,空行加换行不能漏
- 第二行 URL 不带域名,写
/v3/certificates,不是https://api.mch.weixin.qq.com/v3/certificates - 不要出现
//v3/certificates这种多余斜杠
GET 的签名串长这样:
GET\n
/v3/certificates\n
\n
\n
\n
- 代码转义问题。前面都对还是报错,大概率是语言层面把字符串处理歪了。官方给的办法很实用:用你自己的代码,加上文档示例里的密钥和示例请求算一遍,把结果和文档给出的示例签名值对比。对不上,问题就在你的代码里,跟真实密钥无关。
微信支付 V2:原值签名,别提前 URLencode
V2 是另一套规则,坑集中在编码和拼串上(以下为社区通行说法,未逐条实测):
- 密钥固定 32 位,长了短了都无效
- 请求参数编码和签名串编码统一 UTF-8
- 签名原串必须用参数原值。微信支付要求用原值签名,先 URLencode 再签就错了
sign_type不传默认 MD5,少数接口只支持 HMAC-SHA256- 参数名大小写严格按文档来
- 参数值里出现
^、&、空格,或者长度超长,都容易出问题 - 签名原串里的 XML 参数必须和实际请求里的 XML 完全一致;有些接口本来没有
nonce_str,自己补一个进去,签名直接错
腾讯云 API:按 ASCII 排参数名,编码别做两遍
腾讯云的规则写在签名方法文档里,几个点值得单独拎出来:
排序只按参数名。 按参数名字典序(ASCII)升序排,值不参与比大小。InstanceIds.2 排在 InstanceIds.12 之后——因为比较到 . 后面是字符 2 和 1,按 ASCII 比 1 更小。任何按数值大小或自然排序的实现都会排错。
格式化为 参数名=参数值,值用原始值,不是 url 编码后的值。
names = sorted(params.keys()) # 按参数名 ASCII 升序
raw = "&".join(f"{k}={params[k]}" for k in names)
签名串需要 URL 编码。 GET 或 POST + application/x-www-form-urlencoded 时,所有参数值都要 URL 编码,键和 = 不编码。
双重编码。 部分语言的网络库会自动 urlencode,你再手动编一次,值就被编了两遍,签名必错。
RFC3986 细节。 空格必须编成 %20 而不是 +;%XY 里的十六进制字母用大写 A-F,小写会报错;非 ASCII 字符先转 UTF-8 再编码。
错误码可以快速定位阶段:
| 错误码 | 说明 |
|---|---|
AuthFailure.SignatureExpire | 签名过期 |
AuthFailure.SecretIdNotFound | SecretId 未找到 |
AuthFailure.SignatureFailure | 签名校验失败 |
AuthFailure.InvalidSecretId | SecretId 无效 |
华为云 API:CanonicalRequest 的六段结构
华为云的签名指南把规范请求定义成六段拼接:
CanonicalRequest =
HTTPRequestMethod + '\n' +
CanonicalURI + '\n' +
CanonicalQueryString + '\n' +
CanonicalHeaders + '\n' +
SignedHeaders + '\n' +
HexEncode(Hash(RequestPayload))
容易错的地方:
- 查询串按参数名升序排;参数值可以为空,但
=不能省略,写成parm1=value1&parm2= X-Sdk-Date必须参与签名,格式是 ISO8601 UTC。网关会校验时间差,超过 15 分钟直接拒绝- 客户端要和时钟服务器同步。机器时间漂了十几分钟,本地算得再对也过不了网关
逆向别人家接口的 sign
这套是社区多篇文章里的经验汇总,未逐条实测,只适合没有文档、必须硬推的场景。
sign 字段名不固定。 sign、signature、_sign、sig、token、_tb_token_、__aes__、sign_v2 都见过。可靠的锚点是:随每次请求变化 + 缺失就直接报错 + 与请求体强相关。三个条件都满足的字段,基本就是它。
长度是个线索:
| 形态 | 可能的算法 |
|---|---|
| 32 位十六进制 | MD5 |
| 40 位 | SHA1 |
| 64 位 | SHA256 |
结尾带 = | Base64,可能是 AES 或 RSA |
定位手段。 Chrome DevTools 全局搜 sign=、md5(、sha1(、sha256(、encrypt(;在 XHR/fetch 上打断点;找公共 request 封装(axios/fetch 拦截器)、utils.js、crypto.js。
控制变量推导拼接格式。 只改一个参数、去掉 timestamp、调整参数顺序,看 sign 变不变。常见拼接就三种:
k=v&k=v -> a=1&b=2
value1value2 -> 12
key1value1key2value2 -> a1b2
盐值可以加在最前、最后或者中间,位置不同结果完全不同。
时间戳精度。 秒级和毫秒级必须和对方一致,混用一定错。有电商类案例是服务端时间差要求在 3 秒或 1500ms 以内(社区通行说法,未逐条实测)。
常见坑清单:
- 参数排序错
- 编码不统一(UTF-8 / URLencode)
- 时间戳精度
- 隐藏字段:header 里的
deviceId、clientType也参与签名 - 大小写:MD5 结果是大写还是小写
- 空值:
key=和完全不出现key,结果不一样 - Cookie / sessionId 参与签名
进阶对抗。 AST 反混淆、hook CryptoJS.MD5、用 Node 直接调用原 JS 函数、PyExecJS。极端情况下 sign 依赖 WASM、设备指纹、performance.timeOrigin 这类运行时上下文,纯 Python 复刻基本不可行(属社区经验)。
工程做法。 把签名层拆成独立模块,用固定输入写好单测,断言结果等于历史抓包值。接口升级时先跑一遍测试,能立刻知道是签名变了还是业务变了。
通用做法:双端打印中间值
不管是哪家的接口,最后都落到同一件事上——把两端的中间值打出来逐字节比(以下为社区通行说法,未逐条实测):
- 原始参数键值对
- 排序后的字符串
- 编码转换后的结果
- 最终签名值
四个点各打一次,差异出在哪一步一目了然。只在最后打一个签名值,等于什么都没打。
用 Postman 构建一个「黄金请求模板」,固定 timestamp 和 nonce,把时间因素排除掉,先确认算法和串格式正确,再换成动态值。
三类高频原因,按出现频率排:参数顺序、编码格式、时间戳有效性与时间同步。最后一类经常被忽略——服务器 NTP 不同步,会导致一批请求同时验签失败,代码一行没改却突然全挂,先看机器时间。
参考资料
- 微信支付:报错状态码 401 / 错误的签名 / 验签失败 —— https://pay.weixin.qq.com/doc/v3/merchant/4012365347
- 腾讯云:签名方法 —— https://cloud.tencent.com/document/api/628/45175
- 华为云:API 签名指南 —— https://support.huaweicloud.com/devg-apisign/apisign-devg-pdf