Spring Boot嵌入式Tomcat代码配置SSL失败求助
问题描述
尝试为Spring Boot嵌入式Tomcat服务器配置SSL。Spring Boot默认集成Tomcat,通常可通过application.properties配置密钥库路径、信任库路径等SSL参数,但希望通过代码实现配置,以使用自己开发的SSL库(该库具备所需的额外功能)。此前尝试配置失败后,转而使用Jetty或Netty实现代码配置SSL,但一直想再尝试Tomcat的方案。
由于需要支持热重载等特性,无法通过XML配置。当前代码配置仍失败,报错堆栈跟踪如下:
Caused by: org.springframework.boot.web.server.WebServerException: Unable to start embedded Tomcat server at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:229) at org.springframework.boot.web.servlet.context.WebServerStartStopLifecycle.start(WebServerStartStopLifecycle.java:43) at org.springframework.context.support.DefaultLifecycleProcessor.doStart(DefaultLifecycleProcessor.java:178) ... 14 more Caused by: java.lang.IllegalArgumentException: standardService.connector.startFailed at org.apache.catalina.core.StandardService.addConnector(StandardService.java:238) at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.addPreviouslyRemovedConnectors(TomcatWebServer.java:282) at org.springframework.boot.web.embedded.tomcat.TomcatWebServer.start(TomcatWebServer.java:213) ... 16 more Caused by: org.apache.catalina.LifecycleException: Protocol handler start failed at org.apache.catalina.connector.Connector.startInternal(Connector.java:1077) at org.apache.catalina.util.LifecycleBase.start(LifecycleBase.java:183) at org.apache.catalina.core.StandardService.addConnector(StandardService.java:234) ... 18 more Caused by: java.lang.IllegalArgumentException: SSLHostConfig attribute certificateFile must be defined when using an SSL connector at org.apache.tomcat.util.net.AbstractJsseEndpoint.createSSLContext(AbstractJsseEndpoint.java:107) at org.apache.tomcat.util.net.AbstractJsseEndpoint.initialiseSsl(AbstractJsseEndpoint.java:71) at org.apache.tomcat.util.net.NioEndpoint.bind(NioEndpoint.java:235) at org.apache.tomcat.util.net.AbstractEndpoint.bindWithCleanup(AbstractEndpoint.java:1227) at org.apache.tomcat.util.net.AbstractEndpoint.start(AbstractEndpoint.java:1313) at org.apache.coyote.AbstractProtocol.start(AbstractProtocol.java:617) at org.apache.catalina.connector.Connector.startInternal(Connector.java:1074) ... 20 more Caused by: java.io.IOException: SSLHostConfig attribute certificateFile must be defined when using an SSL connector at org.apache.tomcat.util.net.SSLUtilBase.getKeyManagers(SSLUtilBase.java:312) at org.apache.tomcat.util.net.SSLUtilBase.createSSLContext(SSLUtilBase.java:247) at org.apache.tomcat.util.net.AbstractJsseEndpoint.createSSLContext(AbstractJsseEndpoint.java:105) ... 26 more
代码实现
ServerConfig.java
import org.springframework.boot.web.embedded.tomcat.TomcatServletWebServerFactory; import org.springframework.boot.web.servlet.server.ServletWebServerFactory; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class ServerConfig { @Bean public ServletWebServerFactory servletContainer(SSLConnectorCustomizer sslConnectorCustomizer) { TomcatServletWebServerFactory tomcat = new TomcatServletWebServerFactory(); tomcat.addConnectorCustomizers(sslConnectorCustomizer); return tomcat; } }
SSLConfig.java
import nl.altindag.ssl.SSLFactory; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @Configuration public class SSLConfig { @Bean public SSLFactory sslFactory(@Value("${ssl.keystore-path}") String keyStorePath, @Value("${ssl.keystore-password}") char[] keyStorePassword, @Value("${ssl.truststore-path}") String trustStorePath, @Value("${ssl.truststore-password}") char[] trustStorePassword, @Value("${ssl.client-auth}") boolean isClientAuthenticationRequired) { return SSLFactory.builder() .withSwappableIdentityMaterial() .withSwappableTrustMaterial() .withIdentityMaterial(keyStorePath, keyStorePassword) .withTrustMaterial(trustStorePath, trustStorePassword) .withDefaultTrustMaterial() .withSystemTrustMaterial() .withNeedClientAuthentication(isClientAuthenticationRequired) .build(); } }
SSLConnectorCustomizer.java
import nl.altindag.ssl.SSLFactory; import nl.altindag.ssl.util.KeyStoreUtils; import org.apache.catalina.connector.Connector; import org.apache.coyote.http11.AbstractHttp11Protocol; import org.apache.tomcat.util.net.SSLHostConfig; import org.apache.tomcat.util.net.SSLHostConfigCertificate; import org.apache.tomcat.util.net.SSLHostConfigCertificate.Type; import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer; import org.springframework.context.annotation.Configuration; import javax.net.ssl.SSLParameters; import javax.net.ssl.X509TrustManager; import java.security.KeyStore; @Configuration public class SSLConnectorCustomizer implements TomcatConnectorCustomizer { private final SSLFactory sslFactory; public SSLConnectorCustomizer(SSLFactory sslFactory) { this.sslFactory = sslFactory; } @Override public void customize(Connector connector) { connector.setScheme("https"); connector.setSecure(true); connector.setPort(8444); AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler(); configureSsl(protocol); } private void configureSsl(AbstractHttp11Protocol<?> protocol) { protocol.setSSLEnabled(true); SSLHostConfig sslHostConfig = new SSLHostConfig(); sslHostConfig.setSslProtocol("TLS"); sslHostConfig.setHostName(protocol.getDefaultSSLHostConfigName()); configureSslClientAuth(sslHostConfig); KeyStore trustStore = sslFactory.getTrustManager() .map(X509TrustManager::getAcceptedIssuers) .map(KeyStoreUtils::createTrustStore) .orElseThrow(); sslHostConfig.setTrustStore(trustStore); SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED); certificate.setSslContext(new SSLContextWrapper(sslFactory)); sslFactory.getKeyManager().ifPresent(certificate::setCertificateKeyManager); sslHostConfig.addCertificate(certificate); String ciphers = String.join(",", sslFactory.getCiphers()); sslHostConfig.setCiphers(ciphers); String protocols = String.join(",", sslFactory.getProtocols()); sslHostConfig.setProtocols(protocols); protocol.addSslHostConfig(sslHostConfig); } private void configureSslClientAuth(SSLHostConfig config) { String clientAuth; SSLParameters sslParameters = sslFactory.getSslParameters(); if (sslParameters.getNeedClientAuth()) { clientAuth = "required"; } else if (sslParameters.getWantClientAuth()) { clientAuth = "optional"; } else { clientAuth = "none"; } config.setCertificateVerification(clientAuth); } }
SSLContextWrapper.java
import nl.altindag.ssl.SSLFactory; import org.apache.tomcat.util.net.SSLContext; import javax.net.ssl.KeyManager; import javax.net.ssl.SSLEngine; import javax.net.ssl.SSLParameters; import javax.net.ssl.SSLServerSocketFactory; import javax.net.ssl.SSLSessionContext; import javax.net.ssl.TrustManager; import java.security.SecureRandom; import java.security.cert.X509Certificate; public final class SSLContextWrapper implements SSLContext { private final SSLFactory sslFactory; public SSLContextWrapper(SSLFactory sslFactory) { this.sslFactory = sslFactory; } @Override public void init(KeyManager[] kms, TrustManager[] tms, SecureRandom sr) { // not needed to initialize as it is already initialized } @Override public void destroy() { } @Override public SSLSessionContext getServerSessionContext() { return sslFactory.getSslContext().getServerSessionContext(); } @Override public SSLEngine createSSLEngine() { return sslFactory.getSSLEngine(); } @Override public SSLServerSocketFactory getServerSocketFactory() { return sslFactory.getSslServerSocketFactory(); } @Override public SSLParameters getSupportedSSLParameters() { return sslFactory.getSslParameters(); } @Override public X509Certificate[] getCertificateChain(String alias) { return sslFactory.getKeyManager() .map(keyManager -> keyManager.getCertificateChain("server")) .orElseThrow(); } @Override public X509Certificate[] getAcceptedIssuers() { return sslFactory.getTrustedCertificates().toArray(new X509Certificate[0]); } }
密钥库解码代码
无法直接提供密钥库文件,可通过以下代码将Base64编码字符串解码生成二进制文件:
import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardOpenOption; import java.util.Base64; public class Base64DecoderUtils { public static void main(String[] args) throws IOException { String identityContentAsBase64Encoded = ""; String trustStoreContentAsBase64Encoded = ""; Path identityPath = Paths.get("/path/to/identity.jks"); Path trustStorePath = Paths.get("/path/to/truststore.jks"); byte[] decoded = Base64.getDecoder().decode(String.join("", identityContentAsBase64Encoded.split(System.lineSeparator()))); Files.write(identityPath, decoded, StandardOpenOption.CREATE); decoded = Base64.getDecoder().decode(String.join("", trustStoreContentAsBase64Encoded.split(System.lineSeparator()))); Files.write(trustStorePath, decoded, StandardOpenOption.CREATE); } }
密钥库密码均为:secret
配置文件
application.yaml
server: port: 8443 ssl: client-auth: true keystore-path: identity.jks keystore-password: secret truststore-path: truststore.jks truststore-password: secret
依赖配置
<dependency> <groupId>io.github.hakky54</groupId> <artifactId>ayza</artifactId> <version>10.0.0</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot</artifactId> <version>2.7.5</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> <version>2.7.5</version> </dependency>
2023-10-23 更新
提供的KeyManager、SSLContext等配置被忽略,Tomcat似乎原本不支持此类自定义配置,已向Tomcat官方提交PR,但不确定是否会被采纳。
内容的提问来源于stack exchange,提问作者Hakan54
相关产品推荐
相关产品推荐

