KMS 密钥管理服务 — 介绍、搭建与使用

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 内存中持有密钥 服务器磁盘泄露不影响密钥安全
相关推荐
安当加密1 个月前
汽车密钥管理:从“一把钥匙开所有门“到“一车一密“的进化之路
kms·hsm·v2x·汽车网络安全·车联网安全·secoc·汽车密钥管理
安当加密2 个月前
汽车密钥管理系统怎么设计?从HSM到云端KMS的完整架构方案
国密·kms·hsm·密钥管理·汽车安全
飞斯柯罗2 个月前
【专栏】面向控制器开发商的适配型汽车网络安全战略(以KMS为例)
kms·汽车网络安全·gb44495·unr155·secoc·secureflash·控制器安全
SAP Hua3 个月前
WINDOWS SERVER 2008 KMS 服务器安装及激活
kms
安当加密03013 个月前
汽车 ECU “一芯一证” 实现详解:头部车企四级密钥体系实践
汽车·kms·车联网·供应链安全·国密算法·汽车行业·ota安全
撞强1 年前
本地KMS服务器激活常用命令
服务器·kms·slmgr
开着拖拉机回家1 年前
【Ambari】Ranger KMS
hadoop·ambari·kms·ranger kms
京东云技术团队3 年前
K8S集群中使用JDOS KMS服务对敏感数据安全加密 | 京东云技术团队
安全·kubernetes·数据安全·京东云·kms