最近我在对接一个数字化项目,后端接口统一走了国密SM2签名:每个业务请求不仅要带登录令牌token,还要在Header里带上签名sign,传输链路还要求走HTTPS。联调阶段后端只丢给我一份接口文档,把测试私钥和appId发过来,让我先在Postman里把接口全跑通。我在Postman里折腾了一整天,把sign和token的生成逻辑全部搬进了预请求脚本,总算是把这条链路完整打通了。
这篇文章就是那天的完整记录。内容包含sm-crypto国密库怎么装进Postman、SM2签名脚本怎么写、token拿到后怎么自动刷新、HTTPS证书怎么配,以及我在联调时遇到的一堆坑和排查方法。适合正在对接国密接口的后端开发、测试同学,也适合被要求用Postman完成联调对接的客户端同学。代码我都贴出来了,照着做基本能把接口通起来。
1. 项目背景与签名方案设计思路
1.1 接口为什么会用SM2签名
接口暴露在公网上,最重要的三件事是:确认调用方身份、保证参数不被篡改、防止请求被恶意重放。token解决的是身份问题,sign解决的是参数完整性和请求唯一性的问题。
SM2是一种基于椭圆曲线的非对称加密算法。所谓非对称,就是签名用的私钥和解密验签用的公钥是分开的:你自己拿私钥对请求内容签名,服务端拿你提交的公钥验签。整个过程相当于你在关键单据上盖了个私章,收件方拿预留的印模去比对。只要私钥不泄露,别人就伪造不了你的签名。
和RSA这类传统非对称算法比,SM2在同等安全强度下密钥更短,256位密钥就能提供足够的保护强度,而且它属于国密算法体系。现在不少项目在选型时直接把SM2写进接口规范,不是可选项而是必选项。对接这类项目时,客户端和服务端必须使用同一套曲线参数和签名规则,才有得聊。这也是为什么我在Postman里做的第一件事,是先确认服务端签名验签用的算法细节,而不是直接开写脚本。
1.2 签名与Token协同的整体链路
从一次完整的业务请求来看,签名和token配合起来的流程是这样的:
- 调用方先拿appId和appSecret(或者私钥签名后的凭证)到授权接口换取token。
- 业务请求发出前,生成本次请求的时间戳timestamp和一次性随机数nonce。
- 按照接口文档约定的规则,把请求方法、路径、查询参数、请求体、时间戳和nonce拼成一个待签名串。
- 用私钥对这段待签名串做SM2签名,得到sign。
- 请求发出时,Header里带上token、sign、timestamp、nonce和appId。
- 服务端先校验token确认身份,再取出公钥验证sign确认请求内容没有被篡改,同时检查timestamp是否在时间窗口内、nonce是否已经用过。
这里其实有两层防护。token失效了只是身份校验过不去,但参数被人截胡改掉,靠token是发现不了的;sign保证了任何一个参数的改动都会导致验签失败。timestamp和nonce则专门对付重放:就算攻击者把整个请求原样复制,时间戳过期或nonce重复一样会被拒绝。
我们可以用一张表把各自的职责理清:
| 机制 | 作用 | 主要防什么 |
|---|---|---|
| token | 标识调用者身份 | 身份伪造 |
| sign | 验证请求内容完整性 | 参数篡改 |
| timestamp | 限定请求有效时间窗口 | 过期重放 |
| nonce | 标记一次性随机数 | 重放攻击 |
明白了整条链路,后面在Postman里写脚本就有方向了:脚本要做的事,无非就是在请求发出前把sign算出来,把token填进去。
1.3 为什么Postman能承担这个任务
很多人觉得Postman就是个“填URL、点发送”的工具,其实它的预请求脚本(Pre-request Script)能力相当强,可以做到在请求真正发出之前执行一段JavaScript,并且在这段脚本里读取当前请求的信息、修改请求头、读写环境变量。这正是动态生成sign所需要的:每个请求的路径和参数不同,sign必须跟着请求内容实时变化,预请求脚本就是干这个的。
再配合环境变量机制,我可以把测试环境和预发布环境的baseUrl、appId、私钥分开配置,切换环境时只需要在Postman右上角点一下。脚本引擎是标准的V8,支持绝大多数ES6语法,写起来和前端开发没什么区别。
当然它也有边界。Postman脚本方案适合功能联调和接口验收,不适合拿来做超大并发的压测;另外私钥这类敏感信息放到本地工具的脚本里,本身是图方便,生产环境的私钥绝对不能这样干。
2. 环境准备与工具链
2.1 需要准备什么
开始之前,先把下面这些东西准备好:
- Postman客户端,建议用较新的版本,脚本能力更完整。
- Node.js环境,这个只是用来提取sm-crypto库文件,提取完基本就不用了。
- 接口文档,重点是签名规则和token获取方式。
- 测试用的appId、私钥(确认是十六进制HEX格式,还是PEM/DER格式)。
- 服务端测试环境的HTTPS地址和证书情况。
私钥格式这里是个容易踩坑的点。sm-crypto这个库做SM2签名时,要求私钥是64位的HEX字符串,也就是32字节的原始私钥。如果后端给的私钥是PKCS#8的PEM文件,或者Java BouncyCastle导出的DER格式,不能直接塞进脚本,需要先转换成HEX字符串。有些接口文档会直接给HEX格式的测试私钥,那就省事了;如果给的是PEM,要么用算法工具转换,要么让后端同学直接导出一份HEX格式的私钥给你。
2.2 把sm-crypto国密库装进Postman
Postman的脚本沙箱不提供Node.js的模块系统,没法在预请求脚本里直接写require('sm-crypto')。社区里通用的做法是:把sm-crypto打包后的浏览器版JS代码,整段塞进Postman的全局变量里,然后在脚本里用eval执行。
步骤很简单:
mkdir sm2-demo cd sm2-demo npm init -y npm install sm-crypto安装完成后,node_modules/sm-crypto/dist/目录下会有一个sm-crypto.min.js文件,这就是我们要的东西。可以用Node把它读出来看看大小:
const fs = require('fs'); const lib = fs.readFileSync('node_modules/sm-crypto/dist/sm-crypto.min.js', 'utf-8'); console.log(lib.length);接下来打开Postman,进入全局变量管理页面,新建一个变量,名字随便起,我习惯叫SM2_LIB。把sm-crypto.min.js文件的完整内容复制进去,保存。注意这个文件是压缩过的,内容是一整行,复制粘贴时不用管格式,直接贴就行。
然后在预请求脚本里这样加载:
const sm2Lib = pm.globals.get('SM2_LIB'); if (!sm2Lib) { throw new Error('请先配置全局变量 SM2_LIB'); } eval(sm2Lib);eval执行完之后,sm-crypto暴露出来的sm2、sm3、sm4这几个对象就挂在当前作用域上了,后面可以直接用。这个方案看着有点粗暴,但确实管用。如果担心global变量太大导致粘贴卡顿,也可以把库文件内容放到一个环境变量里,用法一样。
2.3 HTTPS证书与国密HTTPS的边界
HTTPS这块,Postman的配置分几种情况。
第一种是最常见的开发环境自签名证书。后端联调环境大概率是内网IP加自签名证书,Postman默认校验证书时会直接报错,提示证书无效。解决办法是在设置里关闭SSL证书校验:Settings -> General -> SSL certificate verification,把开关关掉。
第二种是正规的测试环境证书。这种情况不需要关闭校验,正常访问就行。如果报证书链不完整,把服务端证书链里的CA证书导入系统信任列表,或者让运维把证书配完整。
第三种是双向HTTPS认证,也就是服务端要求客户端也提供证书。Postman支持导入客户端证书:Settings -> Certificates -> Add Certificate,填入服务端地址和端口,上传P12/PFX格式的客户端证书文件即可。
有一种情况需要特别说清楚:如果项目要求的是国密SSL,也叫GMTLS,也就是TLS握手过程本身用SM2/SM3/SM4套件,那Postman目前是搞不定的。Postman底层的TLS协议栈走的是标准OpenSSL/Chromium链路,不含国密套件。遇到这种项目,测试阶段建议让运维在网关或负载均衡上做国密套件转换,或者联调环境临时用标准TLS承载业务报文,业务层SM2签名照常进行。就我接触过的实际项目来说,绝大多数“国密SM2+HTTPS”指的是标准HTTPS通道加业务层SM2签名,Postman完全能处理。
3. 核心实操:在Postman里做SM2签名
3.1 先定签名规则,再写脚本
写脚本之前,必须把接口文档里的签名规则吃透。签名规则就是一段固定的“拼串规则”,服务端会按照同样的规则重建待签名串,然后验签。所以脚本里每一步拼接都必须和服务端完全一致,多一个空格、少一个换行,验签就失败。
我这次对接的项目采用的规则是这样的:
HTTPMethod path queryString body timestamp nonce按这个顺序拼起来,中间用换行符\n分隔。其中queryString是请求URL里的查询参数按照key的字典序升序排列后,用key=value的形式拼接,多个参数之间用&连接。body取原始请求体字符串,不是格式化之后的JSON。
选择这种规则是有讲究的。query参数必须排序,是因为客户端和服务端看到的参数顺序可能不同,只有统一排序规则,才能保证双方拼出来的串一致。timestamp和nonce放在串尾,是为了让签名本身绑定这两个动态值,防止签名被整体搬去重放。
有些项目会要求body先做SM3哈希,再把哈希值拼进待签名串,这种做法的好处是body特别长的时候待签名串不会太冗长。具体用哪种,以接口文档为准。下面我的示例按body原样参与签名来写。
3.2 一个能直接跑的SM2签名脚本
下面是一段完整的预请求脚本,适用于单个请求,核心逻辑都写在里面了:
// 1. 加载sm-crypto库 const sm2Lib = pm.globals.get('SM2_LIB'); if (!sm2Lib) { throw new Error('请先配置全局变量 SM2_LIB'); } eval(sm2Lib); // 2. 读取环境变量 const appId = pm.environment.get('app_id'); const privateKey = pm.environment.get('private_key'); if (!appId || !privateKey) { throw new Error('缺少 app_id 或 private_key 环境变量'); } // 3. 生成公共参数 const timestamp = String(Math.floor(Date.now() / 1000)); const nonce = Math.random().toString(36).substring(2) + Date.now().toString(36); // 4. 组装待签名串 const method = pm.request.method.toUpperCase(); const path = pm.request.url.getPath(); // 对query参数做字典序排序 let queryStr = ''; const rawQuery = pm.request.url.getQueryString(); if (rawQuery) { const pairs = rawQuery.split('&').map(function (item) { const idx = item.indexOf('='); const k = idx > -1 ? item.substring(0, idx) : item; const v = idx > -1 ? item.substring(idx + 1) : ''; return { key: decodeURIComponent(k), value: decodeURIComponent(v) }; }); pairs.sort(function (a, b) { return a.key < b.key ? -1 : 1; }); queryStr = pairs .map(function (p) { return p.key + '=' + p.value; }) .join('&'); } const body = pm.request.body ? pm.request.body.raw || '' : ''; const rawString = [method, path, queryStr, body, timestamp, nonce].join('\n'); // 5. SM2签名 const sign = sm2.doSignature(rawString, privateKey, { hash: true, der: false }); // 6. 写入请求头 pm.request.headers.upsert({ key: 'X-App-Id', value: appId }); pm.request.headers.upsert({ key: 'X-Timestamp', value: timestamp }); pm.request.headers.upsert({ key: 'X-Nonce', value: nonce }); pm.request.headers.upsert({ key: 'X-Sign', value: sign }); console.log('待签名串:\n' + rawString); console.log('SM2签名结果:', sign);这段脚本做了六件事:加载库、读配置、生成动态参数、拼待签名串、签名、写请求头。其中pm.request.url.getQueryString()拿到的是原始查询串,我用split('&')自己解析,而不是直接用一些可能有兼容性差异的API,这样在Postman不同版本之间表现更稳定。
pm.request.headers.upsert这个方法比较关键:如果Header里已经有同名键就替换,没有就新增。如果用add,多次跑同一个请求时会出现重复的Header项,干扰服务端验签。
我在脚本最后加了console.log,这个习惯救了我好几次。待签名串一打印出来,发给后端同学一比对,是排序问题还是空格问题,一眼就能看出来。
3.3 SM2签名选项:hash、der、userId一个都不能错
脚本里sm2.doSignature的第三个参数有几个选项,这里值得多花点时间,因为大多数验签失败都栽在这几个选项上。
第一个是hash。SM2签名的标准流程是先对消息做摘要,再做椭圆曲线签名。sm-crypto库的hash选项设为true时,会先用SM3对消息做哈希再签名,这正是国密标准推荐的做法。如果你设成false,服务端那边用的是哈希后再签名的流程,两边就对不上。
第二个是der。这个选项决定签名结果的编码格式。der: false输出的是R和S两个大整数直接拼接的HEX字符串,长度是128个HEX字符;der: true输出的是ASN.1 DER编码格式。Java端用BouncyCastle做SM2签名时,默认输出DER编码;很多Node.js和前端库默认输出RS拼接。两边必须保持一致。
第三个是userId。SM2签名过程中有一个ZA值的计算,涉及一个用户身份标识,国密标准里默认值是1234567812345678这16个字节。sm-crypto库内部默认用的也是这个值,所以大多数情况下不用显式传。但如果服务端自定义了userId,脚本里就要同步设置:
const sign = sm2.doSignature(rawString, privateKey, { hash: true, der: false, userId: '自定义的ID' });关于SM2密文结构里的C1C2C3和C1C3C2,那个是SM2加密场景的概念,不是签名场景。签名这边只关心R和S的组织方式,也就是上面说的DER还是RS拼接,别被这个概念绕晕。
3.4 调试技巧:先把待签名串打出来
签名不通过,最常见的原因不是算法写错,而是待签名串和服务端重建的串不一致。解决这个问题最有效的办法,就是把脚本里生成的rawString完整打印出来,同时让后端把服务端验签时重建的待签名串日志也拉出来,两边一对比,差异立刻就现形。
具体操作是:脚本用了console.log之后,在Postman左下角打开Console面板,就能看到每次请求打印的待签名串和签名结果。如果待签名串里有隐藏的空白字符,可以把字符串里的空格和换行换成可见符号检查一下。比如在Console里看JSON.stringify(rawString)的输出,能明确看到\n位置是否正确。
我在联调时遇到过一种情况:拼串规则看起来完全一样,但服务端重建的串里没有body参与签名,我这边把body拼进去了,结果就是一直验签失败。这类问题靠肉眼读文档很难发现,打印出待签名串之后一比对,很快就定位了。
4. Token动态生成与会话管理
4.1 登录接口拿Token并自动保存
先把获取token的接口在Postman里创建好。这个接口通常只需要appId和appSecret,不需要做SM2签名,或者用一套单独的签名规则。我用一个GET请求模拟:
URL:{{base_url}}/oauth/token
Headers:
| 参数 | 值 |
|---|---|
| Content-Type | application/json |
Body:
{ "app_id": "{{app_id}}", "app_secret": "{{app_secret}}" }然后在这个请求的Tests脚本里写token保存逻辑:
const res = pm.response.json(); if (res.code === 0 && res.data && res.data.access_token) { pm.environment.set('access_token', res.data.access_token); pm.environment.set('token_expire_at', String(Date.now() + res.data.expires_in * 1000)); if (res.data.refresh_token) { pm.environment.set('refresh_token', res.data.refresh_token); } console.log('token已更新'); } else { console.error('获取token失败', res); }这段脚本的核心是pm.environment.set,把响应里的access_token写入环境变量。顺手把过期时间也存进去,后面自动刷新才有依据。expires_in单位是秒,转成毫秒加上当前时间,就是token的过期时间点。
跑一次这个请求,打开环境变量面板,看到access_token已经有值了,说明保存成功。后面所有业务请求的Header里,就可以直接用Authorization: Bearer {{access_token}}引用这个变量。
4.2 集合级脚本:Token过期自动刷新
业务请求多了之后,每次都要手动去跑一遍登录接口换token,很烦。更麻烦的是token过期之后,业务请求全部变成401,你得发现之后再去重新获取一次。解决这个问题,把逻辑提到集合级别(Collection)的预请求脚本里。
集合级预请求脚本对集合内的所有请求生效,这样每个业务请求就不用自己写token检查逻辑了。思路是:请求发出前检查access_token,如果不存在或者已过期,就调用刷新token的接口,拿到新token后再继续。
这里有个异步处理的关键点。pm.sendRequest是异步的,但Postman在预请求脚本里会等待pm.sendRequest的回调执行完,再发送主请求。所以把后续逻辑放在回调里是安全的:
function ensureToken(callback) { const token = pm.environment.get('access_token'); const expireAt = Number(pm.environment.get('token_expire_at') || 0); if (token && Date.now() < expireAt) { return callback(); } const req = { url: pm.environment.get('base_url') + '/oauth/token', method: 'GET', header: { 'Content-Type': 'application/json' }, body: { mode: 'raw', raw: JSON.stringify({ app_id: pm.environment.get('app_id'), app_secret: pm.environment.get('app_secret') }) } }; pm.sendRequest(req, function (err, res) { if (err) { console.error('刷新token请求失败', err); return callback(); } const data = res.json(); if (data && data.access_token) { pm.environment.set('access_token', data.access_token); pm.environment.set('token_expire_at', String(Date.now() + data.expires_in * 1000)); console.log('token已自动刷新'); } else { console.error('刷新token响应异常', data); } callback(); }); }注意这里我留了一个提前2秒刷新的余量,防止token恰好卡在过期边缘,请求到服务端时已经失效。也可以把判断条件改成Date.now() >= expireAt - 2000。
4.3 统一处理:签名与Token合并成集合级脚本
现在把第3章的签名逻辑和第4章的token刷新合并成一个集合级预请求脚本。这样集合内的所有请求都只需要关心自己的参数和body,sign、token、timestamp、nonce全部自动处理。
集合级脚本的完整流程是:
function ensureToken(callback) { // 省略,代码同上 } function buildSignHeader() { // 1. 加载sm-crypto库 eval(pm.globals.get('SM2_LIB')); // 2. 生成timestamp和nonce const timestamp = String(Math.floor(Date.now() / 1000)); const nonce = Math.random().toString(36).substring(2) + Date.now().toString(36); // 3. 组装待签名串 const method = pm.request.method.toUpperCase(); const path = pm.request.url.getPath(); // ... 和3.2一样的拼串逻辑 // 4. 签名并写Header const sign = sm2.doSignature(rawString, pm.environment.get('private_key'), { hash: true, der: false }); pm.request.headers.upsert({ key: 'X-Sign', value: sign }); pm.request.headers.upsert({ key: 'X-Timestamp', value: timestamp }); pm.request.headers.upsert({ key: 'X-Nonce', value: nonce }); pm.request.headers.upsert({ key: 'X-App-Id', value: pm.environment.get('app_id') }); // 5. token写入请求头 pm.request.headers.upsert({ key: 'Authorization', value: 'Bearer ' + pm.environment.get('access_token') }); console.log('待签名串:\n' + rawString); console.log('签名结果:', sign); } ensureToken(function () { buildSignHeader(); });这套方案最大的好处是:新来的同事把接口文档里的URL和参数填进去就能跑,不用理解签名逻辑;签名逻辑即使要调整,也只改集合级脚本这一个地方,所有请求都跟着生效。我在实际项目里就是这么用的,几十个接口的调试效率提升非常明显。
要注意的是,如果某些请求不需要签名或者不需要token,比如登录接口本身,可以在脚本里加个判断,通过请求名称或者tag过滤掉。我习惯在集合里建一个叫“auth”的文件夹,文件夹下的请求跳过签名逻辑,文件夹外的统一处理。
5. 常见问题排查与避坑实录
5.1 验签不通过的排查清单
验签失败是SM2接口联调里出现频率最高的问题。我把实际踩过的坑整理成了一张表,按优先级排查:
| 现象 | 可能原因 | 排查方向 |
|---|---|---|
| 服务端返回签名无效 | 待签名串不一致 | 打印待签名串,与后端日志比对 |
| 签名无效 | 参与签名的参数排序不一致 | 检查query参数是否按字典序排序 |
| 签名无效 | 签名格式不匹配 | 确认后端要求DER还是RS拼接 |
| 签名无效 | userId不一致 | 确认服务端是否默认值,是否自定义 |
| 签名无效 | body序列化方式不同 | 确认body用raw原文还是格式化后字符串 |
| 签名无效 | 私钥格式错误 | 确认私钥是HEX原码还是需要转换 |
| 签名有效但请求被拒绝 | 时间戳超过允许范围 | 检查客户端与服务端时间偏差 |
| 签名有效但请求被拒绝 | nonce重复或已被消费 | 确认nonce生成是否唯一,是否有缓存机制 |
第一条“签名无效”是一个总现象,具体原因基本都能归到后面几条。我的经验是,先别急着怀疑算法本身,先去做字符串比对。把Postman里打印的待签名串复制出来,让后端也把收到的待签名串复制出来,用文本对比工具看差异,比猜盲盒快得多。
排序不一致是最隐蔽的问题之一。假设URL里的query参数是b=2&a=1,客户端不排序直接拼串,服务端排序后再拼,两个待签名串显然不一样。有些项目连query参数的value也要URL decode之后再拼,有些直接用原始值,这些细节都要对齐。
body参与签名的坑也很典型。Postman里你看到的JSON可能是格式化过的、带缩进的,但请求发出的raw body可能是你填写的原文。脚本里pm.request.body.raw取到的是原始输入,只要手填的body和实际发出的body一致,通常没问题。但如果你在Postman的body编辑器里看到了漂亮的缩进,不要以为服务端收到的是这个格式,HTTP传输时是按你编辑区里的原始内容传的。
5.2 Postman脚本跑不起来
脚本本身报错、或者加载库失败,这类问题通常集中在下面几个地方。
第一个是SM2_LIB全局变量没配置或者内容不全。运行时如果pm.globals.get('SM2_LIB')返回空,脚本第一行就抛异常。检查一下全局变量是否真的保存成功,粘贴大文本时偶尔会保存失败,重新粘贴一次就好。
第二个是eval执行时报语法错误。多数情况是复制sm-crypto.min.js时内容被截断,或者粘贴到了不支持的字符。重新复制一次,确保文件内容是完整的。注意别把ESM版本的sm-crypto.esm.js复制进去,那个文件里是export语法,eval执行会直接抛错,一定要用sm-crypto.min.js这个UMD版本。
第三个是变量引用写错。很多新手在脚本里习惯性写成pm.environment.get('{{app_id}}'),把模板语法的双大括号也带进去了,导致取出来的值是{{app_id}}这个字符串本身。脚本里取变量永远不要带双大括号,只有URL、Header、Body里才能用{{variable}}这种语法。
第四个是Header重复。用pm.request.headers.add添加签名Header后,如果同一个请求跑了多次再修改脚本,之前的Header还在,就会累积出多个X-Sign。这种问题用upsert就能避免,它会自动替换同名的Header项。
5.3 HTTPS证书相关问题
HTTPS证书问题在联调阶段非常常见,尤其是自签名证书和双向认证。
自签名证书报错时,Postman的提示一般是“Self-signed SSL certificates are not supported”或者“SSL certificate problem”。开发环境直接关闭SSL校验就行。需要注意的是,关闭校验只影响当前Postman配置,不影响其他客户端。这个开关在Settings -> General里,关闭后重新发送请求生效。
双向认证场景下,服务端要求客户端上传证书。Postman的配置路径是Settings -> Certificates -> Add Certificate,填入服务端域名和端口,然后上传证书文件。P12/PFX格式可以直接上传,输入证书密码保存即可。有的项目会用单独的PEM格式的客户端证书和私钥,Postman也支持分别上传。
还有一个容易忽略的问题:如果服务端用的是IP地址访问,证书里签发的域名是另一个域名,即使关闭了SSL校验,有些版本Postman可能依然会因为hostname不匹配而报错。这时候要么用和证书匹配的域名访问,要么用Hosts绑定,要么彻底关闭校验。实际联调中我一般直接关校验,省事。
5.4 国密SSL和“业务层SM2”要分清
最后特别提醒一下“国密SM2+HTTPS”这个概念。我碰到过不少朋友把国密SSL和业务层SM2签名混为一谈,然后卡在证书配置上半天。
标准的HTTPS,是TLS握手阶段用RSA或者ECDSA证书做身份认证,然后协商对称密钥加密通信。国密SSL的意思是TLS握手本身使用SM2证书、SM3摘要、SM4对称加密,这是另一套完整的技术栈。而业务层SM2签名,是HTTP请求的内容里用SM2私钥做了签名,传输通道仍然是标准HTTPS或者普通HTTP。
后者在Postman里很容易适配,把sm-crypto库塞进去,脚本跑起来就完事。前者目前Postman不支持,因为底层的TLS栈没实现国密套件。如果项目确实要求国密SSL,一般需要在测试环境部署支持国密套件的网关或负载均衡,把国密TLS终结掉,后端内部仍然走标准协议;客户端这边该做的SM2业务签名照做就行。
所以拿到一个“国密SM2+HTTPS”的项目,先花两分钟确认一下:传输层是标准TLS还是国密TLS?业务层签名要求什么格式?这两个问题确认清楚,后面就少走弯路。
最后说几句实在的
这套方案从“被逼着在Postman里写脚本”开始,到后来变成团队通用的联调标准,我自己最大的体会是:集合级脚本统一处理签名和token,真的能给团队省下大量重复劳动。新来的同事不需要理解SM2算法细节,填好参数就能调接口,后端也不用反复帮忙看签名问题。
再补三个实用原则。第一,测试私钥放环境变量就算了,线上私钥千万不要写进任何本地工具或脚本里,这是底线。第二,所有签名规则以接口文档为准,不要自己发挥,文档里没写清楚的细节一定找后端确认,比如body是否参与签名、timestamp的允许偏差是多少、nonce是否需要服务端缓存。第三,验签不通过的时候控制住情绪,先打印待签名串,让后端把服务端的重放日志拉出来,两个人拿着文本一对,问题基本都能快速定位。
如果你对接的项目里还有SM4加密的字段,或者需要SM3做摘要的body,思路是一样的:sm-crypto库已经支持这些算法,在同一个脚本里调用即可。希望这篇记录能帮你在SM2接口联调时少踩几个坑。