You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.08 02:25:53