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,但支持多种格式(如 jceks、pkcs12)。可通过系统参数覆盖:
-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);
}
注意:getInstance、load 和 store 都会抛出受检异常,需妥善处理。
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. 存储对称密钥
最简单的存储场景,需要三个要素:
- 别名:后续访问的标识符
- 密钥:包装为 KeyStore.SecretKeyEntry
- 密码:包装为 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,需要四个要素:
- 别名
- 私钥:必须是 PrivateKey 实例
- 密码:必填
- 证书链:验证对应公钥
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 的核心功能:
- 密钥库类型选择(JKS/PKCS12/JCEKS)
- 创建与加载密钥库的完整流程
- 三种条目类型的存储方式:
- 对称密钥(需密码包装)
- 私钥(需证书链)
- 受信任证书(最简单)
- 条目读取与类型检查技巧
- 安全删除条目与密钥库
掌握这些操作后,你就能在 Java 应用中安全地管理密钥和证书了。实际开发中建议优先使用 PKCS12 格式,并特别注意密码管理——生产环境务必使用强密码!