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

基于Netty与Spring RestController的双向TLS场景获取客户端SSL证书

双向TLS场景下获取客户端证书及Netty通道信息(Spring WebFlux 2.7.4)

一、先确认双向TLS配置生效

不管是通过代码配置Reactor Netty,还是用Spring Boot配置文件,必须开启强制客户端认证:

方式1:Spring Boot配置文件(application.yml)

server:
  ssl:
    enabled: true
    key-store: classpath:server.jks # 服务端证书库
    key-store-password: your-server-keystore-pwd
    key-password: your-server-key-pwd
    trust-store: classpath:client-truststore.jks # 存放信任的客户端CA证书
    trust-store-password: your-truststore-pwd
    client-auth: required # 强制要求客户端提供证书

方式2:代码配置Reactor Netty

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import reactor.netty.http.server.HttpServer;
import reactor.netty.tls.TlsServerSpec;

@Configuration
public class NettyTlsConfig {
    @Bean
    public HttpServer httpServer() {
        return HttpServer.create()
                .secure(spec -> spec
                        .sslContextBuilder(sslCtxBuilder -> {
                            // 加载服务端证书、私钥及信任的客户端CA
                            sslCtxBuilder.keyManager("server.crt", "server.key")
                                    .trustManager("client-ca.crt");
                        })
                        .clientAuth(TlsServerSpec.ClientAuth.REQUIRED));
    }
}

二、在RestController中获取客户端SSL证书

通过ServerHttpRequest拿到SSL会话,进而提取客户端证书的subjectAltName字段:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.http.server.reactive.ServerHttpRequest;
import javax.net.ssl.SSLSession;
import java.security.cert.X509Certificate;
import java.util.Optional;

@RestController
public class SecureApiController {

    @GetMapping("/api/verify-client")
    public String verifyClientCert(ServerHttpRequest request) {
        SSLSession sslSession = request.getSslInfo().getSession();
        
        try {
            // 获取客户端证书链,第一个元素是客户端自身证书
            X509Certificate[] clientCerts = (X509Certificate[]) sslSession.getPeerCertificates();
            if (clientCerts.length == 0) {
                throw new SecurityException("未获取到客户端证书");
            }
            
            X509Certificate clientCert = clientCerts[0];
            // 提取DNS类型的subjectAltName(类型标识2),按需调整类型
            Optional<String> sanDns = clientCert.getSubjectAlternativeNames().stream()
                    .filter(entry -> (int) entry.get(0) == 2)
                    .map(entry -> entry.get(1).toString())
                    .findFirst();
            
            if (sanDns.isEmpty()) {
                throw new SecurityException("客户端证书无有效DNS类型的subjectAltName");
            }
            
            // 执行subjectAltName验证逻辑
            if (!"allowed-client.example.com".equals(sanDns.get())) {
                throw new SecurityException("客户端证书subjectAltName验证不通过");
            }
            
            return String.format("客户端证书验证通过,subjectAltName: %s", sanDns.get());
        } catch (Exception e) {
            throw new SecurityException("客户端证书验证失败", e);
        }
    }
}

三、获取Netty通道(Channel)信息

通过ServerWebExchange获取底层Netty请求实例,进而拿到通道详情:

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import reactor.netty.http.server.HttpServerRequest;
import reactor.netty.channel.ChannelOperations;

@RestController
public class ChannelInfoController {

    @GetMapping("/api/channel-details")
    public String getChannelInfo(ServerWebExchange exchange) {
        // 获取底层Netty请求对象
        HttpServerRequest nettyRequest = exchange.getNativeRequest(HttpServerRequest.class);
        if (nettyRequest == null) {
            return "无法获取Netty请求实例";
        }
        
        ChannelOperations<?, ?> channelOps = nettyRequest.channelOperations();
        String channelId = channelOps.channel().id().asShortText();
        String remoteAddr = channelOps.channel().remoteAddress().toString();
        
        return String.format("通道ID: %s\n客户端远程地址: %s", channelId, remoteAddr);
    }
}

关键注意点

  • 服务端信任库必须包含客户端证书的签发CA,否则TLS握手直接失败
  • getPeerCertificates()会在客户端未提供证书时抛出SSLPeerUnverifiedException,需捕获处理
  • subjectAltName支持多种类型(DNS=2、IP=7等),根据业务需求调整过滤规则
  • 代码适配Spring Boot 2.7.4及对应Reactor Netty 1.0.x版本

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.29 18:45:30