微信小程序请求签名mtgsig1.2:原理、排查与工程实践
1. 先搞清楚mtgsig1.2是什么1.1 从一次接口报错说起做过微信小程序开发的朋友或多或少都遇到过这种情况自己写好的接口在开发者工具里一切正常一上真机就报“签名校验失败”或者抓包时看到请求参数里多了一个叫mtgsig的字段稍微改一下请求参数后端就返回 401。这个mtgsig就是小程序侧的请求签名。1.2是它的算法版本号。它的核心作用是让服务端能判断“这条请求是不是真的来自我的小程序客户端”以及“请求内容在传输过程中有没有被篡改”。我第一次碰到mtgsig1.2是在做一个小程序的订单提交功能。当时后端同事在接口文档里标注“需要携带签名”但没细说生成规则。我以为是简单的 MD5 拼串结果调了半天一直报错最后翻到小程序基础库的请求封装代码才发现签名是在请求拦截器里自动生成的压根不需要前端手动传。也就是说mtgsig1.2对普通业务开发是“透明”的——它由小程序运行环境或第三方安全 SDK 自动注入。但当你需要做接口调试、数据采集、自动化测试或者排查线上请求异常时就必须理解它的生成和验证逻辑。1.2 它在请求链路中的位置一个典型的小程序请求长这样小程序前端通过wx.request发起 HTTPS 请求请求头里带着Content-Type、Cookie或Authorization请求体里是业务参数比如商品 ID、用户 ID请求头或请求体里还带着mtgsig、timestamp、nonce等附加字段服务端收到后先不急着处理业务第一件事是用同样的签名算法把收到的参数重新计算一遍签名然后和客户端传来的mtgsig对比。一致就放行不一致直接拒绝。这个流程本质上和你去银行办业务要在单据上签字一样——银行不关心你的字好不好看关心的是签完字之后有没有人把金额从 100 改成 10000。签名校验通过说明这份“单据”自签字之后没有人动过。1.3 什么场景下你必须理解它我总结了三类人必须搞懂mtgsig1.2第一类是做小程序接口联调的前后端开发。尤其是后端同事签名规则往往不会写在接口文档里而是由安全组件自动计算。如果前端传了签名但后端验不过两边都得排查算法规则。第二类是做小程序自动化测试的测试工程师。你用脚本模拟发请求如果不知道签名怎么生成请求就会被拦截。做压力测试、接口回归测试前必须先把签名问题解决。第三类是做数据采集或第三方小程序分析的技术人员。想通过 PC 端抓取小程序页面数据或者做竞品分析第一步就会撞上签名校验。当然这里要强调一个前提分析自己开发的小程序、做好授权的联调测试这是正当需求。对他人小程序做未授权的破解或攻击既有风险也违反平台规则不建议碰。2. 为什么小程序接口一定要加签名2.1 小程序前端代码是“透明”的相比传统 PC 客户端小程序有一个很特殊的地方虽然发布后是经过编译的包但前端代码资源包可以直接下载到本地而且有成熟的工具可以反编译还原出部分源码。这意味着什么意味着你不能再像传统 Web 开发那样把业务逻辑完全藏在后端前端只负责展示。小程序前端要发起请求就必须把参数、密钥、拼接规则都放在前端包里。攻击者拿到包之后慢慢看总能看懂请求是怎么发出去的。这个时候如果接口只靠Cookie或者固定的Token来鉴权就非常危险攻击者可以仿造请求头随意提交数据。典型的例子就是“刷单”——本地改一下商品价格参数再重新提交订单如果后端没校验签名这个请求就会被当成正常请求处理。mtgsig1.2这类签名机制解决的核心问题就是让“伪造请求”的成本大幅提高。它不是完全无法绕过但至少能挡住 90% 的随手修改。2.2 防篡改、防重放、防伪装展开说说签名机制到底防什么。防篡改指的是请求参数一旦被修改签名就失效。比如订单金额原本是 100 元攻击者在本地把它改成 1 元再重新计算签名时发现算不出来——因为签名算法里的盐、密钥都混在代码里没有正确还原的算法流程改参数后生成的签名是错的服务端一验就发现问题。防重放指的是同一个合法请求不能被反复提交。解决这个问题通常靠timestamp和nonce。客户端每次请求都带上当前时间戳和一个随机字符串服务端收到记录一下如果同一个nonce在短时间内出现两次或者timestamp与服务端当前时间差超过一定阈值就直接拒绝。防伪装指的是攻击者不能用自己构造的客户端去请求服务端。服务端校验签名时使用的密钥或盐是和小程序代码绑定的攻击者用自己写的脚本请求发过去的签名根本算不对。2.3 为什么用签名而不是只用 Token有朋友可能问传统 Web 开发里用 Token 就够了呀为什么小程序要搞签名区别在于 Token 是“身份凭证”签名是“内容完整性凭证”。Token 只能证明“你是你”但不能证明“你发的这个请求内容是你真实想发的”。在小程序场景里前端代码可以被篡改后再打包运行Token 本身也可以被盗取和重放。所以实际方案是两者叠加用Cookie或Authorization解决身份认证用mtgsig1.2解决请求内容的完整性和唯一性。谁也不能替代谁少了哪一个都不安全。一套称职的请求签名方案通常具备这么几个特点参数字段统一收集、统一排序任何一方不遵守规则就算不出同一个值使用带密钥的摘要算法比如 HMAC-SHA256而不是裸 MD5非对称加盐不同场景甚至不同用户使用不同盐值时间戳防重放随机数防碰撞服务端有兜底策略验证失败会记录日志并触发告警3. mtgsig1.2 的签名逻辑拆解3.1 签名一般由哪些材料构成虽然各平台的实现细节不同但mtgsig1.2这类签名机制的生成逻辑有比较固定的套路。我自己拆过几个小程序包的请求封装总结下来通常是这几类材料参与签名业务参数所有 GET 参数和 POST 表单字段按参数名字典序排序后拼接时间戳10 位或 13 位数字精确到秒或毫秒随机数每次请求生成一个避免两个请求的签名完全相同设备信息比如机型、系统版本、屏幕分辨率标识设备指纹用户身份登录后的openid或 user token标识请求者盐值一个写死在代码里、或者从服务端配置下发的字符串签名算法里最重要的部分拼接规则通常是把这些字段按固定顺序排好拼成一个长字符串然后做摘要。这里有个细节字段顺序极其重要。客户端和服务端必须用完全相同的拼接顺序否则同一份数据算出来的签名就不同。3.2 摘要算法怎么选常见的摘要算法有 MD5、SHA-1、SHA-256高级一点的用 HMAC 系列。mtgsig1.2这代方案我推测采用了 HMAC-SHA256 或者类似的带密钥算法因为从抗破解角度讲裸 MD5 已经很容易被碰撞和暴力破解达不到防篡改的目的。用 HMAC 的好处是密钥不直接参与字节拼接而是作为算法的一个独立输入参数。这样即使攻击者拿到了完整的参与签名字符串没有密钥也算不出正确的签名结果。一个典型的 HMAC-SHA256 生成流程长这样1. 收集所有参与签名的字段 2. 按字段名 ASCII 码升序排序 3. 用 keyvalue 形式拼成字符串中间用 连接 4. 取当前时间戳和随机数 5. 将固定盐值、时间戳、随机数都追加进字符串 6. 使用 HMAC-SHA256密钥为盐值对字符串做摘要 7. 输出十六进制字符串作为 mtgsig 字段的值当然这只是通用思路。不同平台的实现细节差异很大有的会加 Base64 编码有的会做两次摘要有的会把部分字段加密后放进签名内容里。你不需要死记硬背某一套规则但理解了这个套路排查问题时能少走很多弯路。3.3 mtgsig1.2 相比旧版本的可能变化从版本号命名习惯来看1.2显然是迭代过一版的新算法。我没法看到平台内部的变更日志但从行业惯例推测这类版本升级通常围绕几个方向第一是算法强度升级。旧版可能用 MD5新版换成 HMAC-SHA256硬件加速之后性能损失也不大。第二是加入更丰富的环境信息。比如增加陀螺仪、传感器状态、键盘行为等操作行为特征进一步提高模拟器或脚本与其他非法工具的检测率。第三是动态密钥。旧版可能是固定盐写死在代码里新版改成定期下发或按用户维度动态生成这样即使算法泄露攻击者拿到的也只是某一个用户某一个时间段的有效密钥。如果你发现生产环境原来正常的小程序突然大量报签名错误首先要怀疑的就是签名版本升级导致的兼容性问题。特别是旧版本的小程序客户端还没有更新还在用mtgsig1.0的规则发请求服务端已经切到1.2的校验逻辑自然全部失败。4. 实战定位并解决 mtgsig1.2 签名问题4.1 先准备一套趁手的调试环境处理签名问题基础的抓包工具是必须的。我习惯的组合是Charles 或 Fiddler 抓 HTTPS 包查看完整请求头、请求体和响应体Whistle 用来做代理和脚本注入可以改包、重发、对比请求微信开发者工具作为基础调试环境移动端用安卓手机配合真机调试如果你的手机没有开代理小程序只要开启调试模式就能通过开发者工具的 Network 面板直接看到请求详情就不用额外抓包了。真机调试时遇到同样的问题配置代理之后按提示安装并信任 Charles 的 CA 证书即可。有一个常见坑最新版安卓手机开代理抓包后小程序请求直接报 SSL 握手失败。这是因为部分机型对用户证书的信任策略更严或者是小程序本身开启了证书校验。解决办法是把证书安装到系统证书目录需要 root或者用更成熟的代理工具稍加处理。4.2 定位签名生成的入口拿到抓包结果后先看请求参数里都有哪些字段。一个典型的mtgsig1.2请求长这样{ url: https://api.example.com/v1/order/create, method: POST, headers: { Content-Type: application/json, mtgsig: a1b2c3d4e5f67890abcdef, timestamp: 1735689600, nonce: 8f3a2c91 }, body: { product_id: 1001, quantity: 2 } }这里的mtgsig、timestamp、nonce就是签名系统的核心三件套。要搞清楚它怎么生成的路径一般是把小程序包下载到本地找到请求封装模块搜索mtgsig字符串顺藤摸瓜找到签名函数。如果你是自己开发的小程序直接在代码里搜索mtgsig就能找到封装位置。如果你是分析第三方小程序需要用专业工具把 WXAPKG 包解包后定位代码。这里再次提醒只对你拥有或已获授权的代码做分析否则有法律风险。4.3 手工复现签名的三个关键步骤假设你已经定位到了签名函数的伪代码接下来就是按它复现。分三步走第一步确定参与签名的字段。把请求体、请求头、URL 里的参数全部列出来注意别漏掉默认值。很多时候签不上是因为某个字段用的是默认值比如设备型号你复现的时候没传生成的签名就对不上。第二步确定拼接顺序和格式。这里需要严格按代码里的规则来。有的按字典序排有的按固定顺序排有的先拼接再排序再拼接一次。有条件的直接找一条历史请求手工算一遍验证自己理解对不对。第三步确定算法和密钥。如果代码里写了密钥字符串直接提取出来测试。如果密钥是动态下发的就要在登录流程里去拿。有的平台会把密钥拆成几段分散在代码里运行时再拼接这时候需要耐心点一步一步还原。一个很实用的验证方法拿一条抓到的真实请求把它的参数、时间戳、nonce 原封不动地跑一遍你写的复现代码看算出来的签名是不是和抓包结果完全一致。一致就说明你复现对了后面随便改参数都能自己算。4.4 一个常见场景自动化测试脚本怎么处理签名做小程序接口自动化测试的时候最烦的就是签名。我处理过的项目里有这么几种方案按推荐度排序方案一在测试环境关闭签名校验。如果后端可以配置“测试环境不校验签名”那是最省事的方案二直接从小程序端把加密模块导出来在测试脚本里调用同一套逻辑方案三自己按签名算法加密规则写一套 Python/Java 实现在测试用例里动态生成方案二和方案三其实是等价的区别在于你复用现成代码还是手工重写。我通常优先试方案二因为直接用现成模块不容易出错尤其是算法版本更新后你手工重写的代码可能没跟上。需要注意有的签名模块依赖小程序的运行环境比如调用了wx.getSystemInfoSync()获取设备信息。直接拿到 Node.js 或 Python 里跑会报错。这时候需要 mock 掉这些依赖把常量写死进去。我之前处理过一个案例签名里用到屏幕宽高我直接把目标手机的参数写进去就通过了。5. 常见问题与排查技巧实录5.1 签名校验失败的十大原因结合我自己的排查经验把常见原因整理成一张速查表方便大家对照检查。现象可能原因排查方向所有请求都签名失败算法版本不一致确认客户端和服务端用的签名版本号一致新功能接口失败老接口正常新接口用了新的参数规则拉最新代码对比生成逻辑时间戳差值接近过期临界点设备时间不准确校准手机时间确认与服务端时间差值同一个请求重发后失败nonce 或时间戳重放被拦截每次请求重新生成随机数换了台手机就失败设备参数参与签名确认设备信息字段完整开发者工具正常真机失败环境差异导致某个字段不同对比两种环境下的请求头差异抓包改一下参数就失败签名后端联动校验参数修改业务参数时同步重新计算签名线上偶发失败密钥轮换或灰度发布查服务端日志看失败请求的时间点POST 请求失败GET 正常请求体被单独签名确认请求体参与签名的规则安卓正常iOS 失败系统参数格式不同对比两端生成的签名字段格式这张表的用法是先看现象定位到可能的类别再去抓包里对比数据。大部分签名配合问题都能从这里找到方向。5.2 一个真实排查案例参数顺序导致前后端签名不一致说一个我自己踩过的坑印象很深。当时做一个小程序商城的订单模块后端用 Python 实现签名校验前端用插件跑起来的时候订单创建接口时好时坏。好的时候很顺坏的时候报签名错误完全没有规律。后来查了很久发现是签名拼接规则里参数排序的问题。后端同事实现的时候把所有参数放进字典再排序Python 的字典排序按 Unicode 码位处理而前端插件拼字符串时按 JavaScript 的Array.sort()的 UTF-8 字节序排。两个排序规则在大多数场景下结果一样但一旦参数名里出现大小写混合或数字就可能出现差异。举个例子参数abc、Abc、123这三项Python 按 Unicode 码位排123AbcabcJavaScript 默认排序123Abcabc看起来一样但如果参数里有_下划线或-连字符个别场景下 Unicode 码位和 UTF-8 字节序的优先级就不一样了这类问题最坑人的地方在于它不是百分百失败而是只有特定参数组合时才会触发。遇到这种情况我的经验是让前后端把参与签名的原始字符串和最终摘要结果都打印出来一对比就能立刻定位是拼接不一致还是算法不一致。5.3 时间戳同步问题别忽视时间戳问题在开发环境不常见但在生产环境和测试环境经常遇到。原因很简单生产环境的服务器集群可能做了多节点部署节点之间如果存在几十毫秒的时间偏移一旦签名有效窗口设置得比较小就会出现一部分请求刚好在临界点被拒绝。另外手机用户如果自己改了系统时间或者手机长期不联网导致时间漂移也会触发签名过期。这类问题在 C 端异常请求排查里占不小比例。解决的思路有两个层面客户端在发起请求前先通过服务端接口做一次时间同步校准计算本地时间与服务端时间的偏移量签名时用修正后的时间戳。服务端则把签名有效窗口设置得宽一些比如前后 5 分钟而不是严格的 30 秒。当然窗口越大重放攻击的风险越高需要业务上权衡。5.4 环境探测为什么脚本模拟请求总是失败还有一类问题需要特别说明mtgsig1.2这类高级签名机制往往不只是算一个摘要字符串而是会结合运行时环境做风控判断。如果你用脚本模拟请求即使签名算法完全正确请求也可能被拒绝。原因是它识别出了“当前运行环境不是真实的小程序客户端”。它怎么识别的方式很多常见的有这样几类检查 WebView 内核版本和真实微信内置浏览器是否一致检查传感器数据是否处于静止状态真实手机拿在手里一定有轻微晃动检查wx全局对象的方法是否完整很多脚本模拟环境只实现了部分方法。所以如果你确认签名算法、执行逻辑都是对的但模拟请求还是失败不要死磕算法了先检查自己的运行环境是否被识别成了“环境不合规”。解决思路是用真机设备配合代理工具而不是在 PC 模拟器里跑。6. 从签名机制到小程序工程化几个延伸实践6.1 上线前的测试压力测试要不要做签名校验存在的情况下压力测试不是“要不要做”的问题而是“怎么做”的问题。很多团队先用脚本模拟大量请求做压测结果服务端校验签名成了性能瓶颈或者签名库里随机数生成、摘要计算的耗时拖垮了整个接口的 TPS。我的建议是压测分两层。第一层在网关和业务层做不必带签名直接压测业务接口的核心耗时这部分能真实反映业务代码的性能。第二层做端到端压测带上完整签名逻辑验证签名模块在并发场景下是否有性能问题比如时间戳生成是否安全、随机数是否有碰撞、内存里缓存密钥的并发访问是否安全。一定要注意的是压测过程中如果服务端有风控逻辑可能会把压测机器人的 IP 或账号拉黑导致后续请求被拒绝。压测前要和运维同事沟通好把测试账号和压测机器 IP 加白名单。6.2 支付回调与签名的配合小程序支付接口是签名机制应用最严格的场景。从wx.requestPayment拉起支付到后端收到支付回调通知这中间涉及好几层签名。第一层是小程序请求下单接口时的mtgsig1.2签名保证下单参数没被篡改。第二层是微信支付平台返回的支付参数这层签名由微信支付 SDK 完成开发者不需要自己处理。第三层是支付结果回调通知微信支付平台会用平台证书对回调内容做签名开发者收到回调后必须验证签名才能确认订单状态。踩过的坑是有的团队在小程序端做了签名校验但在支付回调接口里漏掉了验签导致伪造的支付成功通知打到了服务器。虽然概率不高但一旦发生就是资损。支付相关的签名校验建议由专门的后端同事负责前端只负责把必要的参数原样传给后端不要在客户端做任何金额相关的逻辑判断。6.3 版本更新与签名兼容小程序有一个机制叫UpdateManager可以检测新版本并提示用户重启小程序。签名算法升级时这个机制尤其重要你不能强制所有用户当天全部更新只能通过接口往下兼容。通常的做法是在签名里增加版本号字段让服务端根据版本号走不同的校验逻辑。灰度期新老逻辑并存新版本客户端发新签名老版本客户端发旧签名等老版本客户端占比降到阈值之下再下掉旧逻辑。但一定要给这个灰度期设一个明确的上限时间否则代码里常年挂着两套签名逻辑出了安全漏洞都不知道该修哪个。我见过一个项目签名版本从1.0升到1.1花了半年才把旧版代码全清掉中间那半年每次代码 Review 都要解释一遍两套逻辑的区别非常痛苦。6.4 给前端小白的排查清单最后给刚接触小程序开发的朋友一份排查清单遇到签名相关的问题按顺序走能解决大部分困扰确认接口文档里是否有签名参数需要自己传还是自动生成抓包看请求里有没有mtgsig、timestamp、nonce字段用开发者工具的 Network 面板对比正常请求和异常请求的差异检查设备时间是否和服务端时间偏差过大对比不同系统、不同机型下请求头是否一致搜索代码中的mtgsig找到生成函数确认版本号如果客户端沿用旧版本确认服务端是否已经切换到新版本校验问题仍未解决联系后端同事把请求头和请求体完整发过去这些排查步骤本身不需要多高深的技术耐心和细致才是找签名问题的关键。很多所谓“诡异问题”最后查下来都是参数漏传、版本不一致或者时间没同步这些基础原因。个人经验是处理mtgsig1.2这类签名机制时最忌讳的就是直接去猜算法。老老实实抓包、对比、复现比你瞎猜一小时有用得多。把这套流程跑熟之后以后遇到mtgsig2.0、3.0也都是一样的打法。