SpringBoot中基于 AES-GCM + KMS 密钥管理的数据加解密 Starter 实践

SpringBoot中基于 AES-GCM + KMS 密钥管理的数据加解密 Starter 实践


一、涉及的技术知识点

1.1 数据加密算法

知识点 说明
AES(Advanced Encryption Standard) 对称加密算法,业界标准
GCM 模式(Galois/Counter Mode) AES 的认证加密模式,同时提供加密和完整性校验
GCM Tag 128位(16字节)认证标签,防篡改
IV(Initialization Vector) 初始化向量,每次加密不同 IV 保证相同明文产生不同密文
SecretKey + SecretKeyIv 密钥对:加密密钥 + 初始化向量

1.2 KMS 密钥管理服务

知识点 说明
KMS(Key Management Service) 集中式密钥管理,密钥不落地存储在业务系统
密钥版本化 支持 encryptWithVersion / decryptWithVersion,密钥轮转时兼容旧数据
密钥缓存 ConcurrentHashMap 本地缓存密钥,避免每次加解密都请求 KMS
OAuth2 认证获取密钥 调 KMS 接口前先通过 MgKmsAuthApiClient 获取 access_token

1.3 数据脱敏

知识点 说明
maskData() 根据数据类型(手机号/地址/身份证/邮箱/银行卡/税号)进行脱敏显示
CipherTextTypeEnums 定义脱敏规则(如手机号显示前3后4,地址显示前6位等)
加密前缀标识 密文带特定前缀(ENCRYPT_PREFIX_DEFAULT_PREFIX),用于判断字段是否已加密

1.4 Spring Boot 集成

知识点 说明
spring.factories 自动配置 JshKmsConfig 作为入口注册相关 Bean
@ConfigurationProperties 通过 JshKmsProperty 绑定 jsh.kms.* 配置
@Value 注入 KMS 地址、认证信息从配置文件注入
volatile 关键字 systemCloudClientToken 声明 volatile,多线程可见性

1.5 设计模式

模式 应用
工具类模式 EncryptBusUtils 提供静态加解密方法
缓存模式 EncryptKeyCacheManager 用 ConcurrentHashMap 缓存密钥
代理/门面模式 SecretKeyClient 封装 KMS 调用细节
单例模式 EncryptBusUtils 私有构造器,纯静态方法
Token 预过期刷新 authTokenPreExpire 提前刷新 Token 避免过期

注:

博客:

https://blog.csdn.net/badao_liumang_qizhi

二、包结构与组件关系

复制代码
xxx.xxx.lib.security.kms
├── xxxKmsConfig.java                      // 自动配置入口
├── prop/
│   └── xxxKmsProperty.java               // 配置属性类
├── config/
│   ├── SecretKeyClient.java              // KMS 密钥获取客户端
│   ├── EncryptKeyCacheManager.java       // 密钥本地缓存管理器
│   └── MgKmsAuthApiClient.java           // KMS 认证客户端(获取access_token)
├── dto/
│   ├── SecretKeyCache.java               // 密钥缓存对象(key + iv)
│   ├── SecretKeyParamsDto.java           // 请求KMS入参
│   ├── SecretKeyResultDto.java           // KMS返回结果
│   └── GetOpenTokenParamsDto.java        // 获取Token入参
├── enums/
│   ├── CipherTextTypeEnums.java          // 密文类型枚举(PHONE/ADDRESS/ID_NUMBER等)
│   └── SecretCacheKeyEnum.java           // 缓存Key枚举
└── util/
    └── EncryptBusUtils.java              // 核心加解密工具类(对外API)

三、核心实现流程

3.1 加密流程

复制代码
业务代码调用 EncryptBusUtils.encrypt(plainText, version)
  │
  ├─→ getSecretKeyCache(version, cipherType)
  │     ├── 先查本地缓存 EncryptKeyCacheManager.getCacheData(cacheKey)
  │     ├── 缓存未命中 → SecretKeyClient.getSecretKeyCache(version, type)
  │     │     ├── MgKmsAuthApiClient.getOpenAccessToken() → 获取KMS访问Token
  │     │     ├── HTTP POST → KMS 接口获取密钥
  │     │     └── 返回 SecretKeyCache(secretKey, secretKeyIv)
  │     └── 写入本地缓存 EncryptKeyCacheManager.setCacheData(cacheKey, cache)
  │
  ├─→ encrypt(secretKeyBytes, ivBytes, plainText)
  │     ├── Cipher.getInstance("AES/GCM/NoPadding")
  │     ├── GCMParameterSpec(GCM_TAG_LENGTH * 8, iv)
  │     ├── cipher.init(ENCRYPT_MODE, secretKeySpec, gcmSpec)
  │     ├── cipher.doFinal(plainText.getBytes())
  │     └── Base64.encode(cipherBytes) + 加密前缀
  │
  └─→ 返回密文(带前缀标识)

3.2 解密流程

复制代码
业务代码调用 EncryptBusUtils.decrypt(cipherText, version)
  │
  ├─→ encrypted(cipherText) → 检查是否有加密前缀
  │     ├── 有前缀 → 是密文,继续解密
  │     └── 无前缀 → 是明文,直接返回
  │
  ├─→ 去除加密前缀,Base64.decode → 得到加密字节数组
  │
  ├─→ getSecretKeyCache(version, cipherType) → 获取密钥(同加密流程)
  │
  ├─→ decrypt(secretKeyBytes, ivBytes, cipherBytes)
  │     ├── Cipher.getInstance("AES/GCM/NoPadding")
  │     ├── GCMParameterSpec(GCM_TAG_LENGTH * 8, iv)
  │     ├── cipher.init(DECRYPT_MODE, secretKeySpec, gcmSpec)
  │     └── new String(cipher.doFinal(cipherBytes))
  │
  └─→ 返回明文

3.3 密钥获取与缓存流程

复制代码
需要密钥
  │
  ├─→ EncryptKeyCacheManager.getCacheData(key)
  │     ├── ConcurrentHashMap.get(key)
  │     ├── 命中 → 直接返回 SecretKeyCache
  │     └── 未命中 ↓
  │
  ├─→ SecretKeyClient.getSecretKeyCache(version, type)
  │     ├── MgKmsAuthApiClient.getRequestHeaderWithToken()
  │     │     ├── 判断 Token 是否过期(volatile + 预过期时间)
  │     │     ├── 过期 → getOpenAccessToken() → 调认证中心刷新
  │     │     └── 返回带 Bearer Token 的 HttpHeaders
  │     ├── HTTP POST → kmsSecretUrl(密钥服务接口)
  │     └── 解析返回 SecretKeyResultDto → 构建 SecretKeyCache
  │
  └─→ EncryptKeyCacheManager.setCacheData(key, cache)
      └── ConcurrentHashMap.put(key, cache)

四、通用示例代码

4.1 spring.factories

properties 复制代码
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.security.kms.KmsAutoConfiguration

4.2 CipherTextTypeEnums(密文类型枚举)

java 复制代码
package com.example.security.kms.enums;

/**
 * 密文数据类型枚举.
 * 定义不同类型数据的脱敏规则.
 */
public enum CipherTextTypeEnums {

    PHONE(1, "手机号", "phone"),
    ADDRESS(2, "地址", "address"),
    ID_NUMBER(3, "身份证号", "id_number"),
    EMAIL(4, "邮箱", "email"),
    BANK_CARD(5, "银行卡号", "bank_card"),
    TAX_NUMBER(6, "税号", "tax_number");

    private final Integer code;
    private final String label;
    private final String table;

    CipherTextTypeEnums(Integer code, String label, String table) {
        this.code = code;
        this.label = label;
        this.table = table;
    }

    public static CipherTextTypeEnums getByCode(Integer code) {
        for (CipherTextTypeEnums e : values()) {
            if (e.code.equals(code)) {
                return e;
            }
        }
        return null;
    }

    public Integer getCode() { return code; }
    public String getLabel() { return label; }
    public String getTable() { return table; }
}

4.3 SecretKeyCache(密钥缓存对象)

java 复制代码
package com.example.security.kms.dto;

/**
 * 密钥缓存数据对象.
 * 包含 AES 密钥和初始化向量.
 */
public class SecretKeyCache {
    private String secretKey;
    private String secretKeyIv;

    public SecretKeyCache(String secretKey, String secretKeyIv) {
        this.secretKey = secretKey;
        this.secretKeyIv = secretKeyIv;
    }

    public String getSecretKey() { return secretKey; }
    public String getSecretKeyIv() { return secretKeyIv; }
}

4.4 EncryptKeyCacheManager(密钥缓存管理器)

java 复制代码
package com.example.security.kms.config;

import com.example.security.kms.dto.SecretKeyCache;
import java.util.concurrent.ConcurrentHashMap;

/**
 * 密钥本地缓存管理器.
 * 使用 ConcurrentHashMap 缓存从 KMS 获取的密钥,避免频繁网络请求.
 */
public class EncryptKeyCacheManager {

    private static final ConcurrentHashMap<String, SecretKeyCache> cacheData
        = new ConcurrentHashMap<>();

    private EncryptKeyCacheManager() {
    }

    public static void setCacheData(String key, SecretKeyCache cache) {
        cacheData.put(key, cache);
    }

    public static SecretKeyCache getCacheData(String key) {
        return cacheData.get(key);
    }
}

4.5 EncryptBusUtils(核心加解密工具类)

java 复制代码
package com.example.security.kms.util;

import com.example.security.kms.config.EncryptKeyCacheManager;
import com.example.security.kms.config.SecretKeyClient;
import com.example.security.kms.dto.SecretKeyCache;
import com.example.security.kms.enums.CipherTextTypeEnums;
import java.util.Base64;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * 数据加解密工具类.
 * 基于 AES-GCM 算法,密钥从 KMS 获取并本地缓存.
 * 
 * 核心特性:
 * 1. AES-256-GCM 加密(认证加密,防篡改)
 * 2. 密钥版本化支持(密钥轮转兼容旧数据)
 * 3. 密文带前缀标识(快速判断是否已加密)
 * 4. 脱敏显示(按数据类型掩码)
 */
public class EncryptBusUtils {

    private static final Logger log = LoggerFactory.getLogger(EncryptBusUtils.class);
    private static final int GCM_TAG_LENGTH = 16; // 128位认证标签
    private static final String ENCRYPT_PREFIX = "ENC:";
    private static final String AES_GCM_ALGORITHM = "AES/GCM/NoPadding";

    private static SecretKeyClient secretKeyClient;

    // 通过 Spring 注入(Setter注入,由配置类调用)
    public void setCilentBean(SecretKeyClient client) {
        secretKeyClient = client;
    }

    private EncryptBusUtils() {
    }

    /**
     * 判断字符串是否是密文(通过前缀标识).
     */
    public static Boolean encrypted(String text) {
        return text != null && text.startsWith(ENCRYPT_PREFIX);
    }

    /**
     * 加密(指定密钥版本).
     *
     * @param plainText 明文
     * @param version   密钥版本
     * @param type      数据类型
     * @return 密文(带前缀)
     */
    public static String encrypt(String plainText, String version, String type) {
        if (plainText == null || plainText.isEmpty()) {
            return plainText;
        }
        try {
            SecretKeyCache keyCache = getSecretKeyCache(version, type);
            return encrypt(keyCache, plainText);
        } catch (Exception e) {
            log.error("加密失败", e);
            return plainText;
        }
    }

    /**
     * 加密核心实现.
     */
    public static String encrypt(SecretKeyCache keyCache, String plainText) {
        try {
            byte[] keyBytes = Base64.getDecoder().decode(keyCache.getSecretKey());
            byte[] ivBytes = Base64.getDecoder().decode(keyCache.getSecretKeyIv());
            return encrypt(keyBytes, ivBytes, plainText);
        } catch (Exception e) {
            log.error("加密异常", e);
            return plainText;
        }
    }

    /**
     * 解密(指定密钥版本).
     *
     * @param cipherText 密文(带前缀)
     * @param version    密钥版本
     * @return 明文
     */
    public static String decrypt(String cipherText, String version, String type) {
        if (cipherText == null || !encrypted(cipherText)) {
            return cipherText; // 非密文直接返回
        }
        try {
            SecretKeyCache keyCache = getSecretKeyCache(version, type);
            return decrypt(keyCache, cipherText);
        } catch (Exception e) {
            log.error("解密失败", e);
            return cipherText;
        }
    }

    /**
     * 解密核心实现.
     */
    public static String decrypt(SecretKeyCache keyCache, String cipherText) {
        try {
            byte[] keyBytes = Base64.getDecoder().decode(keyCache.getSecretKey());
            byte[] ivBytes = Base64.getDecoder().decode(keyCache.getSecretKeyIv());
            // 去除前缀后解密
            String rawCipher = cipherText.substring(ENCRYPT_PREFIX.length());
            return decrypt(keyBytes, ivBytes, rawCipher);
        } catch (Exception e) {
            log.error("解密异常", e);
            return cipherText;
        }
    }

    /**
     * 数据脱敏显示.
     * 根据数据类型应用不同的掩码规则.
     *
     * @param plainText 明文
     * @param type      数据类型
     * @return 脱敏后的字符串
     */
    public static String maskData(String plainText, CipherTextTypeEnums type) {
        if (plainText == null || plainText.isEmpty()) {
            return plainText;
        }
        switch (type) {
            case PHONE:
                // 138****5678
                return plainText.substring(0, 3) + "****"
                    + plainText.substring(plainText.length() - 4);
            case ADDRESS:
                // 前6位 + ****
                return plainText.substring(0, Math.min(6, plainText.length())) + "****";
            case ID_NUMBER:
                // 前3后4
                return plainText.substring(0, 3) + "***********"
                    + plainText.substring(plainText.length() - 4);
            case EMAIL:
                int atIndex = plainText.indexOf('@');
                return plainText.substring(0, Math.min(2, atIndex)) + "****"
                    + plainText.substring(atIndex);
            case BANK_CARD:
                // 前4后4
                return plainText.substring(0, 4) + "********"
                    + plainText.substring(plainText.length() - 4);
            default:
                return "****";
        }
    }

    // ========== 内部方法 ==========

    private static String encrypt(byte[] keyBytes, byte[] ivBytes, String plainText) throws Exception {
        Cipher cipher = Cipher.getInstance(AES_GCM_ALGORITHM);
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, ivBytes);
        cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
        byte[] encrypted = cipher.doFinal(plainText.getBytes("UTF-8"));
        return ENCRYPT_PREFIX + Base64.getEncoder().encodeToString(encrypted);
    }

    private static String decrypt(byte[] keyBytes, byte[] ivBytes, String cipherText) throws Exception {
        Cipher cipher = Cipher.getInstance(AES_GCM_ALGORITHM);
        SecretKeySpec keySpec = new SecretKeySpec(keyBytes, "AES");
        GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_LENGTH * 8, ivBytes);
        cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
        byte[] decoded = Base64.getDecoder().decode(cipherText);
        byte[] decrypted = cipher.doFinal(decoded);
        return new String(decrypted, "UTF-8");
    }

    private static SecretKeyCache getSecretKeyCache(String version, String type) {
        String cacheKey = version + ":" + type;
        SecretKeyCache cache = EncryptKeyCacheManager.getCacheData(cacheKey);
        if (cache == null) {
            cache = secretKeyClient.getSecretKeyCache(version, type);
            EncryptKeyCacheManager.setCacheData(cacheKey, cache);
        }
        return cache;
    }
}

4.6 引入方 application.yml 配置

yaml 复制代码
app:
  kms:
    secret-url: https://kms.example.com/api/inner/secret/get-secret-key
    mg-um-auth:
      url: https://auth.example.com/auth
      client-id: my-service-kms
      client-password: xxxxxx
      username: kms-account
      password: kms-pwd
      token-pre-expire: 120  # 提前120秒刷新Token
    version: 1                # 当前密钥版本
    version-history: 1        # 历史密钥版本(解密旧数据用)

4.7 业务代码使用

java 复制代码
@Service
public class CustomerService {

    /**
     * 保存客户信息(加密敏感字段).
     */
    public void saveCustomer(CustomerDto dto) {
        Customer customer = new Customer();
        customer.setName(dto.getName());
        // 加密手机号和地址
        customer.setPhone(EncryptBusUtils.encrypt(dto.getPhone(), "1", "phone"));
        customer.setAddress(EncryptBusUtils.encrypt(dto.getAddress(), "1", "address"));
        customerRepository.save(customer);
    }

    /**
     * 查询客户信息(解密敏感字段).
     */
    public CustomerDto getCustomer(Integer id) {
        Customer customer = customerRepository.findById(id).get();
        CustomerDto dto = new CustomerDto();
        dto.setName(customer.getName());
        // 解密
        dto.setPhone(EncryptBusUtils.decrypt(customer.getPhone(), "1", "phone"));
        dto.setAddress(EncryptBusUtils.decrypt(customer.getAddress(), "1", "address"));
        return dto;
    }

    /**
     * 列表展示(脱敏显示).
     */
    public CustomerDto getCustomerMasked(Integer id) {
        CustomerDto dto = getCustomer(id);
        dto.setPhone(EncryptBusUtils.maskData(dto.getPhone(), CipherTextTypeEnums.PHONE));
        dto.setAddress(EncryptBusUtils.maskData(dto.getAddress(), CipherTextTypeEnums.ADDRESS));
        return dto;
        // 手机号: 138****5678
        // 地址: 山东省青岛****
    }
}

五、关键设计总结

设计要点 实现方式 收益
密钥不落盘 从 KMS 动态获取,内存缓存 密钥泄露风险最小化
密钥版本化 version 参数支持新旧密钥共存 密钥轮转时不影响历史数据解密
认证加密 AES-GCM 模式(加密 + 完整性校验) 防篡改,比 CBC 模式更安全
密文可识别 前缀 ENC: 标识 快速判断字段是否已加密,避免重复加密
加密失败不阻断 catch 异常后返回原文 加解密故障不影响核心业务流程
密钥缓存 ConcurrentHashMap 避免每次加解密都调 KMS,高性能
Token 预过期 authTokenPreExpire 提前刷新 避免 Token 过期导致获取密钥失败
脱敏显示 maskData() 按类型掩码 列表页展示安全,详情页解密展示
最小依赖 JDK 自带 javax.crypto + Base64 加密逻辑零三方依赖
相关推荐
Gofarlic_OMS1 小时前
NX浮动许可调度黑名单机制,对比两款谁更合理
java·大数据·运维·开源·制造
三言老师2 小时前
CentOS7.9:Redis服务器部署结构化实战教程
linux·运维·服务器·数据库
乐观的Terry2 小时前
8、发布系统-完整流水线的核心
java·spring boot·spring·spring cloud
Alexalbb2 小时前
从角色过滤到可审计授权:单体系统数据权限设计复盘与演进
java
番茄炒鸡蛋加糖2 小时前
MySQL 实战调优& 分表基础
数据库·mysql
万亿少女的梦1682 小时前
基于Spring Boot的游戏交易管理系统设计与实现
java·spring boot·mysql·系统设计·交易管理
Database_Cool_2 小时前
云数据库如何保证高可用、故障了怎么办:阿里云 RDS MySQL 高可用架构详解
数据库·mysql·阿里云
中微极客3 小时前
Veo视频生成与Gemini Agent平台集成实践
数据库·人工智能·oracle·音视频
闲猫3 小时前
Agent工程实践:从WorkFlow到多Agent协作的落地笔记
java·服务器·前端