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

Spring Boot应用中如何按请求动态加载TLS证书?

问题背景

我们的Spring Boot应用采用多租户架构,已通过HttpFilter实现租户级JWT验证:从请求头中获取Issuer URI和JWK集合URI存入租户上下文,用于后续JWT校验,核心代码如下:

@RequiredArgsConstructor
public class JwtPreAuthorizationConfigurer extends HttpFilter {
    private final OAuth2ResourceServerProperties properties;

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception {
        tenantContext.setIssuerUri(request.getHeader(Headers.ISSUER_URI.getName()));
        tenantContext.setJwkSetUri(request.getHeader(Headers.JWK_SET_URI.getName()));
        super.doFilter(request, response, chain);
    }
}
需求说明

现在需要实现租户级TLS动态配置:运行时根据请求来源(如请求头中的租户标识),从证书库中选择对应证书用于TLS通信,规则为:

  • 请求来自客户端A → 使用证书A
  • 请求来自客户端B → 使用证书B
  • 以此类推

现有application.yml中的SSL全局配置为:

server:
  ssl:
    enabled: true
    key-store: "classpath:my-keystore.p12"
    key-store-password: p4ssw0rd
    key-store-type: PKCS12

我们希望参考JWT验证的HttpFilter模式实现,比如以下骨架代码:

public class TLSPerRequestConfigurer extends HttpFilter {

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception {
        var tenantName = request.getHeader(Headers.TENANT_NAME.getName());
        // 使用tenantName动态配置TLS证书
        super.doFilter(request, response, chain);
    }
}

应用已通过Kubernetes容器化并部署在AWS环境,也接受非Spring相关的实现建议。

实现方案

一、Spring Boot原生实现思路

直接通过HttpFilter修改服务器端TLS证书不可行——因为服务器SSL上下文(SSLSocketFactory)是应用启动时初始化的全局配置,无法在请求链路中直接切换。但可以通过以下两种方式实现租户级TLS:

1. 多密钥库+动态SSL上下文切换

  • 提前将所有租户证书存入同一密钥库,或为每个租户单独维护密钥库文件
  • 自定义X509KeyManager实现,通过ThreadLocal感知当前租户,动态返回对应证书链和私钥
  • 替换Tomcat容器默认SSL配置,使用自定义SSL上下文
  • 在HttpFilter中解析租户标识,存入ThreadLocal上下文

核心代码示例:

自定义租户感知KeyManager

public class TenantAwareKeyManager implements X509KeyManager {
    private final Map<String, X509KeyManager> tenantKeyManagers = new ConcurrentHashMap<>();
    private final ThreadLocal<String> currentTenant = new ThreadLocal<>();

    public void addTenantKeyManager(String tenantId, X509KeyManager keyManager) {
        tenantKeyManagers.put(tenantId, keyManager);
    }

    public void setCurrentTenant(String tenantId) {
        currentTenant.set(tenantId);
    }

    @Override
    public String[] getClientAliases(String string, Principal[] prncpls) {
        return getCurrentKeyManager().getClientAliases(string, prncpls);
    }

    @Override
    public String chooseClientAlias(String[] strings, Principal[] prncpls, Socket socket) {
        return getCurrentKeyManager().chooseClientAlias(strings, prncpls, socket);
    }

    @Override
    public String[] getServerAliases(String string, Principal[] prncpls) {
        return getCurrentKeyManager().getServerAliases(string, prncpls);
    }

    @Override
    public String chooseServerAlias(String string, Principal[] prncpls, Socket socket) {
        return getCurrentKeyManager().chooseServerAlias(string, prncpls, socket);
    }

    @Override
    public X509Certificate[] getCertificateChain(String string) {
        return getCurrentKeyManager().getCertificateChain(string);
    }

    @Override
    public PrivateKey getPrivateKey(String string) {
        return getCurrentKeyManager().getPrivateKey(string);
    }

    private X509KeyManager getCurrentKeyManager() {
        String tenantId = currentTenant.get();
        if (tenantId == null || !tenantKeyManagers.containsKey(tenantId)) {
            throw new IllegalArgumentException("No valid TLS config for tenant: " + tenantId);
        }
        return tenantKeyManagers.get(tenantId);
    }
}

配置Tomcat容器SSL上下文

@Configuration
public class TenantTlsConfig {
    @Value("${server.ssl.key-store-password}")
    private String keyStorePassword;

    @Bean
    public TomcatServletWebServerFactory tomcatServletWebServerFactory(TenantAwareKeyManager tenantKeyManager) throws Exception {
        TomcatServletWebServerFactory factory = new TomcatServletWebServerFactory();
        factory.addConnectorCustomizers(connector -> {
            Http11NioProtocol protocol = (Http11NioProtocol) connector.getProtocolHandler();
            try {
                SSLContext sslContext = SSLContext.getInstance("TLS");
                sslContext.init(new KeyManager[]{tenantKeyManager}, null, null);
                protocol.setSslContext(sslContext);
            } catch (Exception e) {
                throw new RuntimeException("Failed to initialize tenant-aware SSL context", e);
            }
        });
        return factory;
    }

    @Bean
    public TenantAwareKeyManager tenantAwareKeyManager() throws Exception {
        TenantAwareKeyManager keyManager = new TenantAwareKeyManager();
        // 加载租户A密钥库
        KeyStore tenantAKeyStore = KeyStore.getInstance("PKCS12");
        try (InputStream is = getClass().getResourceAsStream("/tenant-a-keystore.p12")) {
            tenantAKeyStore.load(is, keyStorePassword.toCharArray());
        }
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(tenantAKeyStore, keyStorePassword.toCharArray());
        keyManager.addTenantKeyManager("tenant-a", (X509KeyManager) kmf.getKeyManagers()[0]);

        // 加载租户B密钥库,以此类推
        // ...

        return keyManager;
    }

    @Bean
    public FilterRegistrationBean<TLSPerRequestConfigurer> tlsPerRequestFilter(TenantAwareKeyManager tenantKeyManager) {
        FilterRegistrationBean<TLSPerRequestConfigurer> registrationBean = new FilterRegistrationBean<>();
        registrationBean.setFilter(new TLSPerRequestConfigurer(tenantKeyManager));
        registrationBean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 确保在JWT过滤器前执行
        return registrationBean;
    }
}

租户TLS过滤器

public class TLSPerRequestConfigurer extends HttpFilter {
    private final TenantAwareKeyManager tenantKeyManager;

    public TLSPerRequestConfigurer(TenantAwareKeyManager tenantKeyManager) {
        this.tenantKeyManager = tenantKeyManager;
    }

    @Override
    protected void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws Exception {
        String tenantName = request.getHeader(Headers.TENANT_NAME.getName());
        try {
            tenantKeyManager.setCurrentTenant(tenantName);
            chain.doFilter(request, response);
        } finally {
            tenantKeyManager.setCurrentTenant(null); // 清理ThreadLocal
        }
    }
}

2. Spring Cloud Gateway路由绑定(若应用为网关)

如果应用是API网关,可将租户TLS配置与路由绑定:

  • 为每个租户配置独立路由规则,匹配租户请求头
  • 每个路由单独配置对应的SSL密钥库、密码等参数
  • Gateway会根据路由动态选择TLS证书对外通信

二、非Spring云原生方案(适配K8s+AWS)

1. AWS ALB+Ingress动态TLS

  • 将租户证书上传至AWS Certificate Manager(ACM)
  • 配置AWS ALB作为Ingress控制器,基于主机头/路径设置路由规则
  • 为每个租户的域名/路径绑定对应ACM证书
  • 完全通过云组件实现,无需修改应用代码

2. K8s多Ingress+Cert-Manager

  • 使用Cert-Manager为每个租户自动签发证书(支持ACM、Let's Encrypt等)
  • 为每个租户创建独立Ingress资源,配置对应TLS证书和路由规则
  • 流量通过NGINX等Ingress控制器时,自动匹配对应Ingress使用证书

3. Istio服务网格动态TLS

  • 部署Istio服务网格,为每个租户配置独立Gateway/VirtualService
  • 基于请求头配置路由规则,绑定对应TLS证书
  • Istio Sidecar自动处理证书切换,应用无需感知

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.04 16:50:42