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

合并Keystore后,单台Java服务器无法支持双网站HTTPS流量怎么办?

解决Java SSL服务器多证书(SNI)支持问题

核心问题

直接合并keystore后,Java默认的KeyManager不会根据客户端请求的SNI(Server Name Indication)域名自动选择对应证书,只会使用第一个匹配的证书,导致其中一个网站无法完成SSL握手。

必要步骤

1. 正确合并Keystore

合并时必须保证两个证书的别名唯一(避免覆盖),使用keytool命令完成导入:

# 复制site1.keystore作为基础的all.keystore
cp /path/to/site1.keystore /path/to/all.keystore
# 将site2的证书导入all.keystore,指定唯一别名(示例用site2)
keytool -importkeystore -srckeystore /path/to/site2.keystore -destkeystore /path/to/all.keystore -srcalias site2 -destalias site2 -srcstorepass changeit -deststorepass changeit

如果原keystore中的别名重复,先修改其中一个的别名:

keytool -changealias -alias originalAlias -destalias site2 -keystore /path/to/site2.keystore -storepass changeit

2. 实现支持SNI的自定义KeyManager

Java默认的SunX509 KeyManager不支持SNI域名匹配,需要自定义X509ExtendedKeyManager来根据请求的域名选择对应证书:

import javax.net.ssl.*;
import java.net.Socket;
import java.security.Principal;
import java.security.PrivateKey;
import java.security.cert.X509Certificate;
import java.util.HashMap;
import java.util.Map;

public class SNIX509KeyManager extends X509ExtendedKeyManager {
    private final X509KeyManager delegate;
    private final Map<String, String> domainAliasMap;

    public SNIX509KeyManager(X509KeyManager delegate, Map<String, String> domainAliasMap) {
        this.delegate = delegate;
        this.domainAliasMap = domainAliasMap;
    }

    @Override
    public String chooseEngineServerAlias(String keyType, Principal[] issuers, SSLEngine engine) {
        // 提取客户端请求的SNI域名
        SNIServerName sniName = engine.getSSLParameters().getServerNames().stream()
                .filter(name -> name.getType() == StandardConstants.SNI_HOST_NAME)
                .findFirst()
                .orElse(null);
        if (sniName != null) {
            String domain = ((SNIHostName)sniName).getAsciiName();
            // 根据域名匹配对应的证书别名
            return domainAliasMap.getOrDefault(domain, delegate.chooseEngineServerAlias(keyType, issuers, engine));
        }
        return delegate.chooseEngineServerAlias(keyType, issuers, engine);
    }

    // 以下方法直接委托给原始KeyManager
    @Override
    public String[] getClientAliases(String keyType, Principal[] issuers) {
        return delegate.getClientAliases(keyType, issuers);
    }

    @Override
    public String chooseClientAlias(String[] keyType, Principal[] issuers, Socket socket) {
        return delegate.chooseClientAlias(keyType, issuers, socket);
    }

    @Override
    public String[] getServerAliases(String keyType, Principal[] issuers) {
        return delegate.getServerAliases(keyType, issuers);
    }

    @Override
    public String chooseServerAlias(String keyType, Principal[] issuers, Socket socket) {
        return delegate.chooseServerAlias(keyType, issuers, socket);
    }

    @Override
    public X509Certificate[] getCertificateChain(String alias) {
        return delegate.getCertificateChain(alias);
    }

    @Override
    public PrivateKey getPrivateKey(String alias) {
        return delegate.getPrivateKey(alias);
    }
}

3. 修改TestServer代码,启用SNI支持

更新代码加载合并后的keystore,并配置自定义KeyManager:

import javax.net.ssl.*;
import java.io.FileInputStream;
import java.io.OutputStream;
import java.net.ServerSocket;
import java.net.Socket;
import java.security.KeyStore;
import java.util.HashMap;
import java.util.Map;

public class TestServer {
    public static void main(String[] args) throws Exception {
        int port = 1443;
        char[] passphrase = "changeit".toCharArray();
        String allKeystorePath = "/path/to/all.keystore";

        // 加载合并后的keystore
        KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
        ks.load(new FileInputStream(allKeystorePath), passphrase);

        // 初始化原始KeyManagerFactory
        KeyManagerFactory kmf = KeyManagerFactory.getInstance("SunX509");
        kmf.init(ks, passphrase);

        // 配置域名与证书别名的映射关系
        Map<String, String> domainAliasMap = new HashMap<>();
        domainAliasMap.put("site1.example.com", "site1"); // 替换为site1的实际域名和别名
        domainAliasMap.put("site2.example.com", "site2"); // 替换为site2的实际域名和别名

        // 创建支持SNI的KeyManager
        X509KeyManager originalKeyManager = (X509KeyManager) kmf.getKeyManagers()[0];
        SNIX509KeyManager sniKeyManager = new SNIX509KeyManager(originalKeyManager, domainAliasMap);

        // 初始化SSLContext,使用自定义KeyManager
        SSLContext ctx = SSLContext.getInstance("TLS");
        ctx.init(new KeyManager[]{sniKeyManager}, null, null);

        SSLServerSocketFactory ssf = ctx.getServerSocketFactory();
        SSLServerSocket ss = (SSLServerSocket) ssf.createServerSocket(port);

        // 显式启用SNI支持(Java7+默认启用,显式设置更稳妥)
        ss.setNeedClientAuth(false);
        ss.setEnabledProtocols(new String[]{"TLSv1.2", "TLSv1.3"});
        ss.setEnabledCipherSuites(ss.getSupportedCipherSuites());

        while (true) {
            Socket socket = ss.accept();
            OutputStream out = socket.getOutputStream();
            String response = "Hello from secure server";
            out.write(("HTTP/1.1 200 OK\r\nContent-Type: text/html\r\nContent-Length: " + response.length() + "\r\n\r\n" + response).getBytes());
            out.flush();
            socket.close();
        }
    }
}

4. 验证功能

使用openssl测试两个域名的SSL握手:

# 测试site1
openssl s_client -connect 你的服务器IP:1443 -servername site1.example.com
# 测试site2
openssl s_client -connect 你的服务器IP:1443 -servername site2.example.com

若两个命令都能正常返回对应证书信息并完成握手,说明配置成功。

关键注意点

  • 确保客户端支持SNI(现代浏览器、curl 7.18.1+等均支持)。
  • 合并后的keystore中所有证书的密钥库密码必须一致,否则需要额外处理多密码逻辑。
  • 建议使用Java8及以上版本,对SNI的支持更完善。

内容的提问来源于stack exchange,提问作者user3665736

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 18:46:13