You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

Spring Cloud Gateway(WebFlux栈)不重启服务更新SSL证书的实现问题

Spring Cloud Gateway(WebFlux栈)不重启服务更新SSL证书的实现问题

你遇到的这个场景非常典型——外部证书定期更新,不想重启Spring Cloud Gateway(基于WebFlux+Netty)来加载新证书。你的思路(监听证书文件变化)是完全正确的,但问题出在Netty服务器启动后,其SSL上下文(SslContext)是绑定到运行中的服务器实例的,直接修改NettyReactiveWebServerFactory的配置完全不会影响已经启动的服务,因为factory只是负责初始化服务器的"工厂",服务器启动后factory就不再参与运行时的配置管理了。

下面给你一套可落地的解决方案:

核心思路

要实现不重启更新SSL证书,必须做到三点:

  1. 保留文件监听逻辑,精准捕捉证书文件的修改事件;
  2. 事件触发后,重新加载密钥库,构建新的Netty SslContext
  3. 直接更新运行中的Netty HttpServer实例的SSL配置,替换旧的SslContext。

代码调整方案

1. 调整配置类,保存运行中的Netty服务器引用

首先修改你的ContainerConfiguration,让它实现ApplicationListener<WebServerInitializedEvent>,这样可以在服务器启动后拿到运行中的NettyWebServer实例,而不是只在初始化阶段操作factory:

@Configuration
public class ContainerConfiguration implements WebServerFactoryCustomizer<NettyReactiveWebServerFactory>,
        ApplicationListener<WebServerInitializedEvent> {

    private final ApplicationContext applicationContext;
    private final boolean sslEnabled;
    private final String sslKeyStoreType;
    private final String sslKeyStoreFilepath;
    private final String sslKeyStorePassword;
    private final String sslKeyAlias;
    private NettyWebServer nettyWebServer; // 保存运行中的Netty服务器实例
    private WatchService watchService;
    private long lastModified = 0; // 防抖:记录证书文件上次修改时间

    // 构造函数保持不变,此处省略...

    // 监听服务器初始化事件,保存NettyWebServer实例
    @Override
    public void onApplicationEvent(WebServerInitializedEvent event) {
        if (event.getWebServer() instanceof NettyWebServer) {
            this.nettyWebServer = (NettyWebServer) event.getWebServer();
        }
    }

    // 应用关闭时关闭WatchService,避免资源泄漏
    @PreDestroy
    public void cleanUp() throws IOException {
        if (watchService != null) {
            watchService.close();
        }
    }

2. 完善文件监听逻辑,添加防抖处理

customize方法中保留文件监听,但添加防抖逻辑(避免编辑器保存时触发多次重复的修改事件):

@Override
public void customize(NettyReactiveWebServerFactory factory) {
    Ssl ssl = new Ssl();
    ssl.setEnabled(sslEnabled);
    if (sslEnabled) {
        ssl.setKeyStoreType(sslKeyStoreType);
        ssl.setKeyStore(sslKeyStoreFilepath);
        ssl.setKeyStorePassword(sslKeyStorePassword);
        ssl.setKeyAlias(sslKeyAlias);

        Path certPath = Paths.get(sslKeyStoreFilepath);
        try {
            watchService = FileSystems.getDefault().newWatchService();
            // 监听证书文件所在目录的修改事件
            certPath.getParent().register(watchService, StandardWatchEventKinds.ENTRY_MODIFY);
        } catch (IOException e) {
            throw new RuntimeException("Failed to create watch service for certificate file", e);
        }

        // 启动监听线程
        new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                try {
                    WatchKey key = watchService.take();
                    try {
                        for (WatchEvent<?> event : key.pollEvents()) {
                            if (event.kind() == StandardWatchEventKinds.ENTRY_MODIFY
                                    && event.context().equals(certPath.getFileName())) {
                                // 防抖:只有当文件实际修改时间晚于上次记录时才触发更新
                                long currentModified = Files.getLastModifiedTime(certPath).toMillis();
                                if (currentModified > lastModified) {
                                    lastModified = currentModified;
                                    log.warn("SSL certificate file modified, starting reload...");
                                    reloadSSLConfig();
                                }
                            }
                        }
                    } finally {
                        // 重置WatchKey,继续监听下一次事件
                        boolean valid = key.reset();
                        if (!valid) {
                            log.error("Watch key is no longer valid, stopping certificate watch");
                            break;
                        }
                    }
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    log.info("Certificate watch thread interrupted");
                } catch (Exception e) {
                    log.error("Error in certificate watch thread", e);
                }
            }
        }, "certificate-watch-thread").start();
    }

    factory.setPort(sslEnabled ? 8443 : 8080);
    factory.setSsl(ssl);
}

3. 实现真正的SSL配置重载逻辑

替换原来的reloadSSLConfig方法,直接操作运行中的Netty服务器,重新加载密钥库并更新SslContext:

private void reloadSSLConfig() {
    if (nettyWebServer == null || !sslEnabled) {
        log.warn("Cannot reload SSL config: server not initialized or SSL disabled");
        return;
    }

    try {
        // 1. 重新加载密钥库,构建新的Netty SslContext
        KeyStore keyStore = KeyStore.getInstance(sslKeyStoreType);
        try (InputStream is = Files.newInputStream(Paths.get(sslKeyStoreFilepath))) {
            keyStore.load(is, sslKeyStorePassword.toCharArray());
        }

        KeyManagerFactory keyManagerFactory = KeyManagerFactory.getInstance(
                KeyManagerFactory.getDefaultAlgorithm());
        keyManagerFactory.init(keyStore, sslKeyStorePassword.toCharArray());

        SslContext newSslContext = SslContextBuilder.forServer(keyManagerFactory)
                .protocols("TLSv1.2", "TLSv1.3") // 指定支持的TLS协议版本,按需调整
                .build();

        // 2. 获取运行中的Netty HttpServer实例,更新其SSL配置
        HttpServer nettyHttpServer = nettyWebServer.getHttpServer();
        // 更新Netty HttpServer的SSL上下文
        nettyHttpServer.secure(sslSpec -> sslSpec.sslContext(newSslContext));

        log.warn("SSL certificate reloaded successfully! New certificate is now in use.");
    } catch (Exception e) {
        log.error("Failed to reload SSL certificate", e);
    }
}

关键说明

  1. 为什么原来的factory.setSsl无效?
    NettyReactiveWebServerFactory是用来初始化Netty服务器的工厂类,服务器启动后,factory的配置就不再影响运行中的实例了。我们需要直接操作已经启动的Netty HttpServer,替换它的SslContext。

  2. 关于Netty版本兼容性
    不同Spring Boot版本对应的Netty版本可能略有差异,nettyHttpServer.secure(...)的API可能会有小变化,如果你的版本中这个方法参数不同,可以参考对应Netty版本的文档调整,但核心逻辑都是替换SslContext。

  3. 额外注意事项

    • 确保应用进程有读取证书文件和监听父目录的权限;
    • 如果密钥库的密码、别名或类型也可能变化,你需要同时监听配置文件的变化,或者在重载时重新读取配置(比如结合Spring Cloud Config的配置刷新功能);
    • 监听线程设置了名称(certificate-watch-thread),方便排查问题;
    • 不要尝试重启Spring上下文,这会导致服务中断,完全违背"不重启更新"的需求。

这样调整后,当证书文件更新时,你的应用会自动重载SSL配置,新的客户端连接会立即使用新证书,旧连接会在自然断开后切换到新证书,全程不需要重启服务。

火山引擎 最新活动