这个Java工具类14个方法兼容微信支付宝
## 📋 目录 - [一、工具类功能](#sec-func) - [二、所需环境](#sec-env) - [三、方法与调用示例(按需调用)](#sec-method) ## 一、工具类功能 参数表按 key 字典序拼成签名原文,再走摘要式或 HMAC-SHA256 出签名,回调验签、请求签名、兼容微信支付宝老协议都能用,拿回来是十六进制签名串与验签结论。 ## 二、所需环境 JDK 17或以上 ## 三、方法与调用示例(按需调用) 🔢 **方法序号**:1 🎯 **方法用途**: - 把参数表按 key 字典序拼成签名原文,`sign`、`sign_type` 与空值参数一律不进原文 - 对接支付网关、开放平台回调时先拿它出原文,再交给具体算法 🧩 **方法定义**:`public static String buildSignSource(Map 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=1` - `buildSignSource(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 params, Set 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=1` - `buildSignSource(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)` 拿到 `0f0d73df94313475f5585dcb66f4caabecc465f01605630afbb02dfa13b18491` - `signSourceByDigest(jsApiSource, SECRET, ApiSignUtils.ALG_SHA256)` 拿到 `8b9bb3ee34396e862a5bb946a7c63b4cad0a1bcc6839372547df4594b0317ce5` - `signSourceByDigest("", SECRET, ApiSignUtils.ALG_MD5)` 拿到 `0b4ad1d96492c0442d79efa9125434f4`,空原文也能算 - `signSourceByDigest(null, SECRET, ApiSignUtils.ALG_SHA256)` 抛 `签名原文不能为null` - `signByDigest(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)` 拿到 `0F0D73DF94313475F5585DCB66F4CAABECC465F01605630AFBB02DFA13B18491` - `verifySourceByDigest(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")` 拿到 `9aa4610b16258cd2970cdaa371c699a93ad8916889a527c89cd456c7600bf27c` - `signSourceByHmac(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 params, String secret, String digestAlgorithm)` 🗣️ **入参解释**:`public static String signByDigest(参数表, 密钥, 摘要算法)` 📤 **返回值解释**:`String`,小写十六进制的签名值 💡 **边界口径**: - 原文口径与 `buildSignSource(params)` 完全一致,`sign` 与空值不参与 - 回调验签时对方带回来的 `sign` 会被自动剔掉,不会把自己算进去 - MD5 只用于兼容,新接入别用 🧪 **方法示例**: - `signByDigest(params, SECRET, ApiSignUtils.ALG_MD5)` 拿到 `d7922d5ef9ab694bc6b87abb5d6c5f86` - `signByDigest(params, SECRET, ApiSignUtils.ALG_SHA256)` 拿到 `0f0d73df94313475f5585dcb66f4caabecc465f01605630afbb02dfa13b18491`,与先拼原文再签同值 --- 🔢 **方法序号**:8 🎯 **方法用途**: - 参数表一步出摘要式签名并指定大写 Hex - 老协议里要求签名字段大写的网关用这一版 🧩 **方法定义**:`public static String signByDigest(Map 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 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 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 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 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`