数字证书在企业级应用中的地位越来越重要,无论是接口双向认证、电子签章还是数据加密传输,背后都离不开 PKI(公钥基础设施)体系的支撑。通常的做法是直接向 CA 机构购买证书,然后配置到服务器上就结束了。但当业务规模扩大,需要给设备、租户、子系统的每张证书都去买商业证书显然不现实,这时候就需要在 Spring Boot 项目中自建一套轻量级 CA,实现证书的签发与全生命周期管理。本文将从原理到代码完整讲解实现过程。

一、PKI 核心概念与整体设计思路
在动手写代码之前,必须先理清 PKI 体系的几个核心角色。PKI 由证书持有者、CA(证书颁发机构)、RA(注册审批机构)、CRL(证书吊销列表)和 OCSP(在线证书状态协议)组成。CA 是整个信任链的根,它持有根证书和根私钥,用根私钥给下级证书签名,从而形成一条信任链。浏览器或客户端在校验证书时,会沿着证书链一直追溯到自己信任的根证书,整条链都合法才认为证书有效。
在 Spring Boot 中落地时,推荐采用分层设计:底层使用 Bouncy Castle 作为密码学库,负责密钥生成、证书构造和签名;中间层封装一个 CertificateService,提供签发、续期、吊销等业务方法;最上层暴露 REST 接口,供其他系统申请和管理证书。同时用数据库记录每张证书的序列号、主题、有效期、状态等信息,吊销状态则同时落到 CRL 文件中供客户端下载校验。
证书生命周期一般包括申请、审批、签发、使用、续期、吊销、归档七个阶段。自建 CA 场景下通常简化为签发、查询、续期、吊销四步,这也是下文代码实现的重点。需要注意,根私钥必须妥善保管,生产环境建议放到加密机或至少用口令保护的 KeyStore 中,绝不能明文存放在代码仓库里。
二、搭建环境与实现 CA 根证书生成
首先要引入 Bouncy Castle 依赖,它是 Java 生态中最成熟的密码学库,JDK 自带的工具对证书构造的支持非常有限。在 pom.xml 中添加如下依赖:
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>
<lt;dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcpkix-jdk18on</artifactId>
<version>1.78.1</version>
</dependency>别忘了在启动类或配置类中注册 Bouncy Castle 提供者,否则后续调用会抛出找不到算法的异常。接着实现根证书的生成。CA 根证书是一个自签名证书,主题和颁发者相同,密钥算法建议使用 RSA 2048 位或 ECDSA P-256,有效期可以设置得长一些,例如十年。示例如下:
Security.addProvider(new BouncyCastleProvider());
public static X509Certificate generateRootCA(KeyPair keyPair, String cn, int validYears)
throws Exception {
X500Name subject = new X500Name("CN=" + cn + ",O=MyCompany,C=CN");
BigInteger serial = BigInteger.valueOf(System.currentTimeMillis());
Date notBefore = new Date();
Date notAfter = Date.from(LocalDateTime.now()
.plusYears(validYears).atZone(ZoneId.systemDefault()).toInstant());
JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
subject, serial, notBefore, notAfter, subject, keyPair.getPublic());
// 根证书需添加 BasicConstraints 扩展,标记为 CA
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(true));
// 密钥用途:证书签名和 CRL 签名
builder.addExtension(Extension.keyUsage, true,
new X509KeyUsage(X509KeyUsage.keyCertSign | X509KeyUsage.cRLSign));
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.setProvider("BC").build(keyPair.getPrivate());
return new JcaX509CertificateConverter()
.setProvider("BC").getCertificate(builder.build(signer));
}生成的根证书和根私钥应保存为 PKCS12 格式的 KeyStore 文件,通过配置文件指定路径和访问口令。应用启动时加载,后续签发用户证书时直接复用,避免每次都重新生成根密钥对。
三、实现终端证书签发与 REST 管理接口
签发终端证书的逻辑与根证书类似,区别在于颁发者要填写 CA 的主题,且需要把 CA 证书链带上。同时应根据业务场景配置不同的密钥用途扩展,例如 TLS 服务端认证、客户端认证或代码签名等。下面是一个完整的签发方法,申请方只提交 CSR(证书签名请求),私钥始终留在申请方本地,这是安全上最推荐的做法:
@Service
public class CertificateService {
@Value("${pki.ca.keystore-path}")
private String keystorePath;
@Value("${pki.ca.keystore-password}")
private String keystorePassword;
public X509Certificate issueCertificate(PKCS10CertificationRequest csr,
int validDays) throws Exception {
KeyStore ks = loadCaKeyStore();
PrivateKey caKey = (PrivateKey) ks.getKey("rootca", keystorePassword.toCharArray());
X509Certificate caCert = (X509Certificate) ks.getCertificate("rootca");
X500Name subject = csr.getSubject();
BigInteger serial = new BigInteger(64, new SecureRandom());
Date notBefore = new Date();
Date notAfter = Date.from(Instant.now().plus(validDays, ChronoUnit.DAYS));
JcaX509v3CertificateBuilder builder = new JcaX509v3CertificateBuilder(
caCert.getSubjectX500Principal(), serial, notBefore, notAfter,
subject, csr.getPublicKey());
// 终端证书 BasicConstraints 为 false
builder.addExtension(Extension.basicConstraints, true, new BasicConstraints(false));
builder.addExtension(Extension.keyUsage, true,
new X509KeyUsage(X509KeyUsage.digitalSignature | X509KeyUsage.keyEncipherment));
// 密钥用途为 TLS 客户端和服务端认证
builder.addExtension(Extension.extendedKeyUsage, true, new ExtendedKeyUsage(
new KeyPurposeId[]{KeyPurposeId.id_kp_clientAuth, KeyPurposeId.id_kp_serverAuth}));
// SubjectKeyIdentifier 增强证书匹配能力
builder.addExtension(Extension.subjectKeyIdentifier, false,
new JcaX509ExtensionUtils().createSubjectKeyIdentifier(csr.getPublicKey()));
ContentSigner signer = new JcaContentSignerBuilder("SHA256withRSA")
.setProvider("BC").build(caKey);
return new JcaX509CertificateConverter()
.setProvider("BC").getCertificate(builder.build(signer));
}
}有了核心服务,再暴露 REST 接口就很直接了。常见的接口设计包括:提交 CSR 申请证书、根据序列号查询证书状态、发起续期、发起吊销,以及下载 CRL 文件。Controller 层接收 PEM 格式的 CSR 字符串,解析后调用上面的服务,返回 PEM 格式的证书。这里强烈建议把每张签发的证书元数据落库,字段包含序列号、主题 DN、签发时间、过期时间、状态等,这样续期和吊销操作才有据可查。
一个容易踩的坑是序列号的生成。证书序列号必须全局唯一,且不要直接使用自增 ID,因为序列号在 X.509 标准中要求为正整数且不超过 20 字节。上面的代码使用 SecureRandom 生成 64 位随机数,既满足唯一性又满足长度约束,是比较稳妥的方案。
四、证书校验、吊销与 CRL 的落地实现
签发只是开始,一个完整的证书管理方案必须解决校验问题。证书校验包含三个层次:签名链校验、有效期校验、吊销状态校验。前两者用 JDK 自带的 CertPathValidator 即可完成,代码如下:
public void validateCertificate(X509Certificate cert) throws Exception {
// 1. 有效期检查
cert.checkValidity();
// 2. 构建信任锚(根 CA)
KeyStore ks = loadCaKeyStore();
X509Certificate caCert = (X509Certificate) ks.getCertificate("rootca");
Set<TrustAnchor> anchors = Set.of(new TrustAnchor(caCert, null));
// 3. 构建并校验证书路径
CertStore store = CertStore.getInstance("Collection",
new CollectionCertStoreParameters(List.of(caCert, cert)));
PKIXParameters params = new PKIXParameters(anchors);
params.addCertStore(store);
params.setRevocationEnabled(false); // 吊销检查单独处理
CertPathValidator.getInstance("PKIX")
.validate(CertPathBuilder.getInstance("PKIX")
.build(CertPathBuilder.getInstance("PKIX") != null
? params : params).getCertPath(), params);
}吊销检查有两种主流方案。第一种是 CRL,CA 定期生成并发布一个包含所有已吊销序列号的列表文件,客户端下载后本地比对。CRL 实现简单,适合吊销量不大的场景,但存在时效性问题,两次发布之间的窗口期内吊销不生效。生成 CRL 同样可以借助 Bouncy Castle 的 CertificateList 构造器。第二种是 OCSP,客户端实时向 CA 查询单张证书的状态,时效性好,但需要额外的 OCSP 响应服务,实现成本更高。
对于内部系统,推荐先用 CRL 起步:应用在每次校验证书时缓存 CRL 文件,设置五到十分钟的刷新间隔,吊销操作立即写入本地吊销表并触发 CRL 重新生成。这样兼顾了实时性和实现复杂度。等业务对吊销时效要求严格时,再升级到 OCSP 或两者并用。
五、安全加固与生产实践建议
证书管理系统本身就是高价值攻击目标,代码写完不代表可以上线。首先,CA 私钥的访问必须最小化,KeyStore 口令建议通过环境变量或密钥管理服务注入,而不是写在 application.yml 明文里。其次,签发接口必须做认证与授权,例如只有持有管理端证书或特定角色 Token 的调用方才能签发和吊销证书,否则任何人都能给自己签一张合法证书,整个信任体系就形同虚设。
另外要设计好证书到期的监控机制。可以在数据库中定期扫描三十天内即将过期的证书,通过消息通知或邮件提醒负责人续期。历史上大量 TLS 故障都是证书过期导致的,自动化监控是必选项而非可选项。日志方面,所有签发、续期、吊销操作都应记录操作人、时间、目标序列号,并保证日志不可篡改,便于安全审计。
最后总结一下技术选型:Spring Boot 3.x 加 Bouncy Castle 1.7x 是目前最稳定的组合,数据库用 MySQL 或 PostgreSQL 均可,CRL 文件可以放在对象存储上分发。如果不想从零实现,也可以考虑集成 EJBCA 或 CFSSL 作为独立 CA 服务,Spring Boot 只负责业务侧的申请审批流程,两种路线各有取舍,自研可控性强、集成方案更省事,按团队实际情况选择即可。
Spring BootPKI数字证书管理修改时间:2026-09-16 22:55:07