使用 Tomcat 以编程方式配置 Spring Boot 服务器的 SSL 失败

问题描述 投票:0回答:1

我正在尝试使用 Tomcat 嵌入式服务器配置 Spring Boot 的 ssl 配置。 Spring Boot 默认提供了一个 tomcat。 SSL 可以使用应用程序属性进行配置,例如密钥库路径、信任库路径等。我不想使用属性来配置它,所以我只想以编程方式配置它。我已经开发了我的自己的 ssl 库,我想使用它,因为它有一些我需要使用的附加功能。过去我尝试配置它但失败了,所以我切换到使用 spring boot 和 jetty 或 netty 以编程方式配置我的服务器 ssl。然而,我仍然想再试一次。今天我仍然无法以编程方式配置它,也许有人可以帮助我。

我的堆栈跟踪是:

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

我的代码如下所示:

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;
    }

}
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();
    }

}
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);
    }

}
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 编码的字符串。因此,如果您想亲自尝试一下,您可以解码并创建二进制文件。

身份.jks

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
O3MCAwGGoA==

truststore.jks

MIIEjgIBAzCCBEcGCSqGSIb3DQEHAaCCBDgEggQ0MIIEMDCCBCwGCSqGSIb3DQEH
BqCCBB0wggQZAgEAMIIEEgYJKoZIhvcNAQcBMCkGCiqGSIb3DQEMAQYwGwQUCQPL
V1YP9wunGMWFyv1BwcRTIAACAwDDUICCA9jrocMHqNhm8aHENyLPmEi8qSuSPdut
++OBZvYRI5Bb2fFwye3dsoo9xEcSpDTcjp3cCiPPnGzcaWyvOvyTFtGr7AuUISte
qI2bfHd+bR1e7KhC4a8xZDhpsn7TmYTOHGhHJi7W4Yaw6UbgYhzfE++KpqNMfWBk
t4/4V2n3BNzShtxXDBw9CmT1Fdcv1lJ1j5YWKdgD0/p59ccEZHtiUvY6QOROBfjq
oI6kkMzW/mDJqQDKowLsW2O/UbPaRC1B8Ew0NSlHWhxaU347UFsotW8iKuXVnei0
gPiPfBFSuZ24MFaz6XENgEZcnNCJBeeK/MGma8i1rOADKbEaErzvlwBNm8BWtlC3
Ix4akym2XSy3pqFJrP+1cJJONwsI766IL72PUyXTBRtKEaqSd6jZZGkiyTPsiiK6
G3apckGnKpgJLyR6hrUd2SC8bwf92waRne9VihA0O4CovO9G0ab6ud02bEISVQAt
/n+lnU6FmjK3tGy1P/Kpx1vomkmZkdWqE56dvAQv5DRV7EZQ9TJU8FoAyP4E5RWD
1AQi7uDA233e1Wv4rYzWhwm/0XeFsdKw/N3rjmeNilgG4/KwSFFkl53fNECDDEmS
czcEkA99JU6nJBCuFF8kn0yVXTh+8ezgdSiT8LIUtqbldzKR8uMXFlzCMOCdJc21
cBC7DX9LA+Dz/pkcEj1smtDDSv+zM+DbEA4iJZBlF68xqiZzhrWcmbdtSKN5iUN/
kkLELUUjO76OIdxXILW2GHLHSe01fbqpHeY9NC732WmF329+ucODzCq9BjxARpAm
bCEX8tbcOrCqm3947JuSbgW6CxzDd2gzmYoE6d4LSfWLRgQ4SdLpgieVhfbsunBk
lnvCrnsa/VAQnEMZEzWCgvRAcuRs6bPvUXXFPwrz8OvQoPmdDRA4uWYfhEZZJjq4
uRGQRt+Bf4v5H1e2yzxSfRKlecJlhpVMyZtNsWTaNgZFX+c1SfzXXZGGMgLBEh9t
wLFdDWA848eXP97te7boqusR/UTXd6hLDmU5bb5NmjzY92aEK3LfIeLhFlKjWQKF
FooNxvKA/jyqUt/LxffAxK4co8ivyLzOMdHsPsqTjpkqjro5RgnYJTJWhBLMois5
sJej6EphOUzBMeEwgdlEs3pbT3/yp8QrhYKUG2HjkUVE2GKVwWNcjRfguGGK8MYC
B3ztnwPeAblpaPi4aQY55maYGXR3SPTAX2MVGScBfL8GhDZGr/yf7441wi6YntOH
CYts3VW/0zHAXNTaycdUTSMg9f7O312aYrk59Hd5Yfiw1StzMtdJMnMxlOdgTP33
w+wwPjAhMAkGBSsOAwIaBQAEFDdfecmJ7H2HFI93DLhyZ4r8Mx2iBBT/uNLzzuIO
SZI0e5jSFcFmOoYSvwIDAYag

这两个 base64 编码的字符串可以转换为二进制文件,代码片段如下。请务必将空内容替换为上面的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

我的应用程序属性文件是:

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>sslcontext-kickstart</artifactId>
    <version>8.1.7</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中启用它:https://github.com/apache/tomcat/pull/673

但是我不知道维护者是否会允许这种选项来使用自定义 sslcontext 配置服务器。

java spring-boot ssl tomcat sslcontext-kickstart
1个回答
0
投票

Tomcat 的维护者在以下 git commit 中调整了代码允许用户在 SSLHostConfigCertificate 上提供 SSLContext 实例,这使得能够以编程方式配置服务器的 ssl 配置。我在这里有一个工作示例作为演示:GitHub:Spring with Tomcat 和自定义 ssl 配置

我最终得到了下面的配置。

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)
                .withNeedClientAuthentication(isClientAuthenticationRequired)
                .build();
    }

}

import nl.altindag.ssl.SSLFactory;
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.beans.factory.annotation.Value;
import org.springframework.boot.web.embedded.tomcat.TomcatConnectorCustomizer;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SSLConnectorCustomizer implements TomcatConnectorCustomizer {

    private final SSLFactory sslFactory;
    private final int port;

    public SSLConnectorCustomizer(SSLFactory sslFactory, @Value("${server.port}") int port) {
        this.sslFactory = sslFactory;
        this.port = port;
    }

    @Override
    public void customize(Connector connector) {
        connector.setScheme("https");
        connector.setSecure(true);
        connector.setPort(port);

        AbstractHttp11Protocol<?> protocol = (AbstractHttp11Protocol<?>) connector.getProtocolHandler();
        protocol.setSSLEnabled(true);

        SSLHostConfig sslHostConfig = new SSLHostConfig();
        SSLHostConfigCertificate certificate = new SSLHostConfigCertificate(sslHostConfig, Type.UNDEFINED);
        certificate.setSslContext(new TomcatSSLContext(sslFactory));
        sslHostConfig.addCertificate(certificate);
        protocol.addSslHostConfig(sslHostConfig);
    }

}
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;
    }

}
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 TomcatSSLContext implements SSLContext {

    private final SSLFactory sslFactory;

    public TomcatSSLContext(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(alias))
                .orElseThrow();
    }

    @Override
    public X509Certificate[] getAcceptedIssuers() {
        return sslFactory.getTrustedCertificates().toArray(new X509Certificate[0]);
    }

}

以及以下依赖配置:

<dependencies>
    <dependency>
        <groupId>io.github.hakky54</groupId>
        <artifactId>sslcontext-kickstart</artifactId>
        <version>8.3.1</version>
    </dependency>

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
        <version>2.7.5</version>
    </dependency>
</dependencies>

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.apache.tomcat.embed</groupId>
            <artifactId>tomcat-embed-core</artifactId>
            <version>10.1.19</version>
        </dependency>
    </dependencies>
</dependencyManagement>
© www.soinside.com 2019 - 2024. All rights reserved.