Spring Boot 动态热更新 HTTPS 证书的实现与原理

在实际生产环境中,HTTPS 证书定期更新是非常常见的需求。传统方式通常要求重启服务来加载新证书,但在一些高可用系统中,重启服务会造成连接中断和短暂不可用。本篇文章将介绍如何在 Spring Boot 项目中,实现 不重启服务的情况下热更新 SSL 证书 的完整方案。

一、原理说明

Spring Boot 默认使用 TomcatServletWebServerFactory 嵌入式 Tomcat 来启动 Web 服务。Tomcat 在初始化阶段会将 HTTPS 的证书信息加载到其 Connector 中。然而,Tomcat 实际上支持在运行时关闭并重新启动某个 Connector,因此我们可以:

1.缓存 Tomcat 实例和当前 HTTPS Connector;

2.当证书文件发生变化时,关闭旧的 Connector;

3.创建一个新的 Connector,加载新的证书;

4.添加到 Tomcat 实例中并启动。

整个过程无需重启 Spring Boot 应用,仅重建了 Tomcat 的 HTTPS 通道。

二、实现步骤

步骤 1:监听 Tomcat 实例并缓存 HTTPS Connector

我们通过监听 WebServerInitializedEvent 事件,在 Spring Boot 启动完成后获取 TomcatWebServer 实例,并缓存 HTTPS Connector 以便后续替换。

import org.apache.catalina.connector.Connector;
import org.apache.catalina.startup.Tomcat;
import org.apache.tomcat.util.net.SSLHostConfig;
import org.apache.tomcat.util.net.SSLHostConfigCertificate;
import org.springframework.boot.web.context.WebServerInitializedEvent;
import org.springframework.boot.web.embedded.tomcat.TomcatWebServer;
import org.springframework.context.ApplicationListener;
import org.springframework.stereotype.Component;

@Component
public class DynamicSslReloader implements ApplicationListener<WebServerInitializedEvent> {

    private TomcatWebServer tomcatWebServer;
    private Connector httpsConnector;

    private final String keystorePath = "/your/path/to/keystore.p12";
    private final String keystorePassword = "changeit";

    @Override
    public void onApplicationEvent(WebServerInitializedEvent event) {
        if (event.getWebServer() instanceof TomcatWebServer) {
            this.tomcatWebServer = (TomcatWebServer) event.getWebServer();

            // 获取 HTTPS Connector(假设只有一个)
            for (Connector connector : tomcatWebServer.getTomcat().getService().findConnectors()) {
                if ("https".equalsIgnoreCase(connector.getScheme())) {
                    this.httpsConnector = connector;
                    break;
                }
            }
        }
    }

    public void reloadSslContext() {
        if (tomcatWebServer == null || httpsConnector == null) {
            System.err.println("Tomcat 未初始化完成,无法重新加载证书");
            return;
        }

        try {
            // 暂停并移除旧 Connector
            httpsConnector.pause();
            httpsConnector.stop();
            tomcatWebServer.getTomcat().getService().removeConnector(httpsConnector);

            // 创建新的 HTTPS Connector
            Connector newConnector = createHttpsConnector();
            tomcatWebServer.getTomcat().getService().addConnector(newConnector);
            newConnector.start();

            // 替换当前 Connector 引用
            this.httpsConnector = newConnector;

            System.out.println("SSL 证书热更新完成");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    private Connector createHttpsConnector() {
        Connector connector = new Connector("org.apache.coyote.http11.Http11NioProtocol");
        connector.setScheme("https");
        connector.setPort(8443); // 请根据实际端口设置
        connector.setSecure(true);

        SSLHostConfig sslHostConfig = new SSLHostConfig();
        sslHostConfig.setCertificatesKeystoreFile(keystorePath);
        sslHostConfig.setCertificatesKeystorePassword(keystorePassword);
        sslHostConfig.setCertificatesKeystoreType("PKCS12");
        sslHostConfig.setProtocols("TLSv1.2,TLSv1.3");

        SSLHostConfigCertificate cert = new SSLHostConfigCertificate(
                sslHostConfig, SSLHostConfigCertificate.Type.UNDEFINED);
        sslHostConfig.addCertificate(cert);

        connector.addSslHostConfig(sslHostConfig);
        return connector;
    }
}

步骤 2:定时检测证书文件是否更新

使用 Spring 的定时任务,每隔一定时间检查证书文件是否有更新。如果发现文件被替换或修改,则调用 reloadSslContext 方法热更新证书。

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.io.File;

@Component
public class CertMonitor {

    private final DynamicSslReloader sslReloader;

    private long lastModified = 0;
    private final String certPath = "/your/path/to/keystore.p12";

    public CertMonitor(DynamicSslReloader sslReloader) {
        this.sslReloader = sslReloader;
    }

    @Scheduled(fixedDelay = 60 * 1000) // 每 1 分钟检查一次
    public void check() {
        File file = new File(certPath);
        if (file.exists() && file.lastModified() > lastModified) {
            lastModified = file.lastModified();
            sslReloader.reloadSslContext();
        }
    }
}

步骤 3:启用定时任务调度

在 Spring Boot 启动类上添加 @EnableScheduling 注解以启用定时任务:

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;

@SpringBootApplication
@EnableScheduling
public class SslHotReloadApp {
    public static void main(String[] args) {
        SpringApplication.run(SslHotReloadApp.class, args);
    }
}

三、注意事项

1.证书格式支持:上述代码使用 PKCS12 格式证书(.p12),也可替换为 JKS 格式;

2.权限问题:确保 Java 进程有权限访问新的证书文件;

3.只支持 Tomcat:适用于 Spring Boot 默认的 Tomcat,如果是 Jetty/Undertow,需要另行实现;

4.端口监听一致性:建议新建 Connector 的端口与旧的一致,否则可能导致监听端口冲突或失效;

5.生产环境建议双证书切换机制,例如蓝绿部署或双文件对比。

6.项目第一次部署时,还是需要一个在配置文件中配置好的ssl证书,和我们热更新的证书路径不一样,这样保证在首次部署时,不会出现证书不存在的问题,后续我们上传到我们代码中指定的路径,可以更新ssl证书,也可以封装自己的上传逻辑,直接通过项目中的上传接口手动上传

server:
  ssl:
    key-store: classpath:ssl/tomcat.pfx
    key-store-password: 123456
    key-store-type: PKCS12
    #key-alias: ssl
    #enabled: true

四、总结

通过监听 WebServerInitializedEvent 获取 Tomcat 实例,并在运行时替换 HTTPS Connector,可以实现在 Spring Boot 中动态热更新 SSL 证书的能力。这种方式避免了系统重启,对于在线系统、微服务网关等具有重要意义。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值