Java 怎么生成 HMAC 签名:接口鉴权常见写法和排查方法
讲清 Java 生成 HMAC-SHA256 签名的基本流程,包括参数排序、待签名字符串、时间戳、Secret、Hex/Base64 输出和签名不一致排查。
相关工具
HMAC 签名到底解决什么问题
很多开放接口不会只靠一个固定 API Key 鉴权,而是要求客户端生成签名。签名的目的,是让服务端确认请求确实来自知道 Secret 的客户端,并且参数没有被随意篡改。HMAC 是常见签名方式之一,常见算法包括 HmacSHA256、HmacSHA1。
HMAC 签名通常不是把 Secret 直接传给服务端,而是用 Secret 对一段待签名字符串做计算,得到签名值。服务端收到请求后,用同样规则计算一次,如果结果一致,就认为请求可信。Secret 不应该出现在 URL、日志或前端代码里。
签名失败最麻烦的地方在于,它通常只返回 signature invalid,但不告诉你哪里错。可能是参数顺序错、时间戳单位错、编码错、Hex/Base64 输出错、Header 漏了字段,也可能是 Secret 用错。排查要按流程来。
生成 HMAC-SHA256 的 Java 基础代码
基础 HMAC-SHA256 代码如下:
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
public class HmacDemo {
public static String hmacSha256Hex(String secret, String message) throws Exception {
Mac mac = Mac.getInstance("HmacSHA256");
SecretKeySpec key = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
mac.init(key);
byte[] bytes = mac.doFinal(message.getBytes(StandardCharsets.UTF_8));
return HexFormat.of().formatHex(bytes);
}
}secret 是密钥,message 是待签名字符串。注意编码统一使用 UTF-8。HexFormat 是较新 Java 版本里的工具,如果项目版本较老,可以自己写字节转十六进制方法,或者使用已有工具库。
有些接口要求 Base64 输出,而不是 Hex。两者完全不同。文档写 hex、lowercase、uppercase、base64 都要看清楚。算法一样,输出格式不同,签名也会不一致。
待签名字符串比算法更容易出错
HMAC 算法本身通常不难,真正容易错的是待签名字符串。接口文档可能要求把参数按字典序排序,再拼成 key=value&key=value;也可能要求把请求方法、路径、时间戳、body 按换行拼接。你必须严格按文档来,不能凭感觉。
比如一种常见规则是:
GET
/api/orders
page=1&size=20×tamp=1720953600另一种规则可能是:
timestamp + "\n" + method + "\n" + path + "\n" + body这两种规则生成的签名完全不同。排查时,第一件事不是换算法,而是把最终参与签名的字符串打印出来。只看签名结果没有意义,因为你不知道原文是否正确。
参数排序和 URL 编码要一致
如果文档要求参数排序,通常按参数名字典序。Java 可以用 TreeMap:
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;
Map<String, String> params = new TreeMap<>();
params.put("timestamp", "1720953600");
params.put("page", "1");
params.put("size", "20");
String canonical = params.entrySet().stream()
.map(e -> e.getKey() + "=" + e.getValue())
.collect(Collectors.joining("&"));但是否需要 URL 编码,要看文档。有些接口要求先编码再签名,有些要求签名原始值,发送请求时再编码。中文、空格、斜杠、加号、百分号都可能影响签名。只要客户端和服务端处理方式不同,结果就会不一致。
空值参数也要注意。空字符串是否参与签名?null 参数是否跳过?数组参数怎么拼?body 是原始 JSON 字符串还是压缩后字符串?这些都必须按文档规定。签名接口最怕差不多,必须完全一致。
时间戳、随机数和 Header
很多签名接口会要求 timestamp 和 nonce。timestamp 用来防止请求长期有效,nonce 用来防止重复请求。时间戳单位可能是秒,也可能是毫秒。如果服务端要求秒,你传毫秒,签名字符串和服务端不同,或者直接被判断为时间超出范围。
Header 里常见字段包括 X-Api-Key、X-Timestamp、X-Nonce、X-Signature。字段名大小写一般在 HTTP 层不敏感,但签名字符串里的字段名可能敏感。文档怎么写,你就怎么拼。不要把 Header 展示名和签名字段名混在一起。
排查签名失败时,可以把 timestamp 放到时间戳工具里确认是否是当前时间附近。如果差很多,先解决时间问题。再检查随机数是否每次变化,签名是否基于同一组参数生成。
签名不一致时的排查清单
第一,确认算法名,比如 HmacSHA256,不要写成 SHA256。第二,确认 Secret 是否正确,有没有多余空格。第三,确认编码统一 UTF-8。第四,打印待签名字符串,逐字符和文档示例对比。第五,确认输出格式是 Hex 还是 Base64,大小写是否要求一致。
第六,确认参数排序、空值处理、URL 编码规则。第七,确认时间戳单位和时区。第八,确认 body 是否和实际发送的一致。有些代码先用一个 JSON 字符串签名,发送时又重新序列化,字段顺序或空格变化可能导致服务端验签失败,具体要看服务端规则是否签 body 原文。
还要注意换行符。文档示例里如果使用 \n 拼接,不要在 Windows 环境下不小心变成 \r\n。签名计算是逐字节的,多一个回车、少一个空行、末尾多一个换行,都会导致结果不同。排查时可以把待签名字符串的长度也打印出来。
Secret 管理也很重要。不要把 Secret 写死在前端、仓库或公开文档里。服务端项目可以放在环境变量、密钥管理服务或部署平台配置中。测试环境和生产环境的 Secret 通常不同,调试时要确认自己使用的是对应环境的密钥。
工具帮的 HMAC 工具适合做独立核对。把 Secret 和待签名字符串复制进去,选择同样算法和输出格式,看结果是否和 Java 输出一致。如果工具和 Java 一致,但服务端仍失败,问题多半在待签名规则或请求字段,而不是 HMAC 实现。
常见问题
HMAC 和普通 SHA256 有什么区别?
SHA256 是哈希算法,HMAC-SHA256 是用密钥参与计算的消息认证码,更适合接口签名。
签名结果应该用 Hex 还是 Base64?
看接口文档。算法相同但输出格式不同,结果也不同。
为什么本地算的签名和服务端不一致?
多数问题出在待签名字符串、参数排序、编码、时间戳单位或输出格式,而不在 HMAC 算法本身。