📋 目录
一、工具类功能
参数表按 key 字典序拼成签名原文,再走摘要式或 HMAC-SHA256 出签名,回调验签、请求签名、兼容微信支付宝老协议都能用,拿回来是十六进制签名串与验签结论。
二、所需环境
JDK 17或以上
三、方法与调用示例(按需调用)
🔢 方法序号:1
🎯 方法用途:
- 把参数表按 key 字典序拼成签名原文,
sign、sign_type与空值参数一律不进原文 - 对接支付网关、开放平台回调时先拿它出原文,再交给具体算法
🧩 方法定义:public static String buildSignSource(Map<String, ?> params)
🗣️ 入参解释:public static String buildSignSource(参数表)
📤 返回值解释:String,形如 k1=v1&k2=v2 的签名原文
💡 边界口径:
- 排序按 key 的字典序走,与参数表原本的插入顺序无关
- 值为 null 与空串的参数被跳过,各家平台在这点上是一致的
- 集合与数组按出现顺序展开成同名多项,
tag=b&tag=a这种写法 - 原文不做 URL 编码,这是它与
UrlCodecUtils.encodeQuery的本质区别,拿编码后的串去签名一定对不上 - 原样拼接的边界有歧义:值里含
&或=时能拼出同一份原文,参与签名的字段不能来自不可信输入的自由拼接 - 参数表传 null 抛
IllegalArgumentException,key 为 null 的条目被直接跳过
🧪 方法示例:
buildSignSource(params)拿到appid=wx08804b9c9f8f8e3a&body=测试商品-上海&nonce_str=593EF8934B5A94F0C2&out_trade_no=ORD20260925001&total_fee=1buildSignSource(multiValue)拿到amount=12800&tag=b&tag=a,集合按出现顺序展开buildSignSource(Map.of("a", "1&b=2"))拿到a=1&b=2,与两个参数拼出的原文完全一样buildSignSource(null)抛参数Map不能为null
🔢 方法序号:2
🎯 方法用途:
- 在默认剔除清单之上再加本平台特有的字段,比如某些平台要把
sign_key、attach摘掉 - 各家协议要求剔除的字段不同,用这一版就不用自己先复制一份 Map 再 remove
🧩 方法定义:public static String buildSignSource(Map<String, ?> params, Set<String> excludeKeys)
🗣️ 入参解释:public static String buildSignSource(参数表, 额外剔除字段)
📤 返回值解释:String,剔除后按字典序拼出的签名原文
💡 边界口径:
- 自定义清单是追加在
sign、sign_type之上的,没法靠它把默认剔除关掉 excludeKeys传 null 时只按默认清单剔,不报错- 其余排序、空值跳过、不做 URL 编码的口径与单参版一致
🧪 方法示例:
buildSignSource(withExtra, Set.of("sign_key"))拿到appid=wx08804b9c9f8f8e3a&body=测试商品-上海&nonce_str=593EF8934B5A94F0C2&out_trade_no=ORD20260925001&total_fee=1,与默认清单剔完的结果一模一样buildSignSource(params, null)拿到body=测试商品-上海&out_trade_no=ORD20260925001&total_fee=1buildSignSource(Map.of("body", "x", "partner", "y"), Set.of("partner"))拿到body=x
🔢 方法序号:3
🎯 方法用途:
- 对现成的签名原文做摘要式签名,原文末尾拼上
&key=密钥再整体取摘要,输出小写 Hex - 微信 JS-SDK 这类要自己拼原文(
jsapi_ticket=...&noncestr=...&url=...)的协议只能走这一层
🧩 方法定义:public static String signSourceByDigest(String signSource, String secret, String digestAlgorithm)
🗣️ 入参解释:public static String signSourceByDigest(签名原文, 密钥, 摘要算法)
📤 返回值解释:String,小写十六进制的签名值
💡 边界口径:
- 算法取
ALG_MD5、ALG_SHA256这类常量,白名单外的名字抛IllegalArgumentException并列出支持范围 - 密钥拼在原文末尾的
&key=之后,这是历史协议的固定写法 - 摘要式为兼容老协议保留,新接入用 HMAC,涉及资金的验签用非对称算法
- 原文为 null、密钥为 null 或空白都抛
IllegalArgumentException,空原文照样能算
🧪 方法示例:
signSourceByDigest(source, SECRET, ApiSignUtils.ALG_SHA256)拿到0f0d73df94313475f5585dcb66f4caabecc465f01605630afbb02dfa13b18491signSourceByDigest(jsApiSource, SECRET, ApiSignUtils.ALG_SHA256)拿到8b9bb3ee34396e862a5bb946a7c63b4cad0a1bcc6839372547df4594b0317ce5signSourceByDigest("", SECRET, ApiSignUtils.ALG_MD5)拿到0b4ad1d96492c0442d79efa9125434f4,空原文也能算signSourceByDigest(null, SECRET, ApiSignUtils.ALG_SHA256)抛签名原文不能为nullsignByDigest(params, SECRET, "SM3")抛不支持的摘要算法: SM3,仅支持 MD5/SHA-1/SHA-256/SHA-512
🔢 方法序号:4
🎯 方法用途:
- 摘要式签名并要求输出大写 Hex,部分银行网关的字段就是这个形态
- 只是大小写不同的展示形式,签名值本身与三参版一致
🧩 方法定义:public static String signSourceByDigest(String signSource, String secret, String digestAlgorithm, boolean uppercase)
🗣️ 入参解释:public static String signSourceByDigest(签名原文, 密钥, 摘要算法, 是否大写)
📤 返回值解释:String,uppercase 为 true 时给大写 Hex
💡 边界口径:
- 转大写用的是
Locale.ROOT,不受系统语言环境影响 - 验签侧比的是解码后的字节,发出去是大写、对方按小写验也能过
- 参数校验与三参版相同
🧪 方法示例:
signSourceByDigest(source, SECRET, ApiSignUtils.ALG_SHA256, true)拿到0F0D73DF94313475F5585DCB66F4CAABECC465F01605630AFBB02DFA13B18491verifySourceByDigest(source, SECRET, ApiSignUtils.ALG_MD5, 大写签名)拿到true
🔢 方法序号:5
🎯 方法用途:
- 对签名原文做 HMAC 签名,输出小写 Hex,新接入首选
HmacSHA256 - 密钥不进原文、而是当 HMAC 的 key 用,比把密钥拼在明文尾巴上稳
🧩 方法定义:public static String signSourceByHmac(String signSource, String key, String hmacAlgorithm)
🗣️ 入参解释:public static String signSourceByHmac(签名原文, 密钥, HMAC算法)
📤 返回值解释:String,小写十六进制的 HMAC 签名值
💡 边界口径:
- HMAC 算法白名单是
HmacMD5、HmacSHA1、HmacSHA256、HmacSHA512,写别的直接抛异常 - 原文为 null、密钥为 null 或空白都抛
IllegalArgumentException - 原文这层不做任何拼接与排序,传进来什么就签什么
🧪 方法示例:
signSourceByHmac(source, SECRET, "HmacSHA256")拿到9aa4610b16258cd2970cdaa371c699a93ad8916889a527c89cd456c7600bf27csignSourceByHmac(source, " ", "HmacSHA256")抛密钥不能为null或空白signSourceByHmac(source, SECRET, "HmacSM3")抛不支持的HMAC算法: HmacSM3,仅支持 HmacMD5/HmacSHA1/HmacSHA256/HmacSHA512
🔢 方法序号:6
🎯 方法用途:
- HMAC 签名并控制输出大小写,网关要求大写 Hex 时用
- 与三参版只差展示形式,签名值一致
🧩 方法定义:public static String signSourceByHmac(String signSource, String key, String hmacAlgorithm, boolean uppercase)
🗣️ 入参解释:public static String signSourceByHmac(签名原文, 密钥, HMAC算法, 是否大写)
📤 返回值解释:String,uppercase 为 true 时给大写 Hex
💡 边界口径:
- 大写只影响字符形态,验签比的是字节
- 参数校验与三参版相同
🧪 方法示例:
signSourceByHmac(source, SECRET, "HmacSHA256", true)拿到9AA4610B16258CD2970CDAA371C699A93AD8916889A527C89CD456C7600BF27C
🔢 方法序号:7
🎯 方法用途:
- 参数表一步出摘要式签名,内部就是拼原文再做摘要
- 兼容微信公众号、支付宝老接口这类
MD5(原文&key=密钥)的历史协议
🧩 方法定义:public static String signByDigest(Map<String, ?> params, String secret, String digestAlgorithm)
🗣️ 入参解释:public static String signByDigest(参数表, 密钥, 摘要算法)
📤 返回值解释:String,小写十六进制的签名值
💡 边界口径:
- 原文口径与
buildSignSource(params)完全一致,sign与空值不参与 - 回调验签时对方带回来的
sign会被自动剔掉,不会把自己算进去 - MD5 只用于兼容,新接入别用
🧪 方法示例:
signByDigest(params, SECRET, ApiSignUtils.ALG_MD5)拿到d7922d5ef9ab694bc6b87abb5d6c5f86signByDigest(params, SECRET, ApiSignUtils.ALG_SHA256)拿到0f0d73df94313475f5585dcb66f4caabecc465f01605630afbb02dfa13b18491,与先拼原文再签同值
🔢 方法序号:8
🎯 方法用途:
- 参数表一步出摘要式签名并指定大写 Hex
- 老协议里要求签名字段大写的网关用这一版
🧩 方法定义:public static String signByDigest(Map<String, ?> params, String secret, String digestAlgorithm, boolean uppercase)
🗣️ 入参解释:public static String signByDigest(参数表, 密钥, 摘要算法, 是否大写)
📤 返回值解释:String,指定大小写形态的签名值
💡 边界口径:
- 只是把三参版的结果整体转大写,原文与算法口径不变
- 参数表为 null 同样抛
IllegalArgumentException
🧪 方法示例:
signByDigest(params, SECRET, ApiSignUtils.ALG_MD5, true)拿到D7922D5EF9AB694BC6B87ABB5D6C5F86
🔢 方法序号:9
🎯 方法用途:
- 参数表一步出 HMAC 签名,新接入的首选写法
- 不想自己先调一次拼原文时,用它把两步并成一步
🧩 方法定义:public static String signByHmac(Map<String, ?> params, String key, String hmacAlgorithm)
🗣️ 入参解释:public static String signByHmac(参数表, 密钥, HMAC算法)
📤 返回值解释:String,小写十六进制的签名值
💡 边界口径:
- 与先
buildSignSource再signSourceByHmac结果同值,两条路径可以混用 - 算法白名单、密钥与原文的校验口径与原文级入口一致
🧪 方法示例:
signByHmac(params, SECRET, "HmacSHA256")拿到9aa4610b16258cd2970cdaa371c699a93ad8916889a527c89cd456c7600bf27c,与原文级入口同值
🔢 方法序号:10
🎯 方法用途:
- 参数表一步出 HMAC 签名并指定大写输出
- 对端字段要求大写时用它,其余口径与三参版相同
🧩 方法定义:public static String signByHmac(Map<String, ?> params, String key, String hmacAlgorithm, boolean uppercase)
🗣️ 入参解释:public static String signByHmac(参数表, 密钥, HMAC算法, 是否大写)
📤 返回值解释:String,指定大小写形态的签名值
💡 边界口径:
- 大写只改展示形式,验签比的是字节
- 需要去掉
=、走 URL 安全字母形的 Base64 签名不属于本类范围
🧪 方法示例:
signByHmac(params, SECRET, "HmacSHA256", true)拿到9AA4610B16258CD2970CDAA371C699A93AD8916889A527C89CD456C7600BF27C
🔢 方法序号:11
🎯 方法用途:
- 校验摘要式签名,走常量时间比较,避免按字节早退带来的计时侧信道
- 已经自己拼好原文(比如微信 JS-SDK)就验原文这一层
🧩 方法定义:public static boolean verifySourceByDigest(String signSource, String secret, String digestAlgorithm, String expectedSign)
🗣️ 入参解释:public static boolean verifySourceByDigest(签名原文, 密钥, 摘要算法, 待验签名)
📤 返回值解释:boolean,签名与原文匹配返回 true
💡 边界口径:
- 比的是解码后的字节,待验签名的大小写、首尾空白都不影响结果
- 签名值缺失、长度不对、含非十六进制字符一律判
false,不抛异常 - 会先算一次签名,所以原文、密钥、算法这类调用方错误照样抛
IllegalArgumentException - 只保证签名与原文一致,防重放还得自己配 nonce 与时间戳校验
🧪 方法示例:
verifySourceByDigest(source, SECRET, ApiSignUtils.ALG_MD5, legacyMd5)拿到true- 待验签名用大写形态传,同样拿到
true verifySourceByDigest(source, SECRET, "SM3", "anyhex")抛不支持的摘要算法: SM3,仅支持 MD5/SHA-1/SHA-256/SHA-512
🔢 方法序号:12
🎯 方法用途:
- 校验 HMAC 签名,同样走常量时间比较
- 对端把签名转成大写、或者串里带了空白时不会因为格式判错
🧩 方法定义:public static boolean verifySourceByHmac(String signSource, String key, String hmacAlgorithm, String expectedSign)
🗣️ 入参解释:public static boolean verifySourceByHmac(签名原文, 密钥, HMAC算法, 待验签名)
📤 返回值解释:boolean,签名与原文匹配返回 true
💡 边界口径:
- 待验签名先按十六进制解析,解不出就判
false - 大小写、首尾空白不参与比较
- 算法名非法、密钥空白这类调用方错误仍抛
IllegalArgumentException
🧪 方法示例:
verifySourceByHmac(source, SECRET, "HmacSHA256", hmacHex.toUpperCase())拿到true- 把签名改成小写再套上首尾空白,
verifyByHmac(params, SECRET, "HmacSHA256", " 大写串 ")仍拿到true
🔢 方法序号:13
🎯 方法用途:
- 对整张回调参数表验摘要式签名,一步完成拼原文与比较
- 支付回调通知的验签入口,
sign字段本身会被自动剔掉
🧩 方法定义:public static boolean verifyByDigest(Map<String, ?> params, String secret, String digestAlgorithm, String expectedSign)
🗣️ 入参解释:public static boolean verifyByDigest(参数表, 密钥, 摘要算法, 待验签名)
📤 返回值解释:boolean,参数表与签名匹配返回 true
💡 边界口径:
- 等于
buildSignSource(params)再走原文级验签,剔除与排序口径完全一致 - 忘了剔
sign这类字段导致验不过的情况在这一版不会发生 - 签名值非法只判
false,参数表为 null 抛异常
🧪 方法示例:
verifyByDigest(params, SECRET, ApiSignUtils.ALG_MD5, legacyMd5)拿到true
🔢 方法序号:14
🎯 方法用途:
- 对整张参数表验 HMAC 签名,公网接口收到请求时的第一道校验
- 金额、订单号被改一分都过不了,签名值本身残缺也不会把异常抛给调用方
🧩 方法定义:public static boolean verifyByHmac(Map<String, ?> params, String key, String hmacAlgorithm, String expectedSign)
🗣️ 入参解释:public static boolean verifyByHmac(参数表, 密钥, HMAC算法, 待验签名)
📤 返回值解释:boolean,参数表与签名匹配返回 true
💡 边界口径:
- 参数字段被改、被删、多出未签名字段都会判
false - 待验签名为 null、空白、长度为奇数、含非十六进制字符都判
false而不抛异常,公网收到的就是这种输入 - 值为 null 与空串的参数本来就不进原文,这类字段改了不影响验签结果
- 验过签名不等于请求没被重放,nonce 与时间戳要自己校验
🧪 方法示例:
verifyByHmac(tampered, SECRET, "HmacSHA256", hmacHex)拿到false,total_fee 被改了一分钱- 参数表原样不动时拿到
true - 待验签名传 null 拿到
false,不抛异常 - 待验签名截掉最后一位(长度变奇数)拿到
false - 待验签名塞进非十六进制字符拿到
false - 待验签名传空白串拿到
false
四、工具类源码(可直接复制)
📦 源码文件