KMS 密钥管理服务 --- 介绍、搭建与使用
一、KMS 介绍
1.1 是什么
KMS(Key Management Service)是一个独立部署的服务,专门负责密钥的生成、存储、分发、轮转和销毁。业务系统需要加解密时,通过 API 向 KMS 申请密钥,用完后密钥只保留在内存中,不持久化到业务系统。
1.2 行业现有方案
| 方案 | 类型 | 特点 |
|---|---|---|
| AWS KMS | 云服务 | 与 AWS 生态深度集成,按调用次数计费 |
| 阿里云 KMS | 云服务 | 支持托管 HSM,合规认证齐全 |
| Google Cloud KMS | 云服务 | 支持非对称加密和签名 |
| Azure Key Vault | 云服务 | 支持证书管理 |
| HashiCorp Vault | 开源自建 | 功能最全面,支持动态密钥、PKI、数据库凭证等 |
| 自研 KMS | 内部服务 | 如本项目中的 jsh-kms-service,按需定制 |
1.3 核心能力
┌────────────────────────────────────────────────┐
│ KMS 核心能力 │
├────────────────────────────────────────────────┤
│ 密钥生成 │ 生成 AES/RSA 等各种密钥 │
│ 密钥存储 │ 加密存储在 HSM 或加密数据库中 │
│ 密钥分发 │ 通过 API + 认证 + HTTPS 安全分发 │
│ 密钥版本 │ 同一密钥支持多版本,平滑轮转 │
│ 访问控制 │ 哪个服务能访问哪个密钥 │
│ 审计日志 │ 记录每次密钥访问的时间、调用方 │
│ 密钥销毁 │ 过期密钥安全销毁,不可恢复 │
└────────────────────────────────────────────────┘
注:
博客:
https://blog.csdn.net/badao_liumang_qizhi
二、自建 KMS 服务搭建
以下以 Spring Boot 为基础搭建一个简化版 KMS 服务,展示核心架构。
2.1 项目结构
kms-service/
├── pom.xml
└── src/main/
├── java/com/example/kms/
│ ├── KmsApplication.java // 启动类
│ ├── controller/
│ │ └── SecretKeyController.java // 密钥分发API
│ ├── service/
│ │ ├── SecretKeyService.java // 密钥管理核心逻辑
│ │ └── MasterKeyService.java // 主密钥管理
│ ├── entity/
│ │ └── SecretKeyEntity.java // 密钥存储实体
│ ├── repository/
│ │ └── SecretKeyRepository.java // 密钥存储层
│ ├── dto/
│ │ ├── SecretKeyRequestDto.java // 请求入参
│ │ └── SecretKeyResponseDto.java // 响应出参
│ └── config/
│ └── SecurityConfig.java // OAuth2资源服务器配置
└── resources/
└── application.yml
2.2 pom.xml(KMS 服务端)
xml
<project>
<groupId>com.example</groupId>
<artifactId>kms-service</artifactId>
<version>1.0.0</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.15</version>
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security.oauth</groupId>
<artifactId>spring-security-oauth2</artifactId>
<version>2.5.2.RELEASE</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
</dependencies>
</project>
2.3 数据库设计
sql
-- 密钥存储表(密钥以加密形式存储,由主密钥保护)
CREATE TABLE secret_key (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
key_name VARCHAR(100) NOT NULL COMMENT '密钥名称',
key_type VARCHAR(50) NOT NULL COMMENT '密钥类型(phone/address/id_number)',
version INT NOT NULL COMMENT '密钥版本',
-- 以下两个字段用主密钥加密后存储,非明文!
encrypted_key TEXT NOT NULL COMMENT '加密后的数据密钥',
encrypted_iv TEXT NOT NULL COMMENT '加密后的IV',
status VARCHAR(20) DEFAULT 'ACTIVE' COMMENT '状态:ACTIVE/ROTATED/DESTROYED',
create_time DATETIME NOT NULL,
expire_time DATETIME COMMENT '过期时间',
UNIQUE KEY uk_type_version (key_type, version)
) COMMENT='密钥存储表';
-- 密钥访问审计日志
CREATE TABLE secret_key_audit_log (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
key_type VARCHAR(50) NOT NULL,
key_version INT NOT NULL,
caller_service VARCHAR(100) NOT NULL COMMENT '调用方服务名',
caller_ip VARCHAR(50) NOT NULL,
action VARCHAR(20) NOT NULL COMMENT '动作:GET/CREATE/ROTATE/DESTROY',
result VARCHAR(20) NOT NULL COMMENT '结果:SUCCESS/DENIED',
create_time DATETIME NOT NULL
) COMMENT='密钥访问审计日志';
2.4 application.yml(KMS 服务端配置)
yaml
server:
port: 8100
ssl:
enabled: true # 必须开启HTTPS
key-store: classpath:keystore.p12
key-store-password: changeit
spring:
datasource:
url: jdbc:mysql://localhost:3306/kms_db
username: kms_admin
password: ${KMS_DB_PASSWORD} # 从环境变量读取
jpa:
hibernate:
ddl-auto: validate
# 主密钥配置(生产环境应从HSM或安全硬件获取)
kms:
master-key: ${KMS_MASTER_KEY} # 从环境变量或HSM读取,绝不写在配置文件
master-iv: ${KMS_MASTER_IV}
security:
oauth2:
resource:
jwt:
key-value: |
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A...
-----END PUBLIC KEY-----
2.5 MasterKeyService(主密钥管理)
java
package com.example.kms.service;
import javax.crypto.Cipher;
import javax.crypto.spec.GCMParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
/**
* 主密钥管理服务.
* 主密钥用于加解密数据密钥(信封加密).
*
* 生产环境中主密钥应存储在 HSM(硬件安全模块)中,
* 此处简化为从环境变量读取.
*/
@Service
public class MasterKeyService {
private static final String ALGORITHM = "AES/GCM/NoPadding";
private static final int GCM_TAG_BITS = 128;
private final byte[] masterKey;
private final byte[] masterIv;
public MasterKeyService(
@Value("${kms.master-key}") String masterKeyBase64,
@Value("${kms.master-iv}") String masterIvBase64) {
this.masterKey = Base64.getDecoder().decode(masterKeyBase64);
this.masterIv = Base64.getDecoder().decode(masterIvBase64);
}
/**
* 用主密钥加密数据密钥(存储时使用).
*/
public String encryptDataKey(String plainDataKey) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(masterKey, "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_BITS, masterIv);
cipher.init(Cipher.ENCRYPT_MODE, keySpec, gcmSpec);
byte[] encrypted = cipher.doFinal(plainDataKey.getBytes("UTF-8"));
return Base64.getEncoder().encodeToString(encrypted);
}
/**
* 用主密钥解密数据密钥(分发时使用).
*/
public String decryptDataKey(String encryptedDataKey) throws Exception {
Cipher cipher = Cipher.getInstance(ALGORITHM);
SecretKeySpec keySpec = new SecretKeySpec(masterKey, "AES");
GCMParameterSpec gcmSpec = new GCMParameterSpec(GCM_TAG_BITS, masterIv);
cipher.init(Cipher.DECRYPT_MODE, keySpec, gcmSpec);
byte[] decoded = Base64.getDecoder().decode(encryptedDataKey);
byte[] decrypted = cipher.doFinal(decoded);
return new String(decrypted, "UTF-8");
}
}
2.6 SecretKeyService(密钥管理核心逻辑)
java
package com.example.kms.service;
import com.example.kms.dto.SecretKeyResponseDto;
import com.example.kms.entity.SecretKeyEntity;
import com.example.kms.repository.SecretKeyRepository;
import java.security.SecureRandom;
import java.util.Base64;
import java.util.Date;
import javax.annotation.Resource;
import org.springframework.stereotype.Service;
/**
* 密钥管理核心服务.
* 负责密钥的生成、存储、分发和轮转.
*/
@Service
public class SecretKeyService {
@Resource
private SecretKeyRepository secretKeyRepository;
@Resource
private MasterKeyService masterKeyService;
@Resource
private AuditLogService auditLogService;
/**
* 获取数据密钥(分发给业务服务).
* 从数据库读取加密的密钥 → 用主密钥解密 → 返回明文密钥.
*/
public SecretKeyResponseDto getSecretKey(String keyType, Integer version,
String callerService, String callerIp) {
// 1. 查询密钥记录
SecretKeyEntity entity = secretKeyRepository.findByKeyTypeAndVersion(keyType, version);
if (entity == null || !"ACTIVE".equals(entity.getStatus())) {
auditLogService.log(keyType, version, callerService, callerIp, "GET", "DENIED");
throw new RuntimeException("密钥不存在或已失效");
}
try {
// 2. 用主密钥解密数据密钥(信封加密的逆过程)
String plainKey = masterKeyService.decryptDataKey(entity.getEncryptedKey());
String plainIv = masterKeyService.decryptDataKey(entity.getEncryptedIv());
// 3. 记录审计日志
auditLogService.log(keyType, version, callerService, callerIp, "GET", "SUCCESS");
// 4. 返回明文密钥(通过 HTTPS 传输)
SecretKeyResponseDto response = new SecretKeyResponseDto();
response.setSecretKey(plainKey);
response.setSecretKeyIv(plainIv);
return response;
} catch (Exception e) {
auditLogService.log(keyType, version, callerService, callerIp, "GET", "ERROR");
throw new RuntimeException("密钥解密失败", e);
}
}
/**
* 生成新密钥(管理员操作).
*/
public void generateKey(String keyType, Integer version) throws Exception {
// 1. 生成随机 AES-256 密钥
SecureRandom random = new SecureRandom();
byte[] keyBytes = new byte[32]; // 256位
random.nextBytes(keyBytes);
String plainKey = Base64.getEncoder().encodeToString(keyBytes);
// 2. 生成随机 IV
byte[] ivBytes = new byte[12]; // GCM 推荐 96位
random.nextBytes(ivBytes);
String plainIv = Base64.getEncoder().encodeToString(ivBytes);
// 3. 用主密钥加密后存储(信封加密)
String encryptedKey = masterKeyService.encryptDataKey(plainKey);
String encryptedIv = masterKeyService.encryptDataKey(plainIv);
// 4. 持久化(存储的是密文,即使数据库泄露也安全)
SecretKeyEntity entity = new SecretKeyEntity();
entity.setKeyName(keyType + "_v" + version);
entity.setKeyType(keyType);
entity.setVersion(version);
entity.setEncryptedKey(encryptedKey);
entity.setEncryptedIv(encryptedIv);
entity.setStatus("ACTIVE");
entity.setCreateTime(new Date());
secretKeyRepository.save(entity);
}
/**
* 密钥轮转.
* 将旧版本标记为 ROTATED,生成新版本.
*/
public void rotateKey(String keyType) throws Exception {
// 1. 查找当前激活版本
SecretKeyEntity current = secretKeyRepository.findFirstByKeyTypeAndStatusOrderByVersionDesc(
keyType, "ACTIVE");
int newVersion = (current == null) ? 1 : current.getVersion() + 1;
// 2. 旧版本标记为已轮转(仍可用于解密旧数据)
if (current != null) {
current.setStatus("ROTATED");
secretKeyRepository.save(current);
}
// 3. 生成新版本密钥
generateKey(keyType, newVersion);
}
}
2.7 SecretKeyController(密钥分发 API)
java
package com.example.kms.controller;
import com.example.kms.dto.SecretKeyRequestDto;
import com.example.kms.dto.SecretKeyResponseDto;
import com.example.kms.service.SecretKeyService;
import javax.annotation.Resource;
import javax.servlet.http.HttpServletRequest;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
/**
* 密钥分发 API.
* 所有请求必须携带有效的 OAuth2 Token.
* 通过 HTTPS 传输密钥数据.
*/
@RestController
@RequestMapping("/api/inner/secret")
public class SecretKeyController {
@Resource
private SecretKeyService secretKeyService;
/**
* 获取数据密钥.
* 业务服务调用此接口获取加解密所需的密钥.
*/
@PostMapping("/get-secret-key")
public Result<SecretKeyResponseDto> getSecretKey(
@RequestBody SecretKeyRequestDto request,
HttpServletRequest httpRequest) {
// 从 Token 中获取调用方服务名
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
String callerService = auth.getName();
String callerIp = getClientIp(httpRequest);
SecretKeyResponseDto response = secretKeyService.getSecretKey(
request.getKeyType(),
request.getVersion(),
callerService,
callerIp);
return Result.success(response);
}
private String getClientIp(HttpServletRequest request) {
String ip = request.getHeader("X-Forwarded-For");
if (ip == null || ip.isEmpty()) {
ip = request.getRemoteAddr();
}
return ip;
}
}
2.8 SecurityConfig(访问控制)
java
package com.example.kms.config;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.oauth2.config.annotation.web.configuration.EnableResourceServer;
import org.springframework.security.oauth2.config.annotation.web.configuration.ResourceServerConfigurerAdapter;
/**
* KMS 安全配置.
* 所有密钥分发接口必须携带有效的 OAuth2 Bearer Token.
*/
@Configuration
@EnableResourceServer
public class SecurityConfig extends ResourceServerConfigurerAdapter {
@Override
public void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
// 密钥管理接口需要管理员角色
.antMatchers("/api/admin/**").hasRole("KMS_ADMIN")
// 密钥获取接口需要服务角色
.antMatchers("/api/inner/**").hasRole("KMS_CLIENT")
// 其他请求拒绝
.anyRequest().denyAll();
}
}
三、业务服务侧使用
3.1 引入 KMS 客户端 SDK
xml
<dependency>
<groupId>com.example</groupId>
<artifactId>kms-client-starter</artifactId>
<version>1.0.0</version>
</dependency>
3.2 配置 KMS 连接信息
yaml
app:
kms:
# KMS 服务地址(HTTPS)
secret-url: https://kms.example.com/api/inner/secret/get-secret-key
# 认证信息(用于获取调用 KMS 的 Token)
auth:
url: https://auth.example.com/oauth/token
client-id: xxx-service-kms
client-password: ${KMS_CLIENT_SECRET} # 从环境变量读取
username: stock-service
password: ${KMS_SERVICE_PASSWORD}
token-pre-expire: 120
# 当前密钥版本
version: 2
# 历史兼容版本(用于解密旧数据)
version-history: 1
3.3 业务代码使用
java
@Service
public class CustomerService {
/**
* 新建客户(加密敏感信息后存库).
*/
@Transactional
public void createCustomer(CustomerDto dto) {
Customer customer = new Customer();
customer.setName(dto.getName());
// 加密手机号(使用当前版本密钥 v2)
customer.setPhone(
EncryptBusUtils.encrypt(dto.getPhone(), "2", "phone"));
// 加密地址
customer.setAddress(
EncryptBusUtils.encrypt(dto.getAddress(), "2", "address"));
// 加密身份证号
customer.setIdNumber(
EncryptBusUtils.encrypt(dto.getIdNumber(), "2", "id_number"));
customerRepository.save(customer);
// 数据库中存储的是: ENC:a1b2c3d4...(密文)
}
/**
* 查询客户详情(解密展示).
*/
public CustomerDto getCustomerDetail(Long id) {
Customer customer = customerRepository.findById(id).get();
CustomerDto dto = new CustomerDto();
dto.setName(customer.getName());
// 解密(自动识别密文版本)
dto.setPhone(EncryptBusUtils.decrypt(customer.getPhone(), "2", "phone"));
dto.setAddress(EncryptBusUtils.decrypt(customer.getAddress(), "2", "address"));
dto.setIdNumber(EncryptBusUtils.decrypt(customer.getIdNumber(), "2", "id_number"));
return dto;
}
/**
* 客户列表(脱敏展示,不解密).
*/
public List<CustomerListDto> listCustomers() {
List<Customer> customers = customerRepository.findAll();
return customers.stream().map(c -> {
CustomerListDto dto = new CustomerListDto();
dto.setName(c.getName());
// 解密后脱敏
String phone = EncryptBusUtils.decrypt(c.getPhone(), "2", "phone");
dto.setPhone(EncryptBusUtils.maskData(phone, CipherTextTypeEnums.PHONE));
// 显示: 138****5678
return dto;
}).collect(Collectors.toList());
}
}
四、生产环境部署要点
| 要点 | 说明 |
|---|---|
| KMS 独立部署 | 与业务服务隔离,独立网络段 |
| HTTPS 必须 | 密钥传输通道必须加密 |
| 主密钥保护 | 生产环境主密钥存放在 HSM(如 AWS CloudHSM、阿里云加密机) |
| 最小权限 | 每个业务服务只能访问自己需要的密钥类型 |
| 高可用 | KMS 多实例部署 + 数据库主从 |
| 审计日志 | 每次密钥访问都记录,定期审计异常访问 |
| 密钥轮转策略 | 建议每季度轮转一次,保留历史版本用于解密旧数据 |
| 灾备 | 主密钥有安全备份机制,KMS 不可用时的降级策略 |
| 网络隔离 | KMS 只允许内网访问,不暴露公网 |
五、与 HashiCorp Vault 的对比
如果不想自建 KMS,HashiCorp Vault 是最常用的开源方案:
| 维度 | 自建 KMS | HashiCorp Vault |
|---|---|---|
| 部署复杂度 | 低(普通 Spring Boot 服务) | 中(需要 Consul/etcd 做存储后端) |
| 功能范围 | 仅密钥管理 | 密钥管理 + 动态密钥 + PKI + 数据库凭证 + SSH 证书 |
| 密封/解封 | 无 | 有(启动时需要解封,安全性更高) |
| UI 管理 | 需自建 | 自带 Web UI |
| 社区生态 | 无 | 丰富的插件和集成 |
| 定制灵活性 | 高(完全自主) | 中(按 Vault 的设计理念使用) |
| 适合场景 | 团队小、需求简单、已有认证体系 | 中大型团队、多种密钥类型、合规要求高 |
六、关键设计总结
| 设计要点 | 实现方式 | 收益 |
|---|---|---|
| 信封加密 | 主密钥加密数据密钥,数据密钥加密业务数据 | 即使数据库泄露,没有主密钥也无法解密 |
| 密钥分层 | Master Key → Data Key → 业务数据 | 轮转 Data Key 无需重加密主密钥 |
| 版本化 | 每个密钥有版本号,密文中标识版本 | 密钥轮转不影响旧数据解密 |
| 审计追踪 | 每次密钥访问记录日志 | 安全事件可追溯 |
| 内存缓存 | 业务侧 ConcurrentHashMap 缓存密钥 | 减少 KMS 调用,提升性能 |
| 认证鉴权 | OAuth2 Token + 角色控制 | 只有授权服务才能获取密钥 |
| HTTPS 传输 | 密钥只通过加密通道传输 | 防中间人窃取 |
| 密钥不落地 | 业务侧只在 JVM 内存中持有密钥 | 服务器磁盘泄露不影响密钥安全 |