接口签名校验不过?先查大小写和空格
签名对了却校验失败,十有八九栽在大小写和空格上:参数名驼峰还是小写、值前后有没有空格、拼接顺序对不对。这篇踩坑实录把最隐蔽的几类问题逐个拆解。
先给一个整体判断:签名不一致,八成出在待签名字符串不一样,而不是摘要算法选错。大小写和空格,正是让这串文本在两边"看起来一样、算出来不一样"的两大来源,其余的坑多数也和它们沾亲带故。
一、签名是怎么算出来的
签名三步:把参与签名的参数按约定规则排序、拼接成一串文本;用密钥对这串文本做摘要,常见算法是 HMAC-SHA256;把摘要转成十六进制字符串随请求发出,服务端用同样流程再算一遍,比对结果。大小写和空格的坑,就分布在这三步和传输途中。
1. 一个最小例子
import hmac, hashlib
params = {"userId": "u1001", "amount": "200"}
raw = "&".join(f"{k}={params[k]}" for k in sorted(params))
sign = hmac.new(key, raw.encode("utf-8"), hashlib.sha256).hexdigest()
hexdigest() 输出小写十六进制。如果服务端用 Java 的 String.format("%X", byte) 转十六进制,得到的是大写。两边算法、密钥、参数完全一致,签名照样对不上,这就是大小写坑的典型现场。
2. 坑位地图
拼接这一步,坑在参数排序对大小写的规则、参数值里的空格;摘要这一步,坑在十六进制输出是大写还是小写;传输这一步,坑在空格被编码成 %20 还是 +、header 名的大小写被网关改写。三步各有雷区,两边约定不清的地方,个个都是坑。
先看清签名是怎么一步步算出来的,后面每个坑才能对号入座。
二、大小写的三个坑
1. 十六进制输出不统一
不同语言、不同库的十六进制转换,默认大小写不一致:Python 的 hexdigest() 出小写,Java 一些格式化写法出大写。直接拿原文做字符串比较,一次都过不了。解法有两条:接口文档写死十六进制统一小写,服务端比较前再做一次 toLowerCase,双保险。别说"对面肯定和我一样",联调现场十个有八个栽在这句话上。
2. 排序规则没写死
ASCII 码表里大写字母排在小写字母前面,A 是 65,a 是 97。参数名里同时有 userName 和 userid 这类混合大小写时,按 ASCII 码排序和按不区分大小写的字典序排序,顺序不同,拼接串不同,签名自然不同。跨语言对接时这个坑最常见,不同语言的 sort 函数对大小写的默认处理并不一样。解法:文档写死参数名按 ASCII 码升序,联调用例里必须放一个带大写字母的参数名,把分歧提前暴露出来。
3. Header 名被网关改写
HTTP header 名本身不区分大小写,Content-Signature 和 content-signature 是同一个头。有些网关、代理会把 header 名统一改写成首字母大写的形式,客户端如果按原样字符串匹配去取签名头,请求一过网关就取不到值。解法:取 header 一律按不区分大小写的方式匹配,取出来的值保持原样参与运算,不做任何顺手的转换。
大小写的坑全都来自"两边规则不一样",规则写进文档、代码里统一归一,这个坑就填平了。
三、空格的三个坑
1. 参数值的首尾空格
表单录入、Excel 复制粘贴,都容易把尾随空格带进参数值,拼进待签名字符串后,肉眼看日志完全发现不了,程序比较就是对不上。解法:拼接前对参数值做 trim,并把"哪些字段允许 trim"写进文档。注意 trim 只动首尾,地址类字段中间的空格有业务含义,不能跟着一起动。
2. %20 和 + 的编码分歧
表单编码 application/x-www-form-urlencoded 里,空格编码成加号;RFC 3986 的规则里,空格编码成 %20。同一个参数值,一方按表单编码、一方按 RFC 3986 编码,待签名字符串从源头就分岔了。更隐蔽的是,同一个系统里 query string 和 body 用了不同的编码函数,自己跟自己都能打起来。解法:文档写死空格按 %20 处理,并明确签名算的是编码前的值还是编码后的值,这两个约定是联调时最容易想当然的地方。
3. 密钥和日志带进来的空格
配置文件里密钥行尾的空格不会报错,复制密钥时带上的换行符,日志工具给长字符串自动加的换行缩进,这些来源都不在签名代码里,排查的人很少第一时间想到。解法:密钥从配置读入后统一 strip;打印待签名字符串时用定界符包起来,比如输出成 [u1001&200],首尾空格在日志里立刻现形。
空格的坑全在"看不见"三个字上,trim 到位、编码写死、日志带定界符,看不见的字符就变成了看得见的。

四、签名不一致时的排查顺序
1. 先比拼接串,再怀疑算法
调试第一步,把两边的待签名字符串完整打出来对一遍,能解决大多数问题。两个串日志里看着一样、程序比较不一样,九成是首尾空格或不可见字符在捣乱。算法和密钥放到最后再怀疑,先查简单的高频原因,排查效率最高。
2. 逐字节比对
肉眼比不出来就上字节级:Python 一行 s.encode("utf-8").hex() 把字符串转成十六进制字节序列,两边各 dump 一份做比对,第一个不同的字节出现在哪个位置、前后是什么内容,一目了然。定位到字节级,是空格、是大小写、还是转义差异,基本能当场判断,不用再猜。
3. 把踩坑现场存成测试向量
出过问题的那组参数、待签名字符串、正确签名值,存成一组固定测试用例,每次回归跑一遍。踩过的坑变成用例留在代码库里,才算真正消化,否则同样的坑换个项目还会再踩一遍。
排查签名问题的正确顺序是:先对拼接串,再上字节级比对,最后才动算法和密钥。
五、把规则写死:文档和代码各做什么
1. 文档里必须写死的五条
排序规则,参数名按 ASCII 码升序;拼接格式,键值对用什么符号连接、空值参不参与;编码规范,空格按 %20;摘要输出,十六进制小写;比较方式,统一转小写后比较。五条少写一条,联调现场就多一片各自想当然的空间,事后补救的成本远高于开头多写两行字。
2. 代码里收拢成一个签名工具
排序、编码、拼接、摘要、大小写归一,全部收进一个独立的工具函数或模块,业务代码只传参数,不自己拼串。客户端和服务端能共用同一份实现最好;跨语言共用不了,就用同一组测试向量保证两边行为一致。签名逻辑散落在各处,等于给未来的自己埋雷。
3. 联调用带陷阱的测试参数
测试参数里故意放四类东西:大写字母的参数名、含空格的参数值、空值、中文值。这四类是两边实现最容易分歧的场景,联调环境提前跑通,生产环境就少熬一夜。
文档写死五条规则,代码收拢一个工具,联调备好带陷阱的用例,签名问题就从事后救火变成了事前排除。

六、低代码平台对接时的签名检查单
用搭贝这类低代码平台做系统对接时,签名从写代码变成填配置:密钥、算法、参数范围、编码规则都是配置项。配置降低了上手门槛,但不改变签名的本质,前文五条文档规则换了个地方核对而已。
1. 配置前先对规则
动手配置前,先和对面系统逐条确认五条规则:排序、拼接、编码、摘要输出、比较方式。平台不会替你猜对面的约定,这一步沟通省不掉,确认清楚再填配置,一次通过。
2. 配置后先跑调试
搭贝的接口集成在调试日志里能看到完整的待签名字符串,把它复制出来,和对面的日志做逐字节比对,排查思路和自研代码完全一致。调试跑通再挂到正式流程上,出问题时才有干净的现场可以回看。
3. 密钥管理的三个动作
密钥粘贴进配置后先跑一次调试,确认没有带入首尾空白;密钥不进代码库、不进群聊;定期轮换,轮换期新旧密钥并行验证,避免一刀切造成业务中断。
平台配置只是换了形式,规则确认、调试比对、密钥管理三件事一件都不能少。

常见问题
Q:想用低代码平台做对接,选型时怎么评估签名这块?
看三点:调试日志能不能看到完整的待签名字符串,密钥配置是否自动去除首尾空白,编码和大小写规则是否可配置。这三点决定了出问题后能不能快速定位。搭贝在接口集成上具备这些能力,平台按用户数报价,可以先在小范围把联调跑通验证,再逐步扩大对接范围。
Q:签名比较用什么方式比较稳妥?
统一转小写后再比较,并且用恒定时间的比较函数,避免时序攻击,多数语言的标准库有现成实现,比如 Java 的 MessageDigest.isEqual。比较之外不要对签名值做多余的格式化,trim 和大小写转换只在约定的位置各做一次。
Q:密钥在存储和交换时要注意什么?
存储放配置中心或环境变量,不进代码库,读入后去掉首尾空白;交换走安全渠道,不在聊天工具里发明文;轮换定期做,新旧密钥并行一段时间再下线旧密钥。三条都做到,密钥环节基本不会出事。
Q:两边都用 HMAC-SHA256,为什么签名还是对不上?
算法相同只保证第三步一致,前两步里排序、拼接、编码任何一处不同,待签名字符串就不一样。排查顺序:先比对拼接串,再查十六进制大小写,最后才怀疑算法和密钥。绝大多数所谓"算法对不上"的问题,最后都落在拼接串上。

接口签名校验失败大多不是算法问题,根源集中在十六进制大小写、参数排序、空格与 URL 编码差异。本文梳理全套坑点,给出字节级排查方案、标准化对接规范,同时附上低代码平台对接的签名配置检查清单。
浙公网安备 33010602011771号