接口签名Webhook验签,这个Java工具类13个方法常量时间比对
## 📋 目录 - 一、工具类功能 - 二、所需环境 - 三、方法与调用示例(按需调用) - 四、工具类源码(可直接复制) ## 一、工具类功能 给接口签名、Webhook 验签、令牌防篡改算 HMAC 并常量时间比对,支持 HmacMD5 到 HmacSHA512 四种算法,输出十六进制、Base64 或原始字节,新接入一律选 HmacSHA256。 ## 二、所需环境 JDK 17或以上 ## 三、方法与调用示例(按需调用) 🔢 **方法序号**:1 🎯 **方法用途**: - 算 HmacMD5 并给小写十六进制串,输出固定 32 位 - 只为还在用 HmacMD5 的遗留系统保留,新接入别选它 🧩 **方法定义**:`public static String hmacMd5(String data, String key)` 🗣️ **入参解释**:`public static String hmacMd5(正文, 密钥)` 📤 **返回值解释**:`String`,32 位小写十六进制的 MAC 💡 **边界口径**: - 正文与密钥都按 UTF-8 取字节,双方必须约定同一套编码 - 空正文是合法输入,只有 null 被拒绝 - 正文为 null 抛 `IllegalArgumentException`,密钥为 null 或空白同样抛 - MD5 已经不该再用于新签名方案,能用 SHA-256 就用 SHA-256 🧪 **方法示例**: - `hmacMd5(BODY, SECRET)` 拿到 `d6dcbdcd2d45983d049e4e7003fc3b95` - 正文传 null 抛 `待认证数据不能为null` - 密钥给空白串抛 `密钥不能为null或空白` --- 🔢 **方法序号**:2 🎯 **方法用途**: - 算 HmacSHA1 并给小写十六进制串,输出 40 位 - 部分开放平台的历史签名要求是它,新接入不选 🧩 **方法定义**:`public static String hmacSha1(String data, String key)` 🗣️ **入参解释**:`public static String hmacSha1(正文, 密钥)` 📤 **返回值解释**:`String`,40 位小写十六进制的 MAC 💡 **边界口径**: - 与 `hmacMd5` 同一条路径,只是算法换成 `HmacSHA1` - 参数校验、UTF-8 取字节、空正文合法这些口径完全一致 🧪 **方法示例**: - `hmacSha1(BODY, SECRET)` 拿到 `61dfc9cbe98cfafc960917405584cfa91564b3fc`,长度 40 --- 🔢 **方法序号**:3 🎯 **方法用途**: - 算 HmacSHA256 并给小写十六进制串,新接入的推荐选择 - 接口签名、Webhook 校验、令牌防篡改都从它起步 🧩 **方法定义**:`public static String hmacSha256(String data, String key)` 🗣️ **入参解释**:`public static String hmacSha256(正文, 密钥)` 📤 **返回值解释**:`String`,64 位小写十六进制的 MAC 💡 **边界口径**: - 等价于 `hmacHex(HMAC_SHA256, data, key)`,输出长度固定 64 位 - 同一输入反复计算结果稳定,可以拿它做缓存键与重放对比 - `Mac` 实例非线程安全,本类每次调用新建,可并发使用 🧪 **方法示例**: - `hmacSha256(BODY, SECRET)` 拿到 `d237de3947138319e4b6d18eaca7d0e26a86816f95b9dd3e0eb6fa21482ea191` - `hmacSha256("", SECRET)` 拿到 `2dadd0e065f61424af1a9e2ab71633c0e7bc6a18b34f06fd1da80ef0ca9e7150`,空正文照样出签名 - 上一步的空串签名再交给 `verifyHex` 验,拿到 `true` --- 🔢 **方法序号**:4 🎯 **方法用途**: - 算 HmacSHA512 并给小写十六进制串,输出 128 位 - 对方协议点名要 SHA-512,或想要更长摘要时用 🧩 **方法定义**:`public static String hmacSha512(String data, String key)` 🗣️ **入参解释**:`public static String hmacSha512(正文, 密钥)` 📤 **返回值解释**:`String`,128 位小写十六进制的 MAC 💡 **边界口径**: - 原始字节固定 64 个,编码成十六进制才是 128 位,签名字段要留够宽度 - 口径与其余具名入口一致,只是摘要更长 🧪 **方法示例**: - `hmacSha512(BODY, SECRET)` 拿到 `823a8add31a9b58ea2ddf4ea4cdc318925b4aa853739715ff63c873787d43a373f8950506d61b858855a9dbb6050585f05ce01b36772fbf94e3516c2805940c4` - `hmac("HmacSHA512", "payload-with-中文", "secret-key").length` 拿到 `64` --- 🔢 **方法序号**:5 🎯 **方法用途**: - 指定算法名算 HMAC 出十六进制,算法名不必逐字对得上 JCA - 配置项里存的常常是 `HMAC-SHA256`、`hmac_sha256` 这类产品名,靠它归一命中 🧩 **方法定义**:`public static String hmacHex(String algorithm, String data, String key)` 🗣️ **入参解释**:`public static String hmacHex(算法名, 正文, 密钥)` 📤 **返回值解释**:`String`,小写十六进制的 MAC 💡 **边界口径**: - 归一规则是去首尾空白、转大写、去掉 `_` 与 `-`,四个算法的别名都能命中 - 别名表之外的名字抛 `IllegalArgumentException` 并列出支持范围,不做模糊猜测 - 算法名为 null 或空白同样抛异常 - 结果是稳定值,同一输入换台机器也算得一样 🧪 **方法示例**: - `hmacHex(HmacUtils.HMAC_SHA256, BODY, SECRET)` 拿到 `d237de3947138319e4b6d18eaca7d0e26a86816f95b9dd3e0eb6fa21482ea191` - `hmacHex("HMAC-SHA256", BODY, SECRET)` 也拿到 `d237de3947138319e4b6d18eaca7d0e26a86816f95b9dd3e0eb6fa21482ea191` - `hmacHex("hmac_sha256", BODY, SECRET)` 同样拿到 `d237de3947138319e4b6d18eaca7d0e26a86816f95b9dd3e0eb6fa21482ea191` - `hmacHex("SM4", BODY, SECRET)` 抛 `不支持的HMAC算法: SM4,仅支持 HmacMD5/HmacSHA1/HmacSHA256/HmacSHA512` - `hmacHex(" ", BODY, SECRET)` 抛 `HMAC算法不能为null或空白` --- 🔢 **方法序号**:6 🎯 **方法用途**: - 指定算法算 HMAC 并输出标准 Base64,含 padding - 签名要放进 header 或 JSON 字段、嫌十六进制太长时用这一版 🧩 **方法定义**:`public static String hmacBase64(String algorithm, String data, String key)` 🗣️ **入参解释**:`public static String hmacBase64(算法名, 正文, 密钥)` 📤 **返回值解释**:`String`,标准 Base64 形态的 MAC 💡 **边界口径**: - 与 `hmacHex` 是同一份字节的两种编码,长度约为十六进制的四分之三 - 出的是标准字母表;对方给 URL 安全或无 padding 的写法,验签侧仍然认 - 参数校验口径同 `hmacHex` 🧪 **方法示例**: - `hmacBase64(HmacUtils.HMAC_SHA256, BODY, SECRET)` 拿到 `0jfeOUcTgxnkttGOrKfQ4mqGgW+Vud0+Drb6IUguoZE=` - `verifyBase64(HmacUtils.HMAC_SHA512, BODY, SECRET, b64)` 拿到 `true`,b64 就是 `hmacBase64(HmacUtils.HMAC_SHA512, BODY, SECRET)` 的结果 --- 🔢 **方法序号**:7 🎯 **方法用途**: - 字节形态的十六进制出口,密钥含 `0x00` 与高字节也不失真 - 数据是已序列化的报文、文件内容时走这个入口,别先转成字符串 🧩 **方法定义**:`public static String hmacHex(String algorithm, byte[] data, byte[] key)` 🗣️ **入参解释**:`public static String hmacHex(算法名, 字节正文, 字节密钥)` 📤 **返回值解释**:`String`,小写十六进制的 MAC 💡 **边界口径**: - 字节按原样进 MAC 计算,不做任何字符集解释 - 空密钥数组被拒:空密钥的 MAC 没有意义,不该静默算出一个值 - 正文是空数组则合法 🧪 **方法示例**: - `hmacHex("HmacSHA256", raw, rawKey)` 拿到 `b759f847ecaa80661dc79a4a11012258261078bced6a4d27148041b96dac3632`,rawKey 是含 `0x00` 的四个字节 - `hmacHex("HmacSHA256", new byte[]{1}, new byte[0])` 抛 `密钥字节不能为null或空数组` - 字符串入口 `hmacHex("HmacSHA256", "payload-with-中文", "secret-key")` 与字节入口比对,拿到 `true` --- 🔢 **方法序号**:8 🎯 **方法用途**: - 字节形态的 Base64 出口,二进制内容要放进文本字段时用 - 密钥本身是字节数组的配置项走这一版 🧩 **方法定义**:`public static String hmacBase64(String algorithm, byte[] data, byte[] key)` 🗣️ **入参解释**:`public static String hmacBase64(算法名, 字节正文, 字节密钥)` 📤 **返回值解释**:`String`,标准 Base64 形态的 MAC 💡 **边界口径**: - 与字节版 `hmacHex` 同一份字节,只是换了编码 - 校验口径与字节版 `hmacHex` 一致 🧪 **方法示例**: - `hmacBase64("HmacSHA256", raw, rawKey)` 拿到 `t1n4R+yqgGYdx5pKEQEiWCYQeLztak0nFIBBuW2sNjI=` --- 🔢 **方法序号**:9 🎯 **方法用途**: - 取 MAC 的原始字节,编码方式与后续派生自己决定 - 要把签名再拼进密钥派生、或自己转成别的编码时用 🧩 **方法定义**:`public static byte[] hmac(String algorithm, String data, String key)` 🗣️ **入参解释**:`public static byte[] hmac(算法名, 正文, 密钥)` 📤 **返回值解释**:`byte[]`,MAC 原始字节,SHA-256 是 32 个 💡 **边界口径**: - 返回的数组与 `hmacHex`、`hmacBase64` 是同一份字节 - 数组可写可改,不想被下游改动就自己复制一份 - 字符串入口固定按 UTF-8 取字节 🧪 **方法示例**: - `hmac(HmacUtils.HMAC_SHA256, BODY, SECRET)` 拿到 `32 字节` - `hmacHex(HmacUtils.HMAC_SHA256, BODY, SECRET).length() == macBytes.length * 2` 拿到 `true`,两种出口同源 --- 🔢 **方法序号**:10 🎯 **方法用途**: - 字节形态取 MAC 原始字节,用于原始报文与文件内容 - 二进制协议要比字节签名时先用它拿值 🧩 **方法定义**:`public static byte[] hmac(String algorithm, byte[] data, byte[] key)` 🗣️ **入参解释**:`public static byte[] hmac(算法名, 字节正文, 字节密钥)` 📤 **返回值解释**:`byte[]`,MAC 原始字节 💡 **边界口径**: - 与字符串入口取的是同一套 UTF-8 字节,两侧结果一致 - 空密钥数组抛 `IllegalArgumentException`,空正文数组合法 🧪 **方法示例**: - `Arrays.equals(macBytes, stringPath)` 拿到 `true`,两个入口算出的是同一份 MAC --- 🔢 **方法序号**:11 🎯 **方法用途**: - 校验十六进制 MAC,常量时间比较,忽略预期值的大小写与空白 - 对方给的签名可能是大写、可能带分组空格时用它,别直接比字符串 🧩 **方法定义**:`public static boolean verifyHex(String algorithm, String data, String key, String expectedHex)` 🗣️ **入参解释**:`public static boolean verifyHex(算法名, 正文, 密钥, 预期签名)` 📤 **返回值解释**:`boolean`,比对通过返回 `true` 💡 **边界口径**: - 比的是解码后的字节,`d237…` 与 `D237…` 判成同一个签名 - 预期值里的空白会被剥掉,分组换行不影响判定;正文本身的空白不参与忽略 - 预期值是非法十六进制或长度为奇数时判不通过,返回 `false` 而不是抛异常 - 预期值为 null 或空白属参数问题,抛 `IllegalArgumentException` 🧪 **方法示例**: - `verifyHex(HmacUtils.HMAC_SHA512, BODY, SECRET, HmacUtils.hmacHex(HmacUtils.HMAC_SHA512, BODY, SECRET).toUpperCase())` 拿到 `true` - `verifyHex(HmacUtils.HMAC_SHA512, BODY + " ", SECRET, HmacUtils.hmacHex(HmacUtils.HMAC_SHA512, BODY, SECRET))` 拿到 `false`,正文多一个空格就判不过 - 预期值截成奇数长度 `d237de3` 时拿到 `false` - 预期值传 null 抛 `预期MAC不能为null或空白` --- 🔢 **方法序号**:12 🎯 **方法用途**: - 校验 Base64 MAC,标准与 URL 安全、有无需 padding、折行写法都能识别 - 网关把签名头折过行、前端给的串没补 `=` 时用这一版 🧩 **方法定义**:`public static boolean verifyBase64(String algorithm, String data, String key, String expectedBase64)` 🗣️ **入参解释**:`public static boolean verifyBase64(算法名, 正文, 密钥, 预期签名)` 📤 **返回值解释**:`boolean`,比对通过返回 `true` 💡 **边界口径**: - 解码依次试标准、URL 安全、补 padding 三种写法,解开再比字节 - 三种写法都解不开就判不通过返回 `false`,不把网关错误记成系统故障 - 预期值为 null 或空白属参数问题,抛 `IllegalArgumentException` 🧪 **方法示例**: - `verifyBase64(HmacUtils.HMAC_SHA512, BODY, SECRET, b64)` 标准形态拿到 `true` - 同一个 MAC 换成 URL 安全且无 padding 的 `urlSafeNoPadding` 仍拿到 `true` - 中间插了回车换行的 `folded` 也拿到 `true` - 预期值是垃圾串时拿到 `false` --- 🔢 **方法序号**:13 🎯 **方法用途**: - 直接比对 MAC 原始字节,二进制协议与自定义编码报文用 - 手里已经是 `byte[]` 时省掉再编码成字符串的一步 🧩 **方法定义**:`public static boolean verify(String algorithm, byte[] data, byte[] key, byte[] expectedMac)` 🗣️ **入参解释**:`public static boolean verify(算法名, 字节正文, 字节密钥, 预期字节)` 📤 **返回值解释**:`boolean`,字节完全一致返回 `true` 💡 **边界口径**: - 用 `MessageDigest.isEqual` 常量时间比较,长度不同直接判不通过 - 预期字节为 null 返回 `false` 而不抛异常,这点与两个字符串版验签入口不同 - 正文与密钥的校验口径同 `hmac` 字节入口 🧪 **方法示例**: - `verify(HmacUtils.HMAC_SHA256, BODY.getBytes(StandardCharsets.UTF_8), SECRET.getBytes(StandardCharsets.UTF_8), macBytes)` 拿到 `true` - 预期字节传 null 拿到 `false`,不抛异常 ## 四、工具类源码(可直接复制) 📦 评论区留言