1. 概述

本文将深入探讨如何使用 Java 的 KeyStore API 管理加密密钥和证书。我们将从基础概念出发,逐步演示创建、加载、存储和删除密钥库条目的完整流程。

2. 密钥库基础

密钥库(Keystore)是 Java 中用于安全存储加密密钥和证书的机制。本质上是一个通过别名(alias)标识的密钥条目集合,可存储私钥、公钥、密钥和受信任证书。

Java 支持多种密钥库类型,各有特定格式和用途:

  • JKS(Java Keystore):Java 默认密钥库类型,主要用于存储密钥对和证书
  • PKCS12:更通用的密钥证书存储格式,跨平台兼容性强
  • JCEKS(Java Cryptography Extension Keystore):用于存储需要更强加密的密钥
  • BKS(Bouncy Castle Keystore):由 Bouncy Castle 提供的密钥库格式

Java 运行时默认包含 JAVA_HOME/jre/lib/security/cacerts 文件作为受信任证书存储。 该文件包含来自可信证书颁发机构(CA)的证书。

默认访问密码是 changeit,但生产环境务必修改。可通过 -Dkeystore.password 系统属性配置密码。

3. KeyStore 类详解

KeyStore 类位于 java.security 包,是管理密钥和证书的核心 API。主要方法包括:

  • *getInstance(String type)*:创建指定类型的密钥库实例(如 JKS、PKCS12)
  • *store(OutputStream stream, char[] password)*:将密钥库保存到输出流
  • *load(InputStream stream, char[] password)*:从输入流加载密钥库,传 null 初始化空库
  • *setEntry(String alias, KeyStore.Entry entry, KeyStore.ProtectionParameter protParam)*:添加/更新条目
  • *getEntry(String alias, KeyStore.ProtectionParameter protParam)*:通过别名获取条目
  • *getCertificate(String alias)*:获取证书条目
  • *setCertificateEntry(String alias, Certificate cert)*:添加/更新证书条目
  • *containsAlias(String alias)*:检查别名是否存在
  • *aliases()*:返回所有别名的枚举

4. 创建密钥库

4.1. 构造实例

可通过 keytool 或编程方式创建:

KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());

默认类型为 JKS,但支持多种格式(如 jcekspkcs12)。可通过系统参数覆盖:

-Dkeystore.type=pkcs12

或直接指定格式:

KeyStore ks = KeyStore.getInstance("pkcs12");

4.2. 初始化

创建后需调用 load 方法:

char[] pwdArray = "password".toCharArray();
ks.load(null, pwdArray);

✅ 传 null 作为流参数表示新建密钥库
⚠️ 密码设为 null 会导致密钥不安全

4.3. 持久化存储

最后保存到文件系统:

try (FileOutputStream fos = new FileOutputStream("newKeyStoreFileName.jks")) {
    ks.store(fos, pwdArray);
}

注意:getInstanceloadstore 都会抛出受检异常,需妥善处理。

5. 加载密钥库

加载现有密钥库需指定格式:

KeyStore ks = KeyStore.getInstance("JKS");
ks.load(new FileInputStream("newKeyStoreFileName.jks"), pwdArray);

常见错误场景:

类型不匹配

java.security.KeyStoreException: KEYSTORE_TYPE not found

密码错误

java.security.UnrecoverableKeyException: Password verification failed

6. 存储条目

密钥库支持三种条目类型:

  • 对称密钥(JCE 中的 Secret Key)
  • 非对称密钥(JCE 中的 Public/Private Key)
  • 受信任证书

6.1. 存储对称密钥

最简单的存储场景,需要三个要素:

  1. 别名:后续访问的标识符
  2. 密钥:包装为 KeyStore.SecretKeyEntry
  3. 密码:包装为 KeyStore.ProtectionParameter
KeyStore.SecretKeyEntry secret
 = new KeyStore.SecretKeyEntry(secretKey);
KeyStore.ProtectionParameter password
 = new KeyStore.PasswordProtection(pwdArray);
ks.setEntry("db-encryption-secret", secret, password);

⚠️ 密码不能为 null,但可为空字符串:

java.security.KeyStoreException: non-null password required to create SecretKeyEntry

为何需要包装?
setEntry 是通用方法,通过条目类型区分处理;密码包装支持 GUI/CLI 回调获取密码。

6.2. 存储私钥

存储非对称密钥更复杂,需处理证书链。推荐使用专用方法 setKeyEntry,需要四个要素:

  1. 别名
  2. 私钥:必须是 PrivateKey 实例
  3. 密码:必填
  4. 证书链:验证对应公钥
X509Certificate[] certificateChain = new X509Certificate[2];
chain[0] = clientCert;
chain[1] = caCert;
ks.setKeyEntry("sso-signing-key", privateKey, pwdArray, certificateChain);

常见错误:

密码为 null

java.security.KeyStoreException: password can't be null

密码为空数组(踩坑警告):

java.security.UnrecoverableKeyException: Given final block not properly padded

更新条目:使用相同别名和新密钥/证书链再次调用即可。

6.3. 存储受信任证书

存储受信任证书最简单,只需别名和证书:

ks.setCertificateEntry("google.com", trustedCertificate);

⚠️ KeyStore 不会验证证书有效性,存储前需自行验证。

更新证书:使用相同别名和新证书再次调用。

7. 读取条目

7.1. 读取单个条目

通过别名直接获取:

Key ssoSigningKey = ks.getKey("sso-signing-key", pwdArray);
Certificate google = ks.getCertificate("google.com");

行为特点:

别名不存在或类型不匹配时返回 null

public void whenEntryIsMissingOrOfIncorrectType_thenReturnsNull() {
    // ... 初始化密钥库
    // ... 添加名为 "widget-api-secret" 的条目

   Assert.assertNull(ks.getKey("some-other-api-secret"));
   Assert.assertNotNull(ks.getKey("widget-api-secret"));
   Assert.assertNull(ks.getCertificate("widget-api-secret")); 
}

密钥密码错误(再次踩坑):

java.security.UnrecoverableKeyException: Given final block not properly padded

7.2. 检查别名存在性

无需获取条目即可检查存在性:

public void whenAddingAlias_thenCanQueryWithoutSaving() {
    // ... 初始化密钥库
    // ... 添加名为 "widget-api-secret" 的条目

    assertTrue(ks.containsAlias("widget-api-secret"));
    assertFalse(ks.containsAlias("some-other-api-secret"));
}

7.3. 检查条目类型

entryInstanceOf 方法可同时检查存在性和类型:

public void whenAddingAlias_thenCanQueryByType() {
    // ... 初始化密钥库
    // ... 添加名为 "widget-api-secret" 的密钥条目

    assertTrue(ks.containsAlias("widget-api-secret"));
    assertFalse(ks.entryInstanceOf(
      "widget-api-secret",
      KeyType.PrivateKeyEntry.class));
}

8. 删除条目

删除操作简单直接:

public void whenDeletingAnAlias_thenIdempotent() {
    // ... 初始化密钥库
    // ... 添加名为 "widget-api-secret" 的条目

    assertEquals(ks.size(), 1);

    ks.deleteEntry("widget-api-secret");
    ks.deleteEntry("some-other-api-secret"); // 重复删除无影响

    assertEquals(ks.size(), 0);
}

deleteEntry 是幂等操作,无论条目是否存在行为一致。

9. 删除密钥库

API 不提供直接删除方法,需借助 Java IO:

Files.delete(Paths.get(keystorePath));

或清空所有条目保留文件:

Enumeration<String> aliases = keyStore.aliases();
while (aliases.hasMoreElements()) {
    String alias = aliases.nextElement();
    keyStore.deleteEntry(alias);
}

10. 总结

本文系统介绍了 KeyStore API 的核心功能:

  1. 密钥库类型选择(JKS/PKCS12/JCEKS)
  2. 创建与加载密钥库的完整流程
  3. 三种条目类型的存储方式:
    • 对称密钥(需密码包装)
    • 私钥(需证书链)
    • 受信任证书(最简单)
  4. 条目读取与类型检查技巧
  5. 安全删除条目与密钥库

掌握这些操作后,你就能在 Java 应用中安全地管理密钥和证书了。实际开发中建议优先使用 PKCS12 格式,并特别注意密码管理——生产环境务必使用强密码!


原始标题:Java KeyStore API | Baeldung

« 上一篇: Java 包装类详解