Spring Boot对访问密钥加解密——HMAC-SHA256

一、客户端示例

假设这是一个Java 客户端(可能是后端服务或桌面应用等),要调用你的服务接口 /api/secure,并用 HMAC-SHA256 做签名。

java 复制代码
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 或 Base64
        String requestBody = "{\"foo\":\"bar\"}"; // JSON
        String bodyHash = sha256Hex(requestBody);

        // 3) 拼装 StringToSign (示例逻辑,可自定义)
        //    这里用换行分隔 method, path, timestamp, bodyHash
        String stringToSign = method + "\n" 
                              + path + "\n" 
                              + timestamp + "\n" 
                              + bodyHash;

        // 4) 做 HMAC-SHA256
        String 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 计算签名 + 在请求头中带上 accessKeyIdtimestampsignature
  • bodyHash 的计算方式可自行定义,也可以用 MD5、直接放明文 body 等。只要客户端和服务端保持一致即可。

二、服务端示例 (Spring Boot)

下面以 Spring Boot + Controller 为例,展示如何验证签名。主要逻辑:

  1. 从 HTTP 头中取 X-AccessKeyId, X-Timestamp, X-Signature
  2. 根据 accessKeyId 找到 secret;
  3. 用相同的方式拼装 StringToSign
  4. 做同样的 HMAC-SHA256 计算;
  5. 比对与客户端传来的 signature 是否相同。

2.1 Controller 示例

java 复制代码
@RestController
@RequestMapping("/api")
public class SecureApiController {

    // 示例:内存中保存 keyId -> keySecret 映射
    private Map<String, String> keyStore = new HashMap<>();

    public SecureApiController() {
        // 假设这里初始化了一个myKeyId -> myKeySecret
        keyStore.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) 查找keySecret
            String 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"
            // StringToSign
            String stringToSign = method + "\n" 
                                + path + "\n" 
                                + timestamp + "\n" 
                                + bodyHash;

            // 5) 服务端做 HMAC-SHA256
            String 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());
        }
    }

    // 计算 SHA256Hex
    private 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 + Base64
    private 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 是 yyyyMMddHHmmss
            DateTimeFormatter 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;如果是对象映射,你要注意读取流计算摘要的先后顺序。
  • 你也可以在 FilterInterceptor 里做这个签名验签逻辑,避免在每个 Controller 里写。
  • timestamp 校验 + 可能的 nonce 防重放(可用 Redis 记录 5 分钟内出现过的 (accessKeyId,timestamp,nonce)),以更好地防御重复调用。

三、总结

  1. 客户端
    • 准备 accessKeyId, accessKeySecret
    • 拼出 StringToSign(通常包含 method、path、timestamp、bodyHash 等);
    • HMAC-SHA256( StringToSign, accessKeySecret ) → signature
    • 在 HTTP 请求头里带上 accessKeyId, signature, timestamp
    • 用 JSON 作为请求体时,别忘了和服务端在 bodyHash 算法上保持一致。
  2. 服务端
    • 通过 accessKeyId 找到对应的 accessKeySecret
    • 按同样规则构造 StringToSign
    • 计算 HMAC-SHA256 并和客户端的 signature 对比;
    • 一致则通过,不一致则 401/403;
    • 可加时间戳nonce限流 等加强安全性。
  3. 优点
    • 不需要 公钥/私钥,也不需要RSA 加解密;
    • 计算速度快、实现相对简单;
    • 通用性强,许多云厂商、API网关都采用类似 HMAC 签名模式。
  4. 注意
    • 一定要保护好 accessKeySecret,客户端泄露就会被冒用。
    • 使用 HTTPS 来保证传输安全,防止中间人截获签名或篡改。
    • 若对大文件、流式上传等,需要在数据处理上稍作适配(hash可能需分段计算)。

这套 HMAC-SHA256 签名鉴权 就是在很多云服务(阿里云、AWS、腾讯云)都在用的模式。只要客户端与服务端约定好 StringToSign 的拼装方式、accessKeyId → secret 映射、时间戳/nonce防重放,就能形成一套轻量、高效的对外接口鉴权机制。

相关推荐
magic 24523 分钟前
Java继承中的静态方法隐藏与实例变量隐藏:深入解析与最佳实践
java·开发语言·javase
Java 第一深情1 小时前
JVM面试题解,垃圾回收之“垃圾回收器”剖析
java·jvm·面试
for621 小时前
一文读懂fgc之cms
java·jvm·实践
花心蝴蝶.2 小时前
MyBatis 入门
java·spring boot·后端·mybatis
苹果醋32 小时前
Win10安装MySQL、Pycharm连接MySQL,Pycharm中运行Django
运维·vue.js·spring boot·nginx·课程设计
Kerwin要坚持日更2 小时前
一文讲解Java中的BIO、NIO、AIO之间的区别
java·开发语言
汤姆yu3 小时前
基于Springboot的社区药房管理系统
java·spring boot·后端·社区药房
程思扬3 小时前
Android笔记:android 动态设置backgroundTint
android·java·网络·笔记·android-studio
2的n次方_3 小时前
Eureka 服务注册和服务发现的使用
spring boot·spring cloud·云原生·eureka·服务发现
八月五3 小时前
跨域问题及解决方案
spring boot·后端