代码 接口签名校验失败:多数问题不在算法,而在签名原串

2026-09-11 09:01:44

接口签名校验失败:多数问题不在算法,而在签名原串

签名报错的现场通常是这样的:同一个接口,抓包看参数一个不少,密钥也换了三遍,服务端还是回 401 或者 SignatureFailure。难点不在写 HMAC 还是 RSA,而在双方对「签名原串」的理解不一致——排序、编码、时间戳、字段名,差一个字符结果就完全不同。

前提与不适用场景

可以照这套流程走的情况:

  • 服务端给了具体错误,比如 401、AuthFailure.SignatureFailure、验签失败,不是笼统的「请求失败」
  • 有接口文档能确定签名串的构造规则,或者能抓到一条已知可用的请求当参考值
  • 能改代码打日志、能重放请求

不太适用的情况:

  • 对方只回「失败」,不给错误码、不给文档,抓包也拿不到可重放的请求,只能靠猜
  • sign 依赖运行时上下文(WASM、设备指纹、performance.timeOrigin 之类),静态复刻的成本往往高于直接跑原环境
  • 有官方文档的云厂商接口,先别急着猜算法,先对着文档逐行核

先分清是「算错」还是「没对上」

微信支付的说法很直接:出现 401 或签名错误,就是签名计算错误(官方排查文档)。不用怀疑网络层、证书链、IP 白名单,那些会报别的错。所以排查方向只有一个:把你的签名原串和服务端期望的原串对齐。

对齐的手段是双端打印中间值,具体放在后面「通用做法」里说。

微信支付 V3:六处最常见的签名错误

按官方文档的顺序,这几处按出错频率排下来差不太多:

  1. 服务商模式用错私钥。服务商必须用自己的商户 API 私钥,不是子商户私钥。
  2. 私钥文件选错。要的是 apiclient_key.pem。常见错法是误用 apiclient_cert.pem——这两个文件名太像了;另一种是误用平台证书 wechatpay.pem 里的公钥。
  3. 三者不对应。私钥、商户号 mchid、商户 API 证书序列号 serial_no 必须一一对应。序列号可以用 openssl 从证书里看:
openssl x509 -in apiclient_cert.pem -noout -serial

Authorization 头的格式:

WECHATPAY2-SHA256-RSA2048 mchid=..,nonce_str=..,signature=..,timestamp=..,serial_no=..
  1. 头里的 nonce_str / timestamp 和算签名时用的不一致。这两个值在 header 和签名串里各出现一次,值必须相同。拼装顺序写反、或者生成两份随机串,都会挂。
  2. 签名串格式。这一条错得最多:
  • \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
  1. 代码转义问题。前面都对还是报错,大概率是语言层面把字符串处理歪了。官方给的办法很实用:用你自己的代码,加上文档示例里的密钥和示例请求算一遍,把结果和文档给出的示例签名值对比。对不上,问题就在你的代码里,跟真实密钥无关。

微信支付 V2:原值签名,别提前 URLencode

V2 是另一套规则,坑集中在编码和拼串上(以下为社区通行说法,未逐条实测):

  • 密钥固定 32 位,长了短了都无效
  • 请求参数编码和签名串编码统一 UTF-8
  • 签名原串必须用参数原值。微信支付要求用原值签名,先 URLencode 再签就错了
  • sign_type 不传默认 MD5,少数接口只支持 HMAC-SHA256
  • 参数名大小写严格按文档来
  • 参数值里出现 ^&、空格,或者长度超长,都容易出问题
  • 签名原串里的 XML 参数必须和实际请求里的 XML 完全一致;有些接口本来没有 nonce_str,自己补一个进去,签名直接错

腾讯云 API:按 ASCII 排参数名,编码别做两遍

腾讯云的规则写在签名方法文档里,几个点值得单独拎出来:

排序只按参数名。 按参数名字典序(ASCII)升序排,值不参与比大小。InstanceIds.2 排在 InstanceIds.12 之后——因为比较到 . 后面是字符 21,按 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.SecretIdNotFoundSecretId 未找到
AuthFailure.SignatureFailure签名校验失败
AuthFailure.InvalidSecretIdSecretId 无效

华为云 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 字段名不固定。 signsignature_signsigtoken_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.jscrypto.js

控制变量推导拼接格式。 只改一个参数、去掉 timestamp、调整参数顺序,看 sign 变不变。常见拼接就三种:

k=v&k=v                -> a=1&b=2
value1value2           -> 12
key1value1key2value2   -> a1b2

盐值可以加在最前、最后或者中间,位置不同结果完全不同。

时间戳精度。 秒级和毫秒级必须和对方一致,混用一定错。有电商类案例是服务端时间差要求在 3 秒或 1500ms 以内(社区通行说法,未逐条实测)。

常见坑清单:

  • 参数排序错
  • 编码不统一(UTF-8 / URLencode)
  • 时间戳精度
  • 隐藏字段:header 里的 deviceIdclientType 也参与签名
  • 大小写:MD5 结果是大写还是小写
  • 空值:key= 和完全不出现 key,结果不一样
  • Cookie / sessionId 参与签名

进阶对抗。 AST 反混淆、hook CryptoJS.MD5、用 Node 直接调用原 JS 函数、PyExecJS。极端情况下 sign 依赖 WASM、设备指纹、performance.timeOrigin 这类运行时上下文,纯 Python 复刻基本不可行(属社区经验)。

工程做法。 把签名层拆成独立模块,用固定输入写好单测,断言结果等于历史抓包值。接口升级时先跑一遍测试,能立刻知道是签名变了还是业务变了。

通用做法:双端打印中间值

不管是哪家的接口,最后都落到同一件事上——把两端的中间值打出来逐字节比(以下为社区通行说法,未逐条实测):

  • 原始参数键值对
  • 排序后的字符串
  • 编码转换后的结果
  • 最终签名值

四个点各打一次,差异出在哪一步一目了然。只在最后打一个签名值,等于什么都没打。

用 Postman 构建一个「黄金请求模板」,固定 timestamp 和 nonce,把时间因素排除掉,先确认算法和串格式正确,再换成动态值。

三类高频原因,按出现频率排:参数顺序、编码格式、时间戳有效性与时间同步。最后一类经常被忽略——服务器 NTP 不同步,会导致一批请求同时验签失败,代码一行没改却突然全挂,先看机器时间。

参考资料

复制全文 生成海报 接口对接 签名 微信支付 腾讯云 API

推荐文章

程序员茄子在线接单