Spring Boot 3.2.0 中的 SSL 热重载功能

Spring Boot 3.2.0 为嵌入式 Web 服务器添加了热加载 SSL 证书和密钥的功能。这意味着无需重启应用就能替换 SSL 配置。Tomcat 和 Netty 嵌入式 Web 服务器支持热重载。

首先,使用 OpenSSL 创建 SSL 私钥和匹配证书:

mkdir certs
cd certs
openssl req -x509 -subj "/CN=demo-cert-1" -keyout demo.key -out demo.crt -sha256 -days 365 -nodes -newkey ed25519 

这会创建一个私钥,存储在 certs/demo.key 中,和一个与之匹配的(自签名)证书,通用名称为 demo-cert-1,存储在 certs/demo.crt 中。

现在创建一个新的 Spring Boot 3.2.0 应用,添加 Spring Web 依赖,默认情况下使用 Tomcat Web 服务器。你可以通过 start.springboot.io 快速创建。

application.yaml 配置文件中,添加如下配置:

spring.ssl.bundle.pem:
  demo:
    reload-on-update: true
    keystore:    
      certificate: "certs/demo.crt"
      private-key: "certs/demo.key"

这配置了一个 SSL Bundle,名称为 demo,并配置上文生成的证书和私钥。

reload-on-update: true,配置指定 Spring Boot 在后台监视文件,并在文件发生变化时触发重新加载。

接着,配置 Web 服务器使用该 Bundle,并监听 8443 端口:

server.ssl.bundle: "demo"
server.port: 8443

添加一个简单的 Controller,只响应一句 "Hello World"

@RestController
class DemoController {
    @GetMapping(path = "/", produces = MediaType.TEXT_PLAIN_VALUE)
    String helloWorld() {
        return "Hello World";
    }
}

启动应用,你可以在日志中看到类似如下的内容,从而确认它是使用 HTTPS 在 8443 端口启动的。

INFO 82407 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port 8443 (https)

现在,可以使用 curl 进行第一次请求:

$ curl --insecure https://localhost:8443/
Hello World%

一切 OK!注意,这里必须使用 --insecure 参数,因为 SSL 证书是自签名的,不被 curl 信任。

curl 切换到 verbose(详细)模式,可以额获取有关 SSL 握手过程的信息:

$ curl --verbose --insecure https://localhost:8443/
*   Trying 127.0.0.1:8443...
* Connected to localhost (127.0.0.1) port 8443 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
*  subject: CN=demo-cert-1
*  start date: Nov  2 09:22:53 2023 GMT
*  expire date: Nov  1 09:22:53 2024 GMT
*  issuer: CN=demo-cert-1
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
* using HTTP/1.x
> GET / HTTP/1.1
> Host: localhost:8443
> User-Agent: curl/8.0.1
> Accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/1.1 200 
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 11
< Date: Thu, 02 Nov 2023 09:33:37 GMT
< 
* Connection #0 to host localhost left intact
Hello World

subject: CN=demo-cert-1 这一行中,可以看到证书的通用名称是 demo-cert-1,这在后面会很重要。

现在试试热重载,用 OpenSSL 生成新的私钥和证书,覆盖旧文件:

cd certs
openssl req -x509 -subj "/CN=demo-cert-2" -keyout demo.key -out demo.crt -sha256 -days 365 -nodes -newkey ed25519

这次,证书通用名称为 demo-cert-2

覆盖旧文件一段时间后,你会在日志中看到如下内容:

INFO 83162 --- [-bundle-watcher] o.a.t.util.net.NioEndpoint.certificate   : Connector [https-jsse-nio-8443], TLS virtual host [_default_], certificate type [UNDEFINED] configured from keystore [/home/xxx/.keystore] using alias [tomcat] with trust store [null]

这段复杂的日志表示 Tomcat 重新加载了私钥和证书。

现在可以用 curl 验证新证书是否已被使用:

$ curl --verbose --insecure https://localhost:8443/
*   Trying 127.0.0.1:8443...
* Connected to localhost (127.0.0.1) port 8443 (#0)
* ALPN: offers h2,http/1.1
* TLSv1.3 (OUT), TLS handshake, Client hello (1):
* TLSv1.3 (IN), TLS handshake, Server hello (2):
* TLSv1.3 (IN), TLS handshake, Encrypted Extensions (8):
* TLSv1.3 (IN), TLS handshake, Certificate (11):
* TLSv1.3 (IN), TLS handshake, CERT verify (15):
* TLSv1.3 (IN), TLS handshake, Finished (20):
* TLSv1.3 (OUT), TLS change cipher, Change cipher spec (1):
* TLSv1.3 (OUT), TLS handshake, Finished (20):
* SSL connection using TLSv1.3 / TLS_AES_256_GCM_SHA384
* ALPN: server did not agree on a protocol. Uses default.
* Server certificate:
*  subject: CN=demo-cert-2
*  start date: Nov  2 09:37:46 2023 GMT
*  expire date: Nov  1 09:37:46 2024 GMT
*  issuer: CN=demo-cert-2
*  SSL certificate verify result: self-signed certificate (18), continuing anyway.
* using HTTP/1.x
> GET / HTTP/1.1
> Host: localhost:8443
> User-Agent: curl/8.0.1
> Accept: */*
> 
* TLSv1.3 (IN), TLS handshake, Newsession Ticket (4):
< HTTP/1.1 200 
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 11
< Date: Thu, 02 Nov 2023 09:39:47 GMT
< 
* Connection #0 to host localhost left intact
Hello World

通过 subject: CN=demo-cert-2 这一行,可以确定新的证书已经生效。

顺便说一下,已经使用旧证书创建的连接,仍然使用的是旧证书。在新证书变更发生后,新的连接才会使用新证书。

希望这个新功能可以简化你的操作。如果你有改进的意见或者是发现了新的错误。欢迎你在官方 issues 上提出。


Ref:https://spring.io/blog/2023/11/07/ssl-hot-reload-in-spring-boot-3-2-0