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
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.security.kms.KmsAutoConfiguration
4.2 CipherTextTypeEnums(密文类型枚举)
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(密钥缓存对象)
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(密钥缓存管理器)
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(核心加解密工具类)
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 配置
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 业务代码使用
@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 |
加密逻辑零三方依赖 |