Spring Boot对访问密钥加解密——HMAC-SHA256
这篇文章资料来自于网络,是对部分知识整理,这里只是记录一下,仅供参考。
HMAC-SHA256 简介
HMAC-SHA256 是一种基于 哈希函数 的消息认证码(Message Authentication Code, MAC),它结合了哈希算法(如 SHA-256)和一个密钥,用于验证消息的完整性和真实性。
HMAC 是 “Hash-based Message Authentication Code” 的缩写,它广泛应用于网络通信中,用于保证消息在传输过程中未被篡改,同时也可以校验消息是否来自可信方。
HMAC-SHA256 的组成
HMAC-SHA256 的运算公式如下:
HMAC(key, message) = SHA256((key ⊕ opad) || SHA256((key ⊕ ipad) || message))
- key:一个秘密密钥,用于认证消息,只有通信双方知道。
- message:需要进行签名的消息内容。
- SHA256:一种安全的哈希函数,用于生成固定长度的摘要值。
- ⊕:按位异或操作(XOR)。
- opad 和 ipad:
opad:外部填充字节 (0x5c)。
ipad:内部填充字节 (0x36)。
流程解释:
- 将密钥与 ipad 结合,并将其与消息一起进行第一次哈希。
- 将密钥与 opad 结合,并将其与第一次哈希的结果一起进行第二次哈希。
- 最终输出 HMAC 值。
HMAC-SHA256 的特点
- 基于密钥:
与普通的 SHA256 不同,HMAC-SHA256 引入了密钥,只有通信双方知道密钥,保证了消息的认证性。
- 抗篡改:
如果消息在传输过程中被篡改,HMAC 认证将失败。
- 抗重放攻击:
通常结合时间戳(timestamp)或随机数(nonce),防止消息被恶意重放。
- 性能高效:
基于哈希函数的算法速度快,适合高并发场景。
- 简单易用:
常用于 REST API、签名验证和网络协议中。
HMAC-SHA256 的应用场景
- API 鉴权:
许多云服务和 API 平台(如 AWS、阿里云等)使用 HMAC-SHA256 来验证 API 请求的真实性和完整性。
- 消息完整性校验:
用于验证消息在传输过程中未被篡改。
- 数据完整性校验:
用于验证文件或数据传输的完整性(结合密钥,防止恶意伪造)。
- 通信协议:
用于加密通信协议(如 TLS)的消息认证。
- Token 签名:
用于生成和验证基于密钥的 Token,如 JWT(JSON Web Token)的签名部分。
HMAC-SHA256 的优点
- 安全性强:
基于 SHA-256 的安全性,结合密钥使用,安全性更高。
- 对抗攻击:
有效抵抗暴力破解、哈希碰撞和中间人攻击。
- 跨平台支持:
各种编程语言和库都支持 HMAC-SHA256。
- 性能优越:
计算量小,适合高性能需求的场景。
HMAC 与其他加密算法的对比
常见问题
HMAC-SHA256 与普通 SHA-256 有什么区别?
- SHA-256 是一种单向哈希算法,用于生成固定长度的摘要值;
- HMAC-SHA256 是基于 SHA-256 和密钥的消息认证码,除了生成摘要外,还能验证消息来源和完整性。
HMAC-SHA256 的密钥如何管理?
- 密钥必须严格保密,通常存储在安全的配置文件或密钥管理服务(如 AWS KMS)中。
是否需要 HTTPS?
- HMAC-SHA256 可以防止消息篡改,但不能保护消息的机密性。建议配合 HTTPS 使用。
可以用 HMAC-SHA256 替代 RSA 签名吗?
- 如果双方共享密钥,HMAC-SHA256 是更高效的选择;
- 如果需要非对称签名或验证,应该使用 RSA。
代码示例
客户端示例
假设这是一个Java客户端(可能是后端服务或桌面应用等),要调用你的服务接口 /api/secure,并用 HMAC-SHA256 做签名。
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Base64;public class HmacClientExample {public static void main(String[] args) throws Exception {// 1) 准备必要参数String accessKeyId = "myKeyId";String accessKeySecret = "myKeySecret"; // 保密String method = "POST";String path = "/api/secure";// 例如携带一个 timestamp (yyyyMMddHHmmss)String timestamp = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"));// 2) 如果有请求体,需要计算 bodyHash (这里只是示例)// 实际可对 JSON 字符串做 MD5 或 SHA256,再 Hex 或 Base64String requestBody = "{\"foo\":\"bar\"}"; // JSONString bodyHash = sha256Hex(requestBody);// 3) 拼装 StringToSign (示例逻辑,可自定义)// 这里用换行分隔 method, path, timestamp, bodyHashString stringToSign = method + "\n" + path + "\n" + timestamp + "\n" + bodyHash;// 4) 做 HMAC-SHA256String signature = hmacSha256Base64(stringToSign, accessKeySecret);// 5) 将签名和 keyId、timestamp 放到 HTTP 头部// 伪代码: 构建 HTTP 请求System.out.println("X-AccessKeyId: " + accessKeyId);System.out.println("X-Timestamp: " + timestamp);System.out.println("X-Signature: " + signature);// 之后再把 requestBody 当作 JSON 发出 (POST)// ...// 这是示例演示,真实项目中可用 HttpClient、OkHttp 等发请求}/*** 计算字符串的 SHA-256 再转 hex (可选:也可用 Base64)*/private static String sha256Hex(String data) throws Exception {MessageDigest digest = MessageDigest.getInstance("SHA-256");byte[] bytes = digest.digest(data.getBytes(StandardCharsets.UTF_8));return bytesToHex(bytes);}/*** HMAC-SHA256 + Base64*/private static String hmacSha256Base64(String data, String secret) throws Exception {Mac mac = Mac.getInstance("HmacSHA256");SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");mac.init(keySpec);byte[] rawHmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));return Base64.getEncoder().encodeToString(rawHmac);}private static String bytesToHex(byte[] bytes) {StringBuilder sb = new StringBuilder(bytes.length * 2);for (byte b : bytes) {sb.append(String.format("%02x", b));}return sb.toString();}
}
核心:
- StringToSign 拼装 + HMAC-SHA256 计算签名 + 在请求头中带上 accessKeyId、timestamp、signature。
- bodyHash 的计算方式可自行定义,也可以用 MD5、直接放明文 body 等。只要客户端和服务端保持一致即可。
服务端示例 (Spring Boot)
下面以 Spring Boot + Controller 为例,展示如何验证签名。主要逻辑:
- 从 HTTP 头中取 X-AccessKeyId, X-Timestamp, X-Signature。
- 根据 accessKeyId 找到 secret;
- 用相同的方式拼装 StringToSign;
- 做同样的 HMAC-SHA256 计算;
- 比对与客户端传来的 signature 是否相同。
2.1 Controller 示例
@RestController
@RequestMapping("/api")
public class SecureApiController {// 示例:内存中保存 keyId -> keySecret 映射private Map<String, String> keyStore = new HashMap<>();public SecureApiController() {// 假设这里初始化了一个myKeyId -> myKeySecretkeyStore.put("myKeyId", "myKeySecret");}@PostMapping("/secure")public ResponseEntity<?> secureEndpoint(HttpServletRequest request,@RequestBody(required=false) String body // raw JSON) {try {// 1) 从header读取String accessKeyId = request.getHeader("X-AccessKeyId");String timestamp = request.getHeader("X-Timestamp");String clientSignature = request.getHeader("X-Signature");if (accessKeyId == null || timestamp == null || clientSignature == null) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Missing auth headers");}// 2) 查找keySecretString keySecret = keyStore.get(accessKeyId);if (keySecret == null) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid accessKeyId");}// 3) 计算 bodyHash(可选)// 假设客户端用了 sha256Hex(body)String bodyHash = sha256Hex(body == null ? "" : body);// 4) 与客户端相同的拼接方式String method = request.getMethod(); // "POST"String path = request.getRequestURI(); // "/api/secure"// StringToSignString stringToSign = method + "\n" + path + "\n" + timestamp + "\n" + bodyHash;// 5) 服务端做 HMAC-SHA256String serverSignature = hmacSha256Base64(stringToSign, keySecret);// 6) 比对签名if (!serverSignature.equals(clientSignature)) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Signature mismatch");}// 7) 可选校验: timestamp 是否过期if (!checkTimestampValid(timestamp)) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Timestamp expired or invalid");}// 8) 一切正常return ResponseEntity.ok("Success! Request body was: " + body);} catch (Exception e) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Auth error: " + e.getMessage());}}// 计算 SHA256Hexprivate String sha256Hex(String data) throws Exception {MessageDigest md = MessageDigest.getInstance("SHA-256");byte[] digest = md.digest(data.getBytes(StandardCharsets.UTF_8));return bytesToHex(digest);}// HMAC-SHA256 + Base64private String hmacSha256Base64(String data, String secret) throws Exception {Mac mac = Mac.getInstance("HmacSHA256");SecretKeySpec keySpec = new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");mac.init(keySpec);byte[] rawHmac = mac.doFinal(data.getBytes(StandardCharsets.UTF_8));return Base64.getEncoder().encodeToString(rawHmac);}private String bytesToHex(byte[] bytes) {StringBuilder sb = new StringBuilder(bytes.length * 2);for (byte b : bytes) {sb.append(String.format("%02x", b));}return sb.toString();}// 时间戳校验 (±15分钟示例)private boolean checkTimestampValid(String timestampStr) {try {// 这里假设 timestampStr 是 yyyyMMddHHmmssDateTimeFormatter fmt = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");LocalDateTime reqTime = LocalDateTime.parse(timestampStr, fmt);LocalDateTime now = LocalDateTime.now();return !reqTime.isBefore(now.minusMinutes(15)) && !reqTime.isAfter(now.plusMinutes(15));} catch (Exception e) {return false;}}
}
注意:
- 上面为了演示方便,用 @RequestBody(required=false) String body 直接拿到原始 JSON 字符串,再做 sha256Hex;如果是对象映射,你要注意读取流和计算摘要的先后顺序。
- 你也可以在 Filter 或 Interceptor 里做这个签名验签逻辑,避免在每个 Controller 里写。
- timestamp 校验 + 可能的 nonce 防重放(可用 Redis 记录 5 分钟内出现过的 (accessKeyId,timestamp,nonce)),以更好地防御重复调用。
API请求认证
API请求认证: 通过HMAC-SHA256生成的签名,服务器能够验证请求是否由合法的客户端发送,并且数据是否被篡改。在API请求中,HMAC签名机制通过生成一个签名(sign),让服务器可以验证请求的来源是否可信、内容是否未被篡改。签名通过一个双方共享的密钥(这里是secret)和时间戳计算出来。通常这种方法用在请求的认证头或查询参数中,以增强API的安全性。
API请求认证流程: 具体实现参照API-Mail-钉钉机器人部分。
假设你有一个API服务器和一个客户端应用程序,服务器在处理请求时需要验证请求的真实性。以下是具体的操作流程:
客户端生成签名并发送请求:
假设API服务器需要每个请求包含一个签名,以验证请求是否来自合法的客户端。
客户端获取当前时间戳,比如:1698765432123。
客户端将时间戳和一个预定义的密钥(即双方已知的secret)生成一个待签名字符串(stringToSign),对应的stringToSign可能是:"1698765432123\nthis is secret"。
String stringToSign = timestamp + "\n" + secret;
然后,客户端使用HmacSHA256算法对该字符串进行加密,生成签名字节数组,并进行Base64编码和URL编码,假设生成的签名结果为:"WfD0xfJUsfN2aZbiKsY7Yqgln/Up65bMgoB5TzQsmc0%3D"。
String sign = URLEncoder.encode(new String(Base64.encodeBase64(signData)),"UTF-8");
最后,客户端将该签名以及时间戳放入请求中发送给服务器。例如,这可能是一个GET请求:
https://api.example.com/resource?timestamp=1698765432123&sign=WfD0xfJUsfN2aZbiKsY7Yqgln/Up65bMgoB5TzQsmc0%3D
服务器验证签名:
服务器接收到请求后,会获取其中的时间戳1698765432123和签名sign="WfD0xfJUsfN2aZbiKsY7Yqgln/Up65bMgoB5TzQsmc0%3D"。
服务器同样会使用相同的secret密钥,重新生成客户端的签名过程:
- 将时间戳和secret组成同样的stringToSign。
- 使用HmacSHA256加密生成签名字节数组。
- 对生成的字节数组进行Base64编码和URL编码,得到服务器计算出的签名。
服务器将计算出的签名与请求中传递的签名进行对比。如果一致,则表示该请求有效。
验证成功的意义:
- 数据完整性:因为HMAC签名是用双方共享的secret密钥生成的,若签名一致,表明数据在传输过程中没有被篡改。
- 请求来源真实性:只有知道secret密钥的客户端才能生成有效签名,因此服务器可以信任这个请求是来自合法的客户端。
过期时间的验证(防重放攻击):
- 通常,时间戳会设有过期时间,服务器只接受一定时间内的签名(比如5分钟内)。超过时间限制,签名会被认为无效,即使内容和密钥相同。
- 通过限制签名的有效时间,服务器能够抵御重放攻击,因为即便黑客拦截了签名,过期后也无法重复使用。
长度扩展攻击
为什么需要进行两次哈希: HMAC的两次哈希设计主要为了安全性,增加抗攻击性和密钥隔离能力。在具体实现中,HMAC通过内外层填充和两次哈希有效防止了长度扩展攻击,并且使得HMAC在面对不同的哈希算法时都能提供稳健的安全保证。这些特性使得HMAC成为加密协议中广泛使用的消息认证码算法。
防止长度扩展攻击
提高密钥隔离性,避免密钥泄露:
- 直接对密钥和消息进行单次哈希容易暴露一些信息。如果单次哈希时消息过长,且每次哈希都用同一个密钥,可能导致攻击者通过已知的哈希结果分析出密钥。
- HMAC通过两次哈希,使用内外层填充常量(ipad和opad)将密钥隔离起来。即便攻击者能够破解内层或外层的某一哈希,仍然难以获得原始密钥。
提高对不同哈希函数的通用性:
- HMAC最早被设计出来时,考虑到了不同的哈希算法(如SHA-1、SHA-256、MD5)都可以用作基础哈希算法。
- 通过两次哈希,HMAC的结构不依赖于某种特定哈希算法的实现,因此它在理论上对各种哈希算法都安全。
长度扩展攻击: 长度扩展攻击是一种针对基于Merkle-Damgård结构的哈希算法(如MD5、SHA-1、SHA-256等)的攻击方式。攻击者可以在已知哈希值和部分原始数据的情况下,通过附加内容来生成一个有效的哈希,从而伪造签名。
Merkle-Damgård结构简介:
Merkle-Damgård结构是一种分块的哈希算法设计方法,将输入消息分成多个块,每一块依次输入到哈希函数中,并将上一次的哈希输出作为下一次的输入。
该结构的特性导致在已知中间状态的情况下,可以利用已知的哈希结果继续进行哈希计算,模拟追加内容,产生新内容的有效哈希。
长度扩展攻击的具体示例: 假设我们有一个哈希算法SHA-256和一个服务器,该服务器用SHA-256(key + message)生成消息的签名。服务器端会将生成的签名和消息一起发送给客户端。攻击者可以利用这种机制伪造新的签名。
示例流程:
服务器生成签名
- 假设服务器有一个密钥key(只有服务器知道)和一个消息message(已知)。
- 服务器使用SHA-256(key + message)计算签名,并将结果作为签名发送给客户端。
- 比如,message是字符串"data", 签名结果是abc123...(假设这是SHA-256(key + "data")的输出)。
攻击者获得签名并伪造新消息
- 攻击者截获到这个签名abc123...和message = "data"。
- 假设攻击者想要生成一个伪造的签名,但签名内容包括新的附加内容,比如"data" + "extra_data"。
- 攻击者并不知道key,因此无法直接对"key + data + extra_data"进行哈希。但由于SHA-256的Merkle-Damgård结构,攻击者可以在已知SHA-256(key + "data")的基础上追加内容。
执行长度扩展攻击
- 攻击者通过一些数学操作,重新构造一个新的哈希输入,使得它相当于对"key + data + padding + extra_data"的哈希计算。
- 由于SHA-256允许在中间状态继续追加哈希,因此攻击者可以在SHA-256(key + "data")的基础上模拟一个追加运算,生成SHA-256(key + "data" + padding + "extra_data")的哈希结果。
- 最终,攻击者生成了一个伪造的签名abc456...(假设这是SHA-256(key + "data" + padding + "extra_data")的结果)。
伪造请求发送
- 攻击者将"data + extra_data"和伪造签名abc456...发送给服务器。
- 服务器使用相同的方法验证签名abc456...,由于生成方式相同,因此服务器会误认为这个签名是合法的。
为什么HMAC可以防止这种攻击: 在HMAC中,密钥参与了两次哈希计算。
- 内层哈希计算H((key ⊕ ipad) ∥ message),再进行外层哈希H((key ⊕ opad) ∥ 内层哈希结果)。
- 由于密钥在外层哈希中再次混入,使得攻击者即便掌握内层哈希值,也无法继续追加伪造信息。这种结构防止了长度扩展攻击。
重放攻击
重放攻击: HMAC(带密钥的哈希消息认证码)本身不能直接防止重放攻击,但它可以作为防止重放攻击的一个关键组成部分,尤其是当与时间戳或唯一标识(Nonce)结合使用时。下面具体讲解HMAC的作用和为什么它在防止重放攻击中有效。
结合HMAC、时间戳和Nonce防止重放攻击: 假设一个API请求中包含HMAC、时间戳和Nonce,以下是防止重放攻击的完整流程。
客户端生成带HMAC的请求:
- 步骤1:客户端生成请求消息(例如"user_id=123&amount=50")。
- 步骤2:客户端在请求中加入一个时间戳timestamp(表示请求的发出时间)和一个唯一标识nonce(一个随机字符串)。
- 步骤3:客户端使用共享密钥对消息、时间戳和Nonce生成HMAC签名。
- 步骤4:将消息、时间戳、Nonce和HMAC签名一起发送给服务器。
服务器验证请求:
- 步骤1:服务器接收到请求,提取出消息内容、时间戳、Nonce和HMAC签名。
- 步骤2:服务器验证时间戳,确保请求在有效时间范围内(例如,5分钟内的请求有效),若超时则拒绝请求。时间戳限制了请求的有效时间窗口,确保请求必须在一定的时间范围内发送。
- 步骤3:服务器检查nonce是否在短时间内重复(通常会将nonce存入数据库或缓存中,存储一段时间),若nonce已存在,则认为是重放请求并拒绝。Nonce保证了请求的唯一性,即便时间戳在有效期内,每个请求也需要不同的nonce,防止重复请求。
- 步骤4:服务器使用共享密钥重新计算HMAC签名并验证签名是否一致,以确保消息未被篡改。时间戳、Nonce和HMAC结合后,攻击者无法重用旧请求数据,也无法伪造新请求,从而防止了重放攻击。
请求处理与Nonce存储:
- 若验证通过,服务器处理请求并将nonce存入缓存中(比如Redis)或数据库,记录短时间内已使用过的Nonce。
- 下次如果收到相同的nonce,服务器会直接拒绝请求,因为这是一个重复的请求(重放攻击)。
nonce的失效时间的设置: 通常情况下,Nonce的存储时间与请求的有效时间窗口保持一致。也就是说,Nonce的有效存储时间和时间戳所限定的请求有效期通常相同。这样做可以确保请求的唯一性,同时节省存储资源和验证逻辑的复杂性。
防止重放攻击的一致性:
- 既然服务器要求请求必须在5分钟的时间窗口内有效,那么只需要在这5分钟内检查是否有重复的Nonce即可。
- 超过5分钟的Nonce可以安全地删除,因为即使攻击者重新发送此请求,时间戳也会超时导致请求无效。
总结
客户端
- 准备 accessKeyId, accessKeySecret;
- 拼出 StringToSign(通常包含 method、path、timestamp、bodyHash 等);
- HMAC-SHA256( StringToSign, accessKeySecret ) → signature;
- 在 HTTP 请求头里带上 accessKeyId, signature, timestamp;
- 用 JSON 作为请求体时,别忘了和服务端在 bodyHash 算法上保持一致。
服务端
- 通过 accessKeyId 找到对应的 accessKeySecret;
- 按同样规则构造 StringToSign;
- 计算 HMAC-SHA256 并和客户端的 signature 对比;
- 一致则通过,不一致则 401/403;
- 可加时间戳、nonce、限流 等加强安全性。
优点
- 不需要公钥/私钥,也不需要RSA 加解密;
- 计算速度快、实现相对简单;
- 通用性强,许多云厂商、API网关都采用类似 HMAC 签名模式。
注意
- 一定要保护好 accessKeySecret,客户端泄露就会被冒用。
- 使用 HTTPS 来保证传输安全,防止中间人截获签名或篡改。
- 若对大文件、流式上传等,需要在数据处理上稍作适配(hash可能需分段计算)。
- 这套 HMAC-SHA256 签名鉴权就是在很多云服务(阿里云、AWS、腾讯云)都在用的模式。只要客户端与服务端约定好StringToSign 的拼装方式、accessKeyId → secret 映射、时间戳/nonce防重放,就能形成一套轻量、高效的对外接口鉴权机制。
参考
https://blog.csdn.net/print_helloword/article/details/144727914
https://blog.csdn.net/CSDN_YYD_997/article/details/143369935