Java 应用换证书比 Nginx 多两步。Nginx 拿到 PEM 文件,reload 一下就生效;Tomcat 和 Spring Boot 通常读的是 keystore(.jks 或 .p12),每次续期都要从云厂商控制台下载 Tomcat 格式的证书,或者自己用 openssl pkcs12、keytool 把证书和私钥转成 keystore,替换文件,再重启应用。证书一年换一次时,这些步骤手动做没什么问题。现在新签发的公开证书最长只有 200 天,2027 年 3 月 15 日起降到 100 天,2029 年 3 月 15 日起降到 47 天,到那时差不多每个月都要换一次。
这篇讲 Java 应用的证书怎么做成自动续期和部署:先确认证书由谁加载,再分别说 Tomcat 和 Spring Boot 的配置,然后是换证时最常见的三个错误,最后是两种自动化做法:自己写 Certbot hook,或者交给 LapseZero 托管。
先确认证书配在 Nginx 还是 Java 应用上
很多 Java 项目前面有 Nginx 或负载均衡,由它们处理 HTTPS,Tomcat 只监听 8080。这种情况下证书和 Java 没有关系,按 Nginx 或云产品的方式续期就行,keystore 那一套用不上。
判断方法很直接:Tomcat 看 conf/server.xml 里有没有带 SSLHostConfig 的 Connector,Spring Boot 看 application.yml、application.properties 和启动参数里有没有 server.ssl。都没有,一般就是 Java 应用不加载证书。下面的内容只针对 Java 自己加载证书的情况。
Tomcat 的证书配置
Tomcat 8.5 以后(包括 9、10、11)的写法相同,都是在 Connector 里放一个 SSLHostConfig:
<Connector port="8443" protocol="org.apache.coyote.http11.Http11NioProtocol" SSLEnabled="true">
<SSLHostConfig>
<Certificate certificateKeystoreFile="/opt/tomcat/ssl/tomcat.p12"
certificateKeystorePassword="changeit"
certificateKeystoreType="PKCS12" />
</SSLHostConfig>
</Connector>
自动化只关心 Certificate 上的几个属性:
certificateKeystoreFile:keystore 文件路径,续期后的新文件就写到这里。certificateKeystorePassword:keystore 密码,新文件必须用同一个密码生成。certificateKeystoreType:不写时默认是JKS。现在建议用 PKCS12,并显式写出来。certificateKeyAlias:示例里没写。不写时 Tomcat 用 keystore 里读到的第一个密钥,keystore 里只有一张证书时这样最省事。
如果不想转 keystore,Tomcat 8.5 起也能直接读 PEM 文件,用 certificateFile、certificateKeyFile、certificateChainFile 三个属性分别指向证书、私钥和中间证书。已经在用 keystore 的不必为了自动化改成 PEM,两种都能自动部署。属性的完整说明见 Tomcat 的 HTTP Connector 文档。
文件换好以后要让 Tomcat 重新加载。最简单的是重启,代价是重启期间 HTTPS 不可用:一台小内存测试机上,Tomcat 10.1 正常重启时 HTTPS 中断了大约 7 秒。不想中断可以用 Manager 应用的 sslReload,它只重新读取证书和私钥文件,不重新解析 server.xml:
curl -fsS -u deployer:password "http://localhost:8080/manager/text/sslReload" | grep -q '^OK'
用这个命令要先启用 Manager 应用,并给账号分配 manager-script 角色。Manager 执行失败时返回的是以 FAIL 开头的文本,curl 本身仍然成功,所以要用 grep 检查结果,失败时命令才会返回非 0。
Spring Boot 的证书配置
Spring Boot 内嵌 Tomcat,证书写在应用配置里:
server:
port: 8443
ssl:
key-store: file:/opt/app/ssl/app.p12
key-store-type: PKCS12
key-store-password: changeit
Spring Boot 2.7 和 3.x 都是这个写法。这里最需要注意的是 file:。很多教程写的是 classpath:keystore.p12,keystore 和代码一起打进 jar 包,换证书就要重新打包发布,没法在服务器上自动替换。要自动化,先把 keystore 挪到 jar 外面,改成 file: 开头的绝对路径。
Spring Boot 同样可以直接用 PEM:server.ssl.certificate 指向证书链,server.ssl.certificate-private-key 指向私钥。
Spring Boot 3.2 起可以不重启换证。把证书配置成 SSL bundle 并打开 reload-on-update,Spring Boot 会在后台监视文件,内容变了就重新加载:
spring:
ssl:
bundle:
jks:
web:
reload-on-update: true
keystore:
location: file:/opt/app/ssl/app.p12
password: changeit
type: PKCS12
server:
ssl:
bundle: web
bundle 的类型名叫 jks,但里面也能放 PKCS12 文件,由 type 决定。server.ssl.bundle 不能和 server.ssl.key-store 这些属性混用,改成 bundle 时要把原来的删掉。版本低于 3.2 的只能重启。Spring Boot 应用启动比 Tomcat 慢,同一台测试机上一个很小的 Spring Boot 3.5 应用重启时,HTTPS 中断约 15 秒;业务应用通常更久。
换证时最常见的三个错误
别名对不上
很多 Tomcat 教程生成 keystore 时用 keytool -alias tomcat,配置里也跟着写 certificateKeyAlias="tomcat"(Spring Boot 是 server.ssl.key-alias: tomcat)。续期后换一个工具生成 keystore,别名很可能变了,应用重启时就会报:
java.io.IOException: Alias name [tomcat] does not identify a key entry
这个错误平时看不到,因为应用正在运行,用的是内存里的旧证书。等到续期后第一次重启,应用才起不来。解决方法有两个:生成新 keystore 时指定同一个别名(openssl pkcs12 -export 加 -name tomcat),或者在 keystore 里只有一张证书时删掉配置里的别名。
密码对不上
新 keystore 的密码必须和配置里的 certificateKeystorePassword 或 server.ssl.key-store-password 一致,否则启动时报:
Failed PKCS12 integrity checking
从云厂商下载的 JKS,私钥密码有时和 keystore 密码不同,配置里会多一项 certificateKeyPassword(Spring Boot 是 server.ssl.key-password)。换成自动生成的 PKCS12 后,私钥密码和 keystore 密码相同,这一项要删掉,或者改成和 keystore 密码一致。
重启“成功”了,应用却没起来
这一个最容易漏。用 systemd 管理的 Java 服务大多是 Type=simple,systemctl restart 在进程启动后就返回,不等 Java 加载完证书。别名或密码错了的时候,systemctl restart 照样返回 0:Tomcat 进程还在,HTTPS Connector 却没有起来;Spring Boot 则是过几秒自己退出。部署脚本看到命令成功,以为换证完成,其实服务已经挂了。
所以重启命令要等到 HTTPS 真正可用才算成功,等不到就返回非 0,让自动化流程知道出了问题:
#!/bin/sh
# 用法:restart-and-wait.sh <服务名> <HTTPS 端口>
set -eu
systemctl restart "$1"
for attempt in $(seq 1 30); do
if curl --insecure --fail --silent --max-time 1 "https://127.0.0.1:$2/" >/dev/null; then
exit 0
fi
sleep 1
done
echo "$1 did not become HTTPS-ready" >&2
exit 1
--insecure 只用于本机检查端口是否起来;探测的路径换成你的应用里不需要登录、能返回 200 的地址。
第一次自动化时还要检查文件权限:Java 进程通常以非 root 用户运行,新写入的 keystore 文件要让这个用户能读。
自己写脚本:Certbot 的 deploy hook
证书由本机的 Certbot 签发时,可以把转换、替换和重启写进 deploy hook。Certbot 每次续期成功后执行它,并通过 RENEWED_LINEAGE 传入证书目录:
#!/bin/sh
set -eu
openssl pkcs12 -export \
-in "$RENEWED_LINEAGE/fullchain.pem" \
-inkey "$RENEWED_LINEAGE/privkey.pem" \
-name tomcat -passout file:/etc/tomcat/keystore.pass \
-out /opt/tomcat/ssl/tomcat.p12.new
mv /opt/tomcat/ssl/tomcat.p12.new /opt/tomcat/ssl/tomcat.p12
/usr/local/bin/restart-and-wait.sh tomcat 8443
用 acme.sh 时,先用 --install-cert 指定证书链和私钥的输出路径,把脚本里的 $RENEWED_LINEAGE/... 换成这两个路径,再通过 --reloadcmd 调用脚本。
一台服务器、一个应用时,这样就够了。服务器和应用多了以后,下面几件事都要自己补:
- 失败恢复:上面的脚本在重启失败时,旧 keystore 已经被覆盖。要先备份,失败时换回去再重启,否则应用就停在报错状态等人处理。
- 多台分发:Certbot 在哪台机器上续期,新证书就只在哪台。多台服务器要么各装一套 Certbot 和 hook,要么自己把证书同步过去,每台执行一遍。
- 失败通知:Certbot 默认不发通知,hook 失败只记在日志里,没人看日志就不会知道。
用 LapseZero 自动部署到 Java 应用
LapseZero 是一个托管服务,负责签发证书、到期前自动续期,并部署到服务器和云产品上。证书是免费的 DV 证书,支持通配符,通过 DNS 验证签发:可以填 DNS 服务商的 API 密钥,也可以只加一条 CNAME。现在用的证书不用迁移,第一次部署就会把新证书写到原来的 keystore 路径上。
一台 Java 服务器只需要配置一次:添加部署目标时选「服务器」,软件选 Tomcat 或 Spring Boot,然后填三项:
- 证书路径:配置里的 keystore 路径,Spring Boot 去掉
file:前缀。 - Keystore 密码:和配置里的一致。配置里写了别名的,把同一个别名也填上。
- 部署后命令:上面的
restart-and-wait.sh。
服务器可以通过 SSH 连接,也可以装一个部署客户端,不用开放入站端口。私钥和密码都加密保存,详见安全说明。
之后每次续期,LapseZero 自动生成 PKCS12 文件、写到原路径、执行部署后命令,前面说的三个错误都不用再操心:keystore 按你填的密码和别名生成,不会对不上;命令失败(比如应用 30 秒内没起来)时,自动换回旧 keystore 再执行一次命令,应用继续用旧证书,通知里会写明已回滚。在「生效检查」里填上应用的 HTTPS 地址,部署后还会确认线上已经换成新证书。
应用部署在多台服务器上时,每台服务器算一个部署位置,填法相同。前面的 Nginx、负载均衡和云产品也在同一个地方部署同一张证书,不用在每台机器上维护一套 hook。
免费版包含 3 个域名、5 个部署位置,不限时间,够先拿一两台服务器试。一个域名部署到 6 台服务器就超出了免费版,入门版 6 元/月,包含 10 个域名、20 个部署位置。