Java RSA密钥生成实战:SecureRandom的坑与最佳实践

Java RSA密钥生成实战:SecureRandom的坑与最佳实践

1. 项目概述:RSA密钥生成,一个被忽视的“暗礁”

在Java开发中,尤其是涉及安全、支付、身份认证的场景,RSA非对称加密算法几乎是标配。我们常常熟练地敲下KeyPairGenerator.getInstance("RSA"),然后调用generateKeyPair(),一个公私钥对就“轻松”到手了。看起来一切都很简单,不是吗?但正是这种“简单”的认知,让很多项目在安全性和稳定性上埋下了不易察觉的隐患。我见过太多线上事故,从偶发的加密解密失败,到更隐蔽的安全风险,追根溯源,问题往往就出在密钥生成这个最初的环节。

这个内容,就是专门来聊聊Java里生成RSA密钥对的那些“坑”。它不仅仅是写给安全工程师看的,更是每一位需要在Java项目中使用RSA的开发者——无论是处理用户密码、API签名、还是配置文件加密——都应该了解的实战经验。我们会深入一个最核心也最容易被误解的组件:SecureRandom,并探讨其“种子”的最佳实践。理解了这些,你才能确保生成的密钥是真正“强壮”且“可靠”的,而不是一个看似可用、实则脆弱的“纸老虎”。

2. 核心需求解析:为什么“正确”生成密钥如此重要?

在深入技术细节之前,我们必须先达成一个共识:密钥是密码学系统的基石。如果基石不稳,无论上层的加密算法多么坚固、协议设计多么精妙,整个系统都可能轰然倒塌。对于RSA来说,密钥生成的质量直接决定了两个核心属性:安全强度系统稳定性

2.1 安全强度:伪随机性的致命陷阱

RSA算法的安全性,建立在“大整数分解是困难问题”这一数学假设上。而生成一个安全的大素数(p和q),其本质是一个随机过程。如果这个随机过程是可预测的,或者随机性不足,那么攻击者就有可能推测出你使用的素数,从而轻松破解私钥。

Java中,密钥生成器的随机源默认(且通常)就是SecureRandom。如果SecureRandom的种子(seed)熵源不足或可预测,那么它产生的“随机”数序列就是可重现的。想象一下,攻击者如果能够复现你生成密钥时的随机数序列,他就能生成和你一模一样的密钥对。这绝非危言耸听,历史上因随机数生成器缺陷导致的安全事件屡见不鲜。

核心需求一:必须确保密钥生成过程的随机性具有高熵且不可预测。这直接关系到私钥的保密性。

2.2 系统稳定性:性能与阻塞的隐形杀手

另一个常被忽视的方面是性能。SecureRandom在初始化时,需要收集足够的“熵”(可以理解为环境的随机噪声,如鼠标移动、键盘敲击、系统中断时间等)来作为种子。在Linux服务器上,默认的熵源是/dev/random。当系统熵池不足时,从/dev/random读取的操作会阻塞,直到收集到足够的熵。

这意味着什么?意味着你的应用在启动时,或者在高并发场景下首次调用密钥生成时,可能会被“卡住”。我亲身经历过一个案例:一个微服务在K8s中频繁重启,每次启动时都需要初始化一个RSA密钥用于内部通信。在容器化环境中,系统活动很少,熵池增长缓慢,导致服务启动时间从2秒延长到超过30秒,触发了健康检查超时,陷入无限重启循环。

核心需求二:必须避免密钥生成过程因熵源不足而导致线程阻塞,影响系统启动速度和响应能力。这关系到服务的可用性和SLA。

2.3 功能正确性:跨平台与跨版本的一致性

Java应用可能部署在Windows开发机、Linux生产服务器,或者不同的JDK版本上。不同平台、不同JDK版本对SecureRandom的默认实现可能有差异。如果不加注意,你可能会发现,在本地测试正常的密钥生成逻辑,到了服务器上却变慢了,或者(在极端情况下)由于默认算法提供商不同,导致生成的密钥格式有细微差别,进而引发后续加密解密或签名验签的失败。

核心需求三:需要保证密钥生成行为在不同部署环境下的一致性和可预期性。这关系到开发和运维的顺畅度。

综上所述,正确生成RSA密钥对,绝不仅仅是调用一个API那么简单。它需要我们主动管理随机数生成器,平衡安全、性能和一致性这三方面的需求。接下来,我们就拆解最常见的坑和最佳实践。

3. 核心细节解析与实操要点

理解了“为什么”,我们来看看“怎么做”。本节将深入KeyPairGeneratorSecureRandom的交互细节,并指出几个关键的操作要点。

3.1 坑点一:默认的SecureRandom及其阻塞风险

在Java中,如果你不显式指定,KeyPairGenerator会使用一个默认的SecureRandom实例。这个默认实例的行为取决于JDK实现和配置。

// 这是最常见的写法,也是隐患最大的写法之一 KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048); // 使用默认的SecureRandom KeyPair keyPair = keyGen.generateKeyPair();

在Linux环境下,许多JDK版本(如OpenJDK)默认使用NativePRNG算法,其种子源可能指向/dev/random。如前所述,在熵不足时,/dev/random会阻塞。对于2048位或更长的RSA密钥,初始化需要不少随机字节,阻塞风险很高。

注意:并非所有环境都默认阻塞。例如,在Windows上或某些Linux发行版配置中,可能会使用非阻塞的熵源。但依赖这种“运气”不是工程师应有的态度。

实操要点一:永远不要依赖默认的随机数生成器行为用于密钥生成。你应该显式地创建并配置一个SecureRandom实例。

3.2 坑点二:使用new SecureRandom()的误区

既然默认的有问题,那我显式new一个总行了吧?

SecureRandom random = new SecureRandom(); KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048, random);

这比默认的稍好,因为new SecureRandom()通常会使用一个预先配置好的、性能较好的算法(在大多数现代JDK上,是SHA1PRNGDRBG)。但是,这里仍然存在两个问题:

  1. 种子初始化时机new SecureRandom()在构造函数中可能会立即进行种子初始化。如果此时系统熵不足,它可能会回退到使用阻塞的熵源,或者使用一个确定性(但密码学安全)的种子。这个行为并不完全透明。
  2. 算法强度SHA1PRNG这个算法名听起来有点过时(SHA-1),虽然它在内部实现上可能已经加强,但从心理和名义上,对于生成密钥这种核心操作,我们最好选择更现代、更被推荐的算法。

实操要点二:避免简单使用无参构造函数。应指定算法,并理解其种子行为。

3.3 坑点三:手动设置固定种子

这是一个非常危险但偶尔会被“聪明”开发者使用的反模式:为了“确保”每次生成的密钥相同(例如,为了方便测试),有人会这样做:

SecureRandom random = new SecureRandom(); random.setSeed("myFixedSeed".getBytes()); // 致命错误! KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048, random);

setSeed方法的作用是补充熵,而不是重置替换熵。然而,如果你在生成随机数之前就调用setSeed,并且这个种子是固定值,那么SecureRandom产生的序列就完全由这个固定种子决定了,变得完全可预测。这等同于你亲手把密钥送给了攻击者。

警告:绝对禁止为用于生产环境密钥生成的SecureRandom设置固定或可预测的种子。

实操要点三:setSeed仅用于补充额外熵,绝不能作为唯一熵源。生产密钥时,严禁使用固定种子。

3.4 最佳实践核心:正确初始化SecureRandom

那么,正确的方式是什么?我们的目标是:获取一个密码学强度高、非阻塞、且种子熵充足的SecureRandom实例。

方案一:使用明确的算法和提供者(推荐)

从JDK 8开始,SecureRandom.getInstanceStrong()方法被引入。这个方法会返回一个被配置为使用高强度算法和熵源的实例。在Linux上,它通常会选择NativePRNGBlocking或类似的实现,但其内部可能采用了混合熵池等机制,性能比直接读/dev/random要好。但请注意,根据官方文档,它仍然可能阻塞,尤其是在虚拟机启动初期。

更可控的方式是指定一个非阻塞的、现代的算法:

// 尝试获取一个非阻塞的、基于DRBG(确定性随机比特生成器)的实例,这是现代标准 SecureRandom random; try { // DRBG是NIST推荐的标准,在JDK 9+上广泛支持 random = SecureRandom.getInstance("DRBG"); } catch (NoSuchAlgorithmException e) { // 回退方案:使用SHA1PRNG,但明确指定从/dev/urandom播种 random = SecureRandom.getInstance("SHA1PRNG"); // 关键步骤:在生成任何随机数之前,先调用nextBytes“激发”一下。 // 对于SHA1PRNG,这通常会使其从默认的熵源(很可能是/dev/urandom)获取种子。 random.nextBytes(new byte[1]); } KeyPairGenerator keyGen = KeyPairGenerator.getInstance("RSA"); keyGen.initialize(2048, random); KeyPair keyPair = keyGen.generateKeyPair();

方案二:配置系统属性(运维层面)

对于容器化部署,一个常见的实践是在JVM启动参数中,强制指定使用非阻塞的熵源:

-Djava.security.egd=file:/dev/./urandom

这个参数历史悠久,有点“黑魔法”的味道(注意路径里的./是为了绕过某些旧版本JDK的缓存机制)。它告诉SecureRandom使用/dev/urandom作为熵源。/dev/urandom是一个非阻塞的伪随机数生成器,它在熵不足时会用内部的算法继续生成数据,而不会阻塞。

优缺点分析

  • 优点:一劳永逸地解决整个JVM内所有SecureRandom调用的阻塞问题。
  • 缺点:这是一个全局设置,影响所有应用。虽然对于绝大多数场景,使用/dev/urandom是安全且推荐的(连Linux内核的维护者也这么认为),但某些极端严格的安全审计可能会要求使用/dev/random。此外,它依赖于宿主机系统的/dev/urandom设备。

实操要点四:对于服务器端应用,尤其是容器化应用,优先考虑使用-Djava.security.egd=file:/dev/./urandomJVM参数。并结合代码中显式使用SecureRandom.getInstance("DRBG")或类似算法,实现双保险。

4. 实操过程与核心环节实现

现在,让我们将上述要点整合成一个完整的、生产可用的RSA密钥对生成工具类。这个类会处理算法选择、异常回退,并包含必要的日志记录。

4.1 构建健壮的SecureRandom工厂方法

首先,我们创建一个专门用于生成安全随机数实例的工具方法。

import lombok.extern.slf4j.Slf4j; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; @Slf4j public class CryptoUtils { /** * 获取一个适用于密钥生成的、非阻塞的SecureRandom实例。 * 优先级:DRBG > SHA1PRNG (with urandom seeding) * @return 配置好的SecureRandom实例 */ public static SecureRandom createSecureRandomForKeys() { SecureRandom secureRandom = null; String preferredAlgorithm = "DRBG"; String fallbackAlgorithm = "SHA1PRNG"; try { // 尝试首选算法 secureRandom = SecureRandom.getInstance(preferredAlgorithm); log.info("成功创建高强度SecureRandom实例,算法: {}", preferredAlgorithm); // 对于DRBG,通常不需要额外的nextBytes调用,其初始化是自包含的。 } catch (NoSuchAlgorithmException e) { log.warn("未找到首选算法[{}],将使用备用算法[{}]。原因:{}", preferredAlgorithm, fallbackAlgorithm, e.getMessage()); try { // 使用备用算法 secureRandom = SecureRandom.getInstance(fallbackAlgorithm); // **关键步骤**:对于SHA1PRNG,调用nextBytes可以触发其从默认熵源(通常是urandom)获取种子。 // 这行代码是避免阻塞的关键。生成1个字节的随机数,迫使它完成初始化。 secureRandom.nextBytes(new byte[1]); log.info("已创建备用SecureRandom实例,算法: {},并完成初始播种。", fallbackAlgorithm); } catch (NoSuchAlgorithmException ex) { // 理论上,SHA1PRNG是所有JDK标准实现都支持的,但为了绝对安全,保留此异常处理。 log.error("连备用算法[{}]也未找到,将使用默认构造函数。这是不安全的,请检查JRE安全配置。", fallbackAlgorithm, ex); secureRandom = new SecureRandom(); // 最后的手段 } } return secureRandom; } }

代码解析

  1. 算法优先级:我们首选DRBG,这是NIST标准,在现代JDK(9+)中普遍支持且设计精良。如果不支持,则回退到经典的SHA1PRNG
  2. 种子初始化:对于SHA1PRNG,仅仅getInstance是不够的。调用nextBytes(new byte[1])会强制它进行实际的种子初始化。在大多数Linux系统的默认配置下,这个初始化过程会从/dev/urandom读取种子,从而避免阻塞。这是一个非常重要的技巧。
  3. 日志记录:记录所使用的算法,便于后期监控和问题排查。如果连SHA1PRNG都找不到,说明运行环境极其异常,必须发出错误日志。

4.2 实现可配置的RSA密钥对生成器

接下来,我们利用上面的SecureRandom工厂来构建密钥对生成器。

import java.security.KeyPair; import java.security.KeyPairGenerator; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; public class RSAKeyPairGenerator { /** * 生成RSA密钥对 * @param keySize 密钥长度,如 2048, 3072, 4096 * @return 生成的KeyPair * @throws RuntimeException 如果密钥生成失败 */ public static KeyPair generateKeyPair(int keySize) { if (keySize < 2048) { throw new IllegalArgumentException("RSA密钥长度至少应为2048位,当前请求:" + keySize); } try { // 1. 获取健壮的SecureRandom SecureRandom secureRandom = CryptoUtils.createSecureRandomForKeys(); // 2. 获取RSA密钥对生成器实例 KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance("RSA"); // 3. 使用我们配置的SecureRandom初始化生成器 keyPairGenerator.initialize(keySize, secureRandom); // 4. 生成密钥对 return keyPairGenerator.generateKeyPair(); } catch (NoSuchAlgorithmException e) { // RSA算法是JRE标准必须支持的,此异常通常意味着环境被严重破坏 throw new RuntimeException("当前Java环境不支持RSA算法", e); } } /** * 生成密钥对,并返回Base64编码的字符串形式(便于存储和传输) * @param keySize 密钥长度 * @return 包含公钥和私钥Base64字符串的Map */ public static Map<String, String> generateKeyPairBase64(int keySize) { KeyPair keyPair = generateKeyPair(keySize); Map<String, String> keyMap = new HashMap<>(); // 获取Base64编码的字符串 String publicKeyBase64 = Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()); String privateKeyBase64 = Base64.getEncoder().encodeToString(keyPair.getPrivate().getEncoded()); keyMap.put("publicKey", publicKeyBase64); keyMap.put("privateKey", privateKeyBase64); return keyMap; } }

代码解析

  1. 参数校验:首先检查密钥长度。目前2048位是安全底线,对于新系统,建议考虑3072位。1024位已被认为不安全。
  2. 资源获取:通过工具类获取安全的SecureRandom实例。
  3. 初始化与生成:标准的KeyPairGenerator初始化流程,但传入了我们精心准备的随机源。
  4. 异常处理NoSuchAlgorithmException对于“RSA”来说几乎不可能发生,但为了代码健壮性仍需捕获。我们将其包装为RuntimeException,因为算法不支持是一个严重的环境问题。
  5. 扩展方法generateKeyPairBase64提供了直接获取Base64编码字符串的便捷方法,这在需要将密钥存入数据库、配置文件或通过API传输时非常有用。

4.3 在Spring Boot应用中的集成示例

在现代Java生态中,Spring Boot是主流。我们可以将密钥生成逻辑封装为Bean或配置属性。

方案一:作为配置Bean,在应用启动时生成

import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import lombok.Data; import java.security.KeyPair; @Configuration public class RsaKeyConfig { @Data @ConfigurationProperties(prefix = "app.rsa") public static class RsaKeyProperties { private int keySize = 2048; } @Bean public KeyPair rsaKeyPair(RsaKeyProperties properties) { log.info("正在生成RSA密钥对,长度: {} bits", properties.getKeySize()); KeyPair keyPair = RSAKeyPairGenerator.generateKeyPair(properties.getKeySize()); log.info("RSA密钥对生成完毕。"); // 这里可以进一步将公钥/私钥的Base64字符串存入环境变量或Redis,供其他服务使用 return keyPair; } }

application.yml中配置:

app: rsa: key-size: 3072 # 可根据需要调整

方案二:懒加载,按需生成

如果密钥不需要在启动时就准备好,或者不同场景需要不同的密钥,可以将其包装为一个Service。

@Service public class RsaKeyService { private final int keySize; private volatile KeyPair keyPairCache; // 简单的缓存 public RsaKeyService(@Value("${app.rsa.key-size:2048}") int keySize) { this.keySize = keySize; } public KeyPair getOrGenerateKeyPair() { if (keyPairCache == null) { synchronized (this) { if (keyPairCache == null) { keyPairCache = RSAKeyPairGenerator.generateKeyPair(keySize); } } } return keyPairCache; } public String getPublicKeyBase64() { KeyPair keyPair = getOrGenerateKeyPair(); return Base64.getEncoder().encodeToString(keyPair.getPublic().getEncoded()); } // ... 类似方法获取私钥 }

5. 常见问题与排查技巧实录

即使遵循了最佳实践,在实际部署和运行中,你仍可能遇到一些问题。下面是我在多年实践中总结的一些典型问题及其排查思路。

5.1 问题一:应用启动或首次调用密钥生成时异常缓慢

现象:在Linux服务器(特别是Docker容器)上,应用启动时间远超预期,或者第一次调用generateKeyPair时接口响应时间长达十几秒甚至几十秒。

排查步骤

  1. 检查JVM参数:首先确认是否已经设置了-Djava.security.egd=file:/dev/./urandom。如果没有,加上它是最快、最有效的解决方案。
  2. 检查系统熵池:登录服务器,执行cat /proc/sys/kernel/random/entropy_avail。这个值表示当前可用的熵估计值(单位是比特)。如果这个值持续很低(例如低于100),说明系统熵源不足。在虚拟机或容器中,这个值可能天生就低。
  3. 检查SecureRandom算法:在代码中打印出SecureRandom.getInstance(...).getAlgorithm()SecureRandom.getInstance(...).getProvider(),确认实际使用的是哪个算法和提供者。
  4. 使用性能更好的熵源:对于长期受熵不足困扰的服务器,可以安装havegedrng-tools这样的软件来增加熵。haveged会利用CPU的时间抖动来生成熵,非常适合虚拟化环境。
    # Ubuntu/Debian sudo apt-get install haveged sudo systemctl enable haveged sudo systemctl start haveged

根本原因SecureRandom实例初始化时,从阻塞型熵源(如/dev/random)读取数据,而系统熵不足,导致线程等待。

5.2 问题二:在不同环境中生成的密钥“行为”不一致

现象:在Windows上开发测试加解密都正常,部署到Linux服务器后,偶尔出现解密失败或签名验证不通过。

排查步骤

  1. 确认密钥二进制一致性:不要比较Base64字符串。将两个环境生成的密钥对分别保存为文件(getEncoded()的字节数组),用二进制比较工具(如cmp命令或编程比较)确认它们是否完全一样。如果不一样,说明随机源不同,这是预期之中的。
  2. 检查密钥规格:如果密钥本身不同,那加解密失败是正常的。你需要确保加密用的公钥和解密用的私钥是配对的。问题可能出在密钥的存储和传输环节,而不是生成环节。检查你是否错误地将环境A的公钥和环境B的私钥配对了。
  3. 检查填充方案:RSA加密解密需要指定填充方案(如PKCS1Padding,OAEPPadding)。确保在加密和解密时使用的是完全相同的填充方案。Cipher.getInstance("RSA")在不同JDK/提供商下的默认填充可能不同,这是一个经典坑。最佳实践是始终显式指定算法、模式和填充
    // 好的做法 Cipher cipher = Cipher.getInstance("RSA/ECB/PKCS1Padding"); // 更好的做法(如果使用OAEP) Cipher cipher = Cipher.getInstance("RSA/ECB/OAEPWithSHA-256AndMGF1Padding");

根本原因:问题往往不在密钥生成本身,而在于密钥的配对使用,或者后续加解密操作时算法/填充方案的不匹配。

5.3 问题三:如何安全地存储和传递生成的密钥?

生成安全的密钥只是第一步,如何保管和使用它们同样重要。

私钥存储

  • 原则:私钥必须保密。绝不能硬编码在源码中,也不应明文存储在配置文件或数据库中。
  • 推荐方案
    1. 硬件安全模块(HSM)或密钥管理服务(KMS):如阿里云KMS、AWS KMS、HashiCorp Vault。这是企业级最佳实践,私钥由服务托管,应用通过API调用进行加解密操作,私钥本身不离开安全设备。
    2. 加密后存储:如果必须由应用自己保管,应将私钥用另一个“主密钥”(可以来自环境变量或启动参数)进行对称加密(如AES-GCM),然后将加密后的密文存入环境变量、配置中心或数据库。应用启动时,读取密文并用主密钥解密后在内存中使用。
    3. 文件系统权限:如果以文件形式存储,务必设置严格的访问权限(如chmod 400 private.key),并确保备份安全。

公钥分发

  • 原则:公钥可以公开,但需确保完整性和真实性,防止被篡改替换。
  • 推荐方案
    1. 通过HTTPS API提供给客户端。
    2. 集成到应用的配置中,或存放在可信的配置中心。
    3. 对于开放API,可以提供固定的公钥端点(/api/public-key)。

5.4 一个真实的“踩坑”案例:Kubernetes中的熵危机

我曾负责一个部署在K8s上的微服务,该服务在启动时需要为每个实例生成一个唯一的RSA密钥对,用于实例间的mTLS认证。最初我们使用了类似new SecureRandom()的代码。

上线后,监控发现服务Pod在启动后,有大约5%的几率会卡在“Ready”状态之前超过30秒。检查日志,发现卡在KeyPairGenerator.initialize这一步。根本原因就是容器内熵不足。

我们的解决方案是组合拳

  1. JVM参数:在所有应用的K8s部署模板中,强制添加-Djava.security.egd=file:/dev/./urandom
  2. 代码加固:按照本文所述,重构了密钥生成工具类,使用SecureRandom.getInstance("DRBG")并做好回退处理。
  3. 基础设施层:在K8s集群的节点上(非容器内)部署了haveged守护进程,提升宿主机的熵产生速率,间接惠及容器。

实施后,服务启动时间恢复稳定在2秒以内,问题彻底解决。

6. 性能考量与密钥长度选择

生成RSA密钥对是一个CPU密集型操作,密钥长度直接影响生成时间和后续加解密性能。

6.1 密钥长度与生成时间

以下是在一台普通Linux服务器(JDK 11)上的粗略测试数据,使用我们优化后的SecureRandom

密钥长度 (bits)平均生成时间安全强度 (NIST建议)
2048~500 ms2030年之前
3072~2000 ms2030年之后
4096~6000 ms长期

结论

  • 2048位:目前最通用的选择,在安全性和性能之间取得了良好平衡。适用于大多数内部API签名、令牌加密等场景。
  • 3072位:未来证明(Future-Proof)的选择。NIST建议在2030年之后使用。如果系统生命周期较长,或处理极其敏感的数据,建议直接使用3072位。虽然生成时间更长,但通常只在应用启动或密钥轮换时执行一次,可以接受。
  • 4096位:除非有特殊的安全合规要求(如某些金融或政府标准),否则通常性能开销过大,性价比不高。

6.2 性能优化建议

  1. 懒加载与缓存:如前面Service示例所示,密钥对生成一次后应缓存起来,在整个应用生命周期内复用。绝对不要在每次需要加解密时都生成新密钥。
  2. 异步初始化:如果应用启动时必须生成密钥,且密钥长度较大(如4096),可以考虑在应用启动时使用一个单独的线程异步生成密钥,避免阻塞主线程导致服务启动超时。
  3. 考虑椭圆曲线(ECC):如果对性能要求极高,且场景允许(例如TLS证书、某些数字签名),可以考虑使用椭圆曲线密码学(ECC)。例如,256位的ECC密钥提供的安全强度相当于3072位的RSA,但密钥生成和加解密速度要快得多。Java通过KeyPairGenerator.getInstance("EC")支持。

7. 密钥生命周期管理与轮换

任何密钥都不应该无限期使用。定期的密钥轮换是安全最佳实践的一部分。

轮换策略

  1. 定期轮换:根据安全策略,设定一个密钥有效期(如1年)。在到期前,启动轮换流程。
  2. 触发式轮换:当发生安全事件(如怀疑私钥泄露)、或密钥使用的算法被发现有重大漏洞时,应立即轮换。
  3. 灰度轮换:对于在线服务,直接更换所有实例的密钥可能导致正在进行的请求失败。应采用灰度发布:
    • 生成新密钥对(KeyPair B)。
    • 逐步部署新版本应用,新版本同时支持用旧密钥(KeyPair A)解密和用新密钥(KeyPair B)加密。
    • 等所有流量都切换到新版本后,再部署一个最终版本,移除对旧密钥A的解密支持,并开始使用密钥B进行所有操作。
    • 安全地销毁旧私钥A。

实现提示:在你的RsaKeyService中,可以设计为支持多组密钥,并通过一个密钥ID(Key ID)来标识当前活跃的密钥。加解密接口需要同时传入数据和Key ID。这样,轮换就变成了更新当前活跃Key ID的操作。