mTSL: netty单向/双向TLS连接

news2024/9/23 9:30:05

创建证书

不管是单向tls还是双向tls(mTLS),都需要创建证书。
创建证书可以使用openssl或者keytool,openssl 参考 mTLS: openssl创建CA证书

单向/双向tls需要使用到的相关文件:

文件单向tls双向tlsServer端Client端备注
ca.key----需要保管好,后面ca.crt续期或者生成server/client证书时需要使用它进行签名
ca.crt可选需要可选可选CA 证书
server.key需要需要需要-服务端密钥,与 pkcs8_server.key 任选一个使用
pkcs8_server.key需要需要需要-PK8格式的服务端密钥,与 server.key 任选一个使用
server.crt需要需要需要-服务端证书
client.key-需要-需要客户端密钥,与 pkcs8_client.key 任选一个使用
pkcs8_client.key-需要-需要PK8格式的客户端密钥,与 client.key 任选一个使用
client.crt-需要-需要客户端证书

netty单向/双向TLS

在netty中tls的处理逻辑是由SslHandler完成的,SslHandler对象创建方式有两种:

  • 通过Java Ssl相关接口+jks密钥库创建SslEngine,再将SslEngine做为构造参数创建SslHandler对象。
  • 通过netty 的SslContextBuilder创建SslContext对象,再由SslContext对象创建SslHandler对象。

ava Ssl相关接口+jks密钥库生成SslHandler的流程如下图所示:
在这里插入图片描述

SslContextBuidler创建SslHandler的方法相对简单,如下:
在这里插入图片描述

关于SslContextBuidler创建SslContext对象和SslHandler对象的方式是本篇文章的重点,后面详细描述。

创建Server端和Client的BootStrap

先是将Server端的ServerBootStrap和Client端的BootStrap对象创建好,并初始化完成,能够在非tls场景下正常通信。

Server端ServerBootstrap
Server端创建ServerBootstrap, 添加编解码器和业务逻辑Handler,监听端口。代码如下:

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.epoll.EpollServerSocketChannel;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import lombok.extern.slf4j.Slf4j;
import org.netty.NettyHelper;

import javax.net.ssl.SSLException;
import java.net.InetSocketAddress;
import java.security.cert.CertificateException;

@Slf4j
public class NettyTLSServer {


    private InetSocketAddress bindAddress;
    private ServerBootstrap bootstrap;
    private EventLoopGroup bossGroup;
    private EventLoopGroup workerGroup;

    public NettyTLSServer() {
        this(8080);
    }

    public NettyTLSServer(int bindPort) {
        this("localhost", bindPort);
    }

    public NettyTLSServer(String bindIp, int bindPort) {
        bindAddress = new InetSocketAddress(bindIp, bindPort);
    }


    private void init() throws CertificateException, SSLException {
        bootstrap = new ServerBootstrap();

        bossGroup = NettyHelper.eventLoopGroup(1, "NettyServerBoss");
        workerGroup = NettyHelper.eventLoopGroup(Math.min(Runtime.getRuntime().availableProcessors() + 1, 32), "NettyServerWorker");

        bootstrap.group(bossGroup, workerGroup)
                .channel(NettyHelper.shouldEpoll() ? EpollServerSocketChannel.class : NioServerSocketChannel.class)
                .option(ChannelOption.SO_REUSEADDR, Boolean.TRUE)
                .childOption(ChannelOption.TCP_NODELAY, Boolean.TRUE)
                .childOption(ChannelOption.SO_KEEPALIVE, Boolean.TRUE)
                .childOption(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                .childOption(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);

        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                log.info("accept client: {} {}", ch.remoteAddress().getHostName(), ch.remoteAddress().getPort());
                ChannelPipeline pipeline = ch.pipeline();
                pipeline
                        //添加字节消息解码器
                        .addLast(new LineBasedFrameDecoder(1024))
                        //添加消息解码器,将字节转换为String
                        .addLast(new StringDecoder())
                        //添加消息编码器,将String转换为字节
                        .addLast(new StringEncoder())
                        //业务逻辑处理Handler
                        .addLast(new ChannelDuplexHandler() {

                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.info("received message from client: {}", msg);
                                ctx.writeAndFlush("server response: " + msg);
                            }

                            @Override
                            public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) throws Exception {
                                log.info("occur exception, close channel:{}.", ctx.channel().remoteAddress(), cause);
                                ctx.channel().closeFuture()
                                        .addListener(future -> {
                                            log.info("close client channel {}: {}",
                                                    ctx.channel().remoteAddress(),
                                                    future.isSuccess());
                                        });
                            }
                        });
            }
        });
    }

    public void bind(boolean sync) throws CertificateException, SSLException {
        init();
        try {
            ChannelFuture channelFuture = bootstrap.bind(bindAddress).sync();
            if (channelFuture.isDone()) {
                log.info("netty server start at house and port: {} ", bindAddress.getPort());
            }
            Channel channel = channelFuture.channel();
            ChannelFuture closeFuture = channel.closeFuture();
            if (sync) {
                closeFuture.sync();
            }
        } catch (Exception e) {
            log.error("netty server start exception,", e);
        } finally {
            if (sync) {
                shutdown();
            }
        }
    }


    public void shutdown() {
        log.info("netty server shutdown");
        log.info("netty server shutdown bossEventLoopGroup&workerEventLoopGroup gracefully");
        bossGroup.shutdownGracefully();
        workerGroup.shutdownGracefully();
    }

}

Client端BootStrap
Client端创建Bootstrap, 添加编解码器和业务逻辑Handler,建立连接。代码如下:

import io.netty.bootstrap.Bootstrap;
import io.netty.buffer.PooledByteBufAllocator;
import io.netty.channel.*;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.codec.LineBasedFrameDecoder;
import io.netty.handler.codec.string.StringDecoder;
import io.netty.handler.codec.string.StringEncoder;
import lombok.extern.slf4j.Slf4j;
import org.netty.NettyHelper;

import javax.net.ssl.SSLException;
import java.net.InetSocketAddress;

@Slf4j
public class NettyTLSClient {


    private InetSocketAddress serverAddress;

    private Bootstrap bootstrap;
    private EventLoopGroup workerGroup;

    private Channel channel;


    public NettyTLSClient(String severHost, int serverPort) {
        serverAddress = new InetSocketAddress(severHost, serverPort);
    }

    public void init() throws SSLException {
        bootstrap = new Bootstrap();
        workerGroup = NettyHelper.eventLoopGroup(1, "NettyClientWorker");
        bootstrap.group(workerGroup)
                .option(ChannelOption.SO_KEEPALIVE, true)
                .option(ChannelOption.TCP_NODELAY, true)
                .option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT)
                .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000)
                .remoteAddress(serverAddress)
                .channel(NettyHelper.socketChannelClass());

        bootstrap.handler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) {
                final ChannelPipeline pipeline = ch.pipeline();
                pipeline
                        //添加字节消息解码器
                        .addLast(new LineBasedFrameDecoder(1024))
                        //添加消息解码器,将字节转换为String
                        .addLast(new StringDecoder())
                        //添加消息编码器,将String转换为字节
                        .addLast(new StringEncoder())
                        //业务逻辑处理Handler
                        .addLast(new ChannelDuplexHandler() {

                            @Override
                            public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
                                log.info("received message from server: {}", msg);
                                super.channelRead(ctx, msg);
                            }
                        });

            }
        });
    }

    public ChannelFuture connect() throws SSLException {
        init();
        //开始连接
        final ChannelFuture promise = bootstrap.connect(serverAddress.getHostName(), serverAddress.getPort());
//        final ChannelFuture promise = bootstrap.connect();
        promise.addListener(future -> {
            log.info("client connect to server: {}", future.isSuccess());
        });
        channel = promise.channel();
        return promise;
    }


    public void shutdown() {
        log.info("netty client shutdown");
        channel.closeFuture()
                .addListener(future -> {
                    log.info("netty client shutdown workerEventLoopGroup gracefully");
                    workerGroup.shutdownGracefully();
                });
    }

    public Channel getChannel() {
        return channel;
    }

}

工具类: NettyHelper
主要用是创建EventLoopGroup和判断是否支持Epoll,代码如下:

import io.netty.channel.EventLoopGroup;
import io.netty.channel.epoll.Epoll;
import io.netty.channel.epoll.EpollEventLoopGroup;
import io.netty.channel.epoll.EpollSocketChannel;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.util.concurrent.DefaultThreadFactory;

import java.util.concurrent.ThreadFactory;


public class NettyHelper {
    static final String NETTY_EPOLL_ENABLE_KEY = "netty.epoll.enable";

    static final String OS_NAME_KEY = "os.name";

    static final String OS_LINUX_PREFIX = "linux";

    public static EventLoopGroup eventLoopGroup(int threads, String threadFactoryName) {
        ThreadFactory threadFactory = new DefaultThreadFactory(threadFactoryName, true);
        return shouldEpoll() ? new EpollEventLoopGroup(threads, threadFactory) :
                new NioEventLoopGroup(threads, threadFactory);
    }


    public static boolean shouldEpoll() {
        if (Boolean.parseBoolean(System.getProperty(NETTY_EPOLL_ENABLE_KEY, "false"))) {
            String osName = System.getProperty(OS_NAME_KEY);
            return osName.toLowerCase().contains(OS_LINUX_PREFIX) && Epoll.isAvailable();
        }

        return false;
    }


    public static Class<? extends SocketChannel> socketChannelClass() {
        return shouldEpoll() ? EpollSocketChannel.class : NioSocketChannel.class;
    }
}

构建单向tls

创建SslContext

自签名证书的SslContext(测试场景)
Server 端

在单向tls场景中,主要是server端需要证书,所以在Server侧需要SelfSignedCertificate对象来生成密钥和证书,同时创建并返回netty的SslContextBuilder构造器创建SslContext对象。代码如下:

public class SslContextUtils {
	/**
     * 创建server SslContext
     * 会自动创建一个临时自签名的证书 -- Generates a temporary self-signed certificate
     *
     * @return
     * @throws CertificateException
     * @throws SSLException
     */
    public static SslContext createTlsServerSslContext() throws CertificateException, SSLException {
        SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK;
        SelfSignedCertificate cert = new SelfSignedCertificate();
        return SslContextBuilder.forServer(cert.certificate(), cert.privateKey())
                .sslProvider(provider)
                .protocols("TLSv1.3", "TLSv1.2")
                .build();
    }
}

在netty ChannelPipeline的初始化Channel逻辑中,通过SslContext生成SslHandler对象,并将其添加到ChannelPipeline中。

Client 端

客户端简单很多,可以不需要证书,因为在单向tls中只在client验证验证服务端的证书是否合法。代码如下:

public class SslContextUtils {
	public static SslContext createTlsClientSslContext() throws SSLException {
        SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK;
        return SslContextBuilder.forClient()
                .sslProvider(provider)
                .trustManager(InsecureTrustManagerFactory.INSTANCE)
                .protocols("TLSv1.3", "TLSv1.2")
                .build();
    }
}
openssl证书创建SslContext

使用openssl 生成证书, 需要的文件如下:

文件Server端Client端备注
ca.crt可选可选CA 证书
server.key需要-服务端密钥,与 pkcs8_server.key 任选一个使用
pkcs8_server.key需要-PK8格式的服务端密钥,与 server.key 任选一个使用
server.crt需要-服务端证书
SslContextUtils将文件转InputStream

如果出现文件相关的报错,可以尝试先将文件将流。
SslContextUtils中文件转InputStream的方法如下:

public class SslContextUtils {

}public static InputStream openInputStream(File file) {
        try {
            return file == null ? null : file.toURI().toURL().openStream();
        } catch (IOException e) {
            throw new IllegalArgumentException("Could not find certificate file or the certificate is invalid.", e);
        }
    }


    private static void safeCloseStream(InputStream stream) {
        if (stream == null) {
            return;
        }
        try {
            stream.close();
        } catch (IOException e) {
            log.warn("Failed to close a stream.", e);
        }
    }
Server 端

逻辑跟自签名证书创建SslContext是一样的,只是将服务端密钥和证书换成了使用openssl生成。
在生成服务端证书时,会用到ca证书,所以也可以把ca证书加入到TrustManager中 ,当然这一步骤是可选的。
代码如下:

public class SslContextUtils {
	public static SslContext createServerSslContext(File keyCertChainFile, File keyFile, String keyPassword, File trustCertFile){
        try (InputStream keyCertChainInputStream = openInputStream(keyCertChainFile);
             InputStream keyInputStream = openInputStream(keyFile);
             InputStream trustCertFileInputStream = openInputStream(trustCertFile)) {
            SslContextBuilder builder;
            if (keyPassword != null) {
                builder = SslContextBuilder.forServer(keyCertChainInputStream, keyInputStream, keyPassword);
            } else {
                builder = SslContextBuilder.forServer(keyCertChainInputStream, keyInputStream);
            }
            if (trustCertFile != null) {
                builder.trustManager(trustCertFileInputStream);
            }

            try {
                SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK;
                return builder
                        .sslProvider(provider)
                        .protocols("TLSv1.3", "TLSv1.2")
                        .build();
            } catch (SSLException e) {
                throw new IllegalStateException("Build SslSession failed.", e);
            }
        } catch (IOException e) {
            throw new IllegalArgumentException("Could not find certificate file or the certificate is invalid.", e);
        }
    }
}
Client 端

client端的逻辑是同自签名证书创建SslContext是一样的,不过要支持ca证书需要稍做调整:

public class SslContextUtils {
	public static SslContext createClientSslContext(File trustCertFile) {
        try (InputStream trustCertFileInputStream = openInputStream(trustCertFile)) {
            SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK;
            SslContextBuilder builder = SslContextBuilder.forClient()
                    .sslProvider(provider)
                    .protocols("TLSv1.3", "TLSv1.2");
            if (trustCertFile != null) {
                builder.trustManager(InsecureTrustManagerFactory.INSTANCE);
            } else {
                builder.trustManager(trustCertFileInputStream);
            }
            return builder.build();
        } catch (SSLException e) {
            throw new IllegalStateException("Build SslSession failed.", e);
        } catch (IOException e) {
            throw new IllegalArgumentException("Could not find certificate file or the certificate is invalid.", e);
        }
    }
}

添加SslHandler,完成ssl handshake

在服务端和客户端的BootStrap对Channel的初始化逻辑做些调整,添加SslHandler和TlsHandler。
它们的用途分别如下:

  • SslHandler是netty提供用来建立tls连接和握手。
  • TlsHandler用于检查ssl handshake,如果是在客户端场景,会将服务端的证书信息打印出来。
Server端

在NettyTLSServer.init()方法中,对Channel的初始化逻辑做调整,添加SslHandler和TlsHandler。

Channel的初始化方法在ChannelInitializer中,代码如下:

@Slf4j
public class NettyTLSServer {
	public void init() throws CertificateException, SSLException {
	
		...

		//创建一个临时自签名证书的SslContext对象
//		 SslContext sslContext = SslContextUtils.createServerSslContext();

        //使用openssl 生成的私钥和证书创建SslContext对象, 不传ca.crt
        SslContext sslContext = SslContextUtils.createServerSslContext(
                new File("./cert/server.crt"),
                new File("./cert/server.key"),
                null,
                null);

        //使用openssl 生成的私钥和证书创建SslContext对象,传ca.crt
//        SslContext sslContext = SslContextUtils.createServerSslContext(
//                new File("./cert/server.crt"),
//                new File("./cert/server.key"),
//                null,
//                new File("./cert/ca.crt"));

        //创建TlsHandler对象,该Handler会进行ssl handshake检查
        TlsHandler tlsHandler = new TlsHandler(true);
		
		//将ChannelInitializer设置为ServerBootstrap对象的childHandler
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {

			// SocketChannel 初始化方法,该方法在Channel注册后只会被调用一次
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                log.info("accept client: {} {}", ch.remoteAddress().getHostName(), ch.remoteAddress().getPort());
                ChannelPipeline pipeline = ch.pipeline();
                pipeline
                        // 添加SslHandler
                        .addLast(sslContext.newHandler(ch.alloc()))
                        // 添加TslHandler
                        .addLast(tlsHandler)
                        //添加字节消息解码器
                        .addLast(new LineBasedFrameDecoder(1024))
                        //添加消息解码器,将字节转换为String
                        .addLast(new StringDecoder())
                        //添加消息编码器,将String转换为字节
                        .addLast(new StringEncoder(){
                            @Override
                            protected void encode(ChannelHandlerContext ctx, CharSequence msg, List<Object> out) throws Exception {
                                super.encode(ctx, msg + "\n", out);
                            }
                        })
                        //业务逻辑处理Handler
                        .addLast(new ChannelDuplexHandler() {

                            ...
                            
                        });
            }
        });
    }
}
Client端

在NettyTLSClient.init()方法中,对Channel的初始化逻辑做调整,添加SslHandler和TlsHandler。

Channel的初始化方法在ChannelInitializer中,代码如下:

public class NettyTLSClient {
	public void init() throws SSLException {

		...
	
		// 创建SslContext对象,不传ca.crt
        SslContext sslContext = SslContextUtils.createClientSslContext();

        // 使用openssl 生成的Ca证书创建SslContext对象,传ca.crt
//        SslContext sslContext = SslContextUtils.createClientSslContext(new File("./cert/ca.crt"));

        //创建TlsHandler对象,该Handler会进行ssl handshake检查,并会将服务端的证书信息打印出来
        TlsHandler tlsHandler = new TlsHandler(false);
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) {
                final ChannelPipeline pipeline = ch.pipeline();
                pipeline
                        // 添加ssl Handler
                        .addLast(sslContext.newHandler(ch.alloc()))
                        // 添加TslHandler
                        .addLast(tlsHandler)
                        //添加字节消息解码器
                        .addLast(new LineBasedFrameDecoder(1024))
                        //添加消息解码器,将字节转换为String
                        .addLast(new StringDecoder())
                        //添加消息编码器,将String转换为字节
                        .addLast(new StringEncoder(){
                            @Override
                            protected void encode(ChannelHandlerContext ctx, CharSequence msg, List<Object> out) throws Exception {
                                super.encode(ctx, msg + "\n", out);
                            }
                        })
                        //业务逻辑处理Handler
                        .addLast(new ChannelDuplexHandler() {

							...
							
                        });

            }
        });
	}
}
TlsHandler

代码如下:

import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import lombok.extern.slf4j.Slf4j;

import javax.net.ssl.SSLSession;
import javax.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Date;


@ChannelHandler.Sharable
@Slf4j
public class TlsHandler extends ChannelDuplexHandler {

    private boolean serverSide;

    public TlsHandler(boolean serverSide) {
        this.serverSide = serverSide;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.pipeline()
        	.get(SslHandler.class)
        	.handshakeFuture()
        	.addListener(
                new GenericFutureListener<Future<Channel>>() {
                    @Override
                    public void operationComplete(Future<Channel> future) throws Exception {
                        if (future.isSuccess()) {
                            log.info("[{}] {} 握手成功", getSideType(), ctx.channel().remoteAddress());
                            SSLSession ss = ctx.pipeline().get(SslHandler.class).engine().getSession();
                            log.info("[{}] {} cipherSuite: {}", getSideType(), ctx.channel().remoteAddress(), ss.getCipherSuite());
                            if (!serverSide) {
                                X509Certificate cert = ss.getPeerCertificateChain()[0];
                                String info = null;
                                // 获得证书版本
                                info = String.valueOf(cert.getVersion());
                                System.out.println("证书版本:" + info);
                                // 获得证书序列号
                                info = cert.getSerialNumber().toString(16);
                                System.out.println("证书序列号:" + info);
                                // 获得证书有效期
                                Date beforedate = cert.getNotBefore();
                                info = new SimpleDateFormat("yyyy/MM/dd").format(beforedate);
                                System.out.println("证书生效日期:" + info);
                                Date afterdate = (Date) cert.getNotAfter();
                                info = new SimpleDateFormat("yyyy/MM/dd").format(afterdate);
                                System.out.println("证书失效日期:" + info);
                                // 获得证书主体信息
                                info = cert.getSubjectDN().getName();
                                System.out.println("证书拥有者:" + info);
                                // 获得证书颁发者信息
                                info = cert.getIssuerDN().getName();
                                System.out.println("证书颁发者:" + info);
                                // 获得证书签名算法名称
                                info = cert.getSigAlgName();
                                System.out.println("证书签名算法:" + info);
                            }
                        } else {
                            log.warn("[{}] {} 握手失败,关闭连接", getSideType(), ctx.channel().remoteAddress());
                            ctx.channel().closeFuture().addListener(closeFuture -> {
                                log.info("[{}] {} 关闭连接:{}", getSideType(), ctx.channel().remoteAddress(), closeFuture.isSuccess());
                            });
                        }
                    }
                });

        SocketChannel channel = (SocketChannel) ctx.channel();
    }


    private String getSideType() {
        return serverSide ? "SERVER" : "CLIENT";
    }
}

构建双向tls (mTLS)

创建MTls的SslContext

在SslContextUtils中添加两个方法,分别是:

  • 创建服务端MTls SslContext的对象
  • 创建客户端MTls 的SslContext

代码如下:

public class SslContextUtils {
	/**
     * 创建服务端MTls 的SslContext
     *
     * @param keyCertChainFile 服务端证书
     * @param keyFile          服务端私钥
     * @param keyPassword      服务端私钥加密密码
     * @param trustCertFile    CA证书
     * @return
     */
    public static SslContext createServerMTslContext(File keyCertChainFile, File keyFile, String keyPassword, File trustCertFile) {
        SslContextBuilder builder;
        try (InputStream keyCertChainInputStream = openInputStream(keyCertChainFile);
             InputStream keyInputStream = openInputStream(keyFile);
             InputStream trustCertFileInputStream = openInputStream(trustCertFile)) {

            if (keyPassword != null) {
                builder = SslContextBuilder.forServer(keyCertChainInputStream, keyInputStream, keyPassword);
            } else {
                builder = SslContextBuilder.forServer(keyCertChainInputStream, keyInputStream);
            }
            builder.trustManager(trustCertFileInputStream);
            builder.clientAuth(ClientAuth.REQUIRE);

            try {
                SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK;
                return builder
                        .sslProvider(provider)
                        .protocols("TLSv1.3", "TLSv1.2")
                        .build();
            } catch (SSLException e) {
                throw new IllegalStateException("Build SslSession failed.", e);
            }
        } catch (IOException e) {
            throw new IllegalArgumentException("Could not find certificate file or the certificate is invalid.", e);
        }
    }

    /**
     * 创建客户端MTls 的SslContext
     *
     * @param keyCertChainFile 客户端证书
     * @param keyFile          客户端私钥
     * @param keyPassword      客户端私钥加密密码
     * @param trustCertFile    CA证书
     * @return
     */
    public static SslContext createClientMTslContext(File keyCertChainFile, File keyFile, String keyPassword, File trustCertFile) {
        try (InputStream keyCertChainInputStream = openInputStream(keyCertChainFile);
             InputStream keyInputStream = openInputStream(keyFile);
             InputStream trustCertFileInputStream = openInputStream(trustCertFile)) {
            SslContextBuilder builder = SslContextBuilder.forClient();
            builder.trustManager(trustCertFileInputStream);
            if (keyPassword != null) {
                builder.keyManager(keyCertChainInputStream, keyInputStream, keyPassword);
            } else {
                builder.keyManager(keyCertChainInputStream, keyInputStream);
            }

            try {
                SslProvider provider = SslProvider.isAlpnSupported(SslProvider.OPENSSL) ? SslProvider.OPENSSL : SslProvider.JDK;
                return builder
                        .sslProvider(provider)
                        .protocols("TLSv1.3", "TLSv1.2")
                        .build();
            } catch (SSLException e) {
                throw new IllegalStateException("Build SslSession failed.", e);
            }
        } catch (IOException e) {
            throw new IllegalArgumentException("Could not find certificate file or the certificate is invalid.", e);
        }
    }
}

BootStrap对Channel的初始化逻辑

同单向Tls一样,要服务端和客户端的BootStrap对Channel的初始化逻辑做些调整,主要是SslContext的调整。所以在单向ssl的代码基础上做些调整就可以了。

服务端在NettyTLSServer.init()方法中将SslContext改成调用SslContextUtils.createServerMTslContext()创建。
代码如下:

public class NettyTLSServer {
	public void init() throws CertificateException, SSLException {
	
		...

		//使用openssl 生成的私钥和证书创建支持mtls的SslContext对象
        SslContext sslContext = SslContextUtils.createServerMTslContext(
                new File("./cert/server.crt"),
                new File("./cert/pkcs8_server.key"),
                null,
                new File("./cert/ca.crt"));

        //创建TlsHandler对象,该Handler会进行ssl handshake检查,会将对端的证书信息打印出来
        TlsHandler tlsHandler = new TlsHandler(true, true);
        
		
		//将ChannelInitializer设置为ServerBootstrap对象的childHandler
        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {

			// SocketChannel 初始化方法,该方法在Channel注册后只会被调用一次
            @Override
            protected void initChannel(SocketChannel ch) throws Exception {
                log.info("accept client: {} {}", ch.remoteAddress().getHostName(), ch.remoteAddress().getPort());
                ChannelPipeline pipeline = ch.pipeline();
                pipeline
                        // 添加SslHandler
                        .addLast(sslContext.newHandler(ch.alloc()))
                        // 添加TslHandler
                        .addLast(tlsHandler)
                        //添加字节消息解码器
                        .addLast(new LineBasedFrameDecoder(1024))
                        //添加消息解码器,将字节转换为String
                        .addLast(new StringDecoder())
                        //添加消息编码器,将String转换为字节
                        .addLast(new StringEncoder(){
                            @Override
                            protected void encode(ChannelHandlerContext ctx, CharSequence msg, List<Object> out) throws Exception {
                                super.encode(ctx, msg + "\n", out);
                            }
                        })
                        //业务逻辑处理Handler
                        .addLast(new ChannelDuplexHandler() {

                            ...
                            
                        });
            }
        });
    }
}

客户端在NettyTLSClient.init()方法中将SslContext改成调用SslContextUtils.createClientMTslContext()创建。
代码如下:

```java
public class NettyTLSClient {
	public void init() throws SSLException {

		...

		//使用openssl 生成的私钥和证书创建支持mtls的SslContext对象
        SslContext sslContext = SslContextUtils.createClientMTslContext(
                new File("./cert/client.crt"),
                new File("./cert/pkcs8_client.key"),
                null,
                new File("./cert/ca.crt"));

        //创建TlsHandler对象,该Handler会进行ssl handshake检查,并会将对端的证书信息打印出来
        TlsHandler tlsHandler = new TlsHandler(true, false);	

        bootstrap.handler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(SocketChannel ch) {
                final ChannelPipeline pipeline = ch.pipeline();
                pipeline
                        // 添加ssl Handler
                        .addLast(sslContext.newHandler(ch.alloc()))
                        // 添加TslHandler
                        .addLast(tlsHandler)
                        //添加字节消息解码器
                        .addLast(new LineBasedFrameDecoder(1024))
                        //添加消息解码器,将字节转换为String
                        .addLast(new StringDecoder())
                        //添加消息编码器,将String转换为字节
                        .addLast(new StringEncoder(){
                            @Override
                            protected void encode(ChannelHandlerContext ctx, CharSequence msg, List<Object> out) throws Exception {
                                super.encode(ctx, msg + "\n", out);
                            }
                        })
                        //业务逻辑处理Handler
                        .addLast(new ChannelDuplexHandler() {

							...
							
                        });

            }
        });
	}
}

调整TlsHandler,支持mtls场景下打印对端的证书信息

在TlsHandler中添加一个名为mtls的boolean类型成员变量,通过这个成员变量判断是否使用mtls,如果是则打印对端的证书信息,否则在client打印服务端的证书信息。
代码如下:

import io.netty.channel.Channel;
import io.netty.channel.ChannelDuplexHandler;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.socket.SocketChannel;
import io.netty.handler.ssl.SslHandler;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import lombok.extern.slf4j.Slf4j;

import javax.net.ssl.SSLSession;
import javax.security.cert.X509Certificate;
import java.text.SimpleDateFormat;
import java.util.Date;



@Slf4j
public class TlsHandler extends ChannelDuplexHandler {

    private boolean serverSide;
    private boolean mtls;

    public TlsHandler(boolean serverSide, boolean mtls) {
        this.serverSide = serverSide;
        this.mtls = mtls;
    }

    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        ctx.pipeline().get(SslHandler.class).handshakeFuture().addListener(
                new GenericFutureListener<Future<Channel>>() {
                    @Override
                    public void operationComplete(Future<Channel> future) throws Exception {
                        if (future.isSuccess()) {
                            log.info("[{}] {} 握手成功", getSideType(), ctx.channel().remoteAddress());
                            SSLSession ss = ctx.pipeline().get(SslHandler.class).engine().getSession();
                            log.info("[{}] {} cipherSuite: {}", getSideType(), ctx.channel().remoteAddress(), ss.getCipherSuite());
                            if (mtls || !serverSide) {
                                X509Certificate cert = ss.getPeerCertificateChain()[0];
                                String info = null;
                                // 获得证书版本
                                info = String.valueOf(cert.getVersion());
                                System.out.println("证书版本:" + info);
                                // 获得证书序列号
                                info = cert.getSerialNumber().toString(16);
                                System.out.println("证书序列号:" + info);
                                // 获得证书有效期
                                Date beforedate = cert.getNotBefore();
                                info = new SimpleDateFormat("yyyy/MM/dd").format(beforedate);
                                System.out.println("证书生效日期:" + info);
                                Date afterdate = (Date) cert.getNotAfter();
                                info = new SimpleDateFormat("yyyy/MM/dd").format(afterdate);
                                System.out.println("证书失效日期:" + info);
                                // 获得证书主体信息
                                info = cert.getSubjectDN().getName();
                                System.out.println("证书拥有者:" + info);
                                // 获得证书颁发者信息
                                info = cert.getIssuerDN().getName();
                                System.out.println("证书颁发者:" + info);
                                // 获得证书签名算法名称
                                info = cert.getSigAlgName();
                                System.out.println("证书签名算法:" + info);
                            }
                        } else {
                            log.warn("[{}] {} 握手失败,关闭连接", getSideType(), ctx.channel().remoteAddress());
                            ctx.channel().closeFuture().addListener(closeFuture -> {
                                log.info("[{}] {} 关闭连接:{}", getSideType(), ctx.channel().remoteAddress(), closeFuture.isSuccess());
                            });
                        }
                    }
                });

        SocketChannel channel = (SocketChannel) ctx.channel();
        System.out.println(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date()) + " conn:");
        System.out.println("IP:" + channel.localAddress().getHostString());
        System.out.println("Port:" + channel.localAddress().getPort());
    }


    private String getSideType() {
        return serverSide ? "SERVER" : "CLIENT";
    }
}

创建Main类进行测试

测试Main Class:

import javax.net.ssl.SSLException;
import java.security.cert.CertificateException;
import java.util.Scanner;

public class NettyMTlsMain {

    public static void main(String[] args) throws CertificateException, SSLException {
        String serverHost = "localhost";
        int serverPort = 10001;

        NettyTLSServer server = new NettyTLSServer(serverHost, serverPort);
        server.bind(false);


        NettyTLSClient client = new NettyTLSClient(serverHost, serverPort);
        client.connect().addListener(future -> {
            if (future.isSuccess()) {
                client.getChannel().writeAndFlush("--test--");
            }
        });

        Scanner scanner = new Scanner(System.in);

        while (true) {
            System.out.println("waiting input");
            String line = scanner.nextLine();
            if ("exit".equals(line) || "eq".equals(line) || "quit".equals(line)) {
                client.shutdown();
                server.shutdown();
                return;
            }
            client.getChannel().writeAndFlush(line);
        }
    }
}

参考

netty实现TLS/SSL双向加密认证
Netty+OpenSSL TCP双向认证证书配置
基于Netty的MQTT Server实现并支持SSL
Netty tls验证
netty使用ssl双向认证
netty中实现双向认证的SSL连接
记一次TrustAnchor with subject异常解决
SpringBoot (WebFlux Netty) 支持动态更换https证书
手动实现CA数字认证(java)
java编程方式生成CA证书
netty https有什么方式根据域名设置证书?

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1483964.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Linux高负载排查最佳实践

在Linux系统中&#xff0c;经常会因为负载过高导致各种性能问题。那么如何进行排查&#xff0c;其实是有迹可循&#xff0c;而且模式固定。 本次就来分享一下&#xff0c;CPU占用过高、磁盘IO占用过高的排查方法。 还是那句话&#xff0c;以最佳实践入手&#xff0c;真传一句话…

mysql 常用命令练习

管理表格从表中查询数据从多个表查询修改数据sql变量类型 管理表格 创建一个包含三列的新表 CREATE TABLE products (id INT,name VARCHAR(255) NOT NULL,price INT DEFAULT 0,PRIMARY KEY(id) // 自增 ); 从数据库中删除表 DROP TABLE product; 向表中添加新列 ALTER TAB…

使用Xftp连接CentOS 7进行文件的传输

一、查看虚拟机IP地址 在虚拟机中打开终端输入 ifconfig &#xff1a; 我的虚拟机IP为192.168.23.131 二、打开XFtp 7连接虚拟机 其余设置为默认&#xff0c;点击连接后输入你的用户名和密码&#xff1a; 输入密码后弹出如下界面表示连接成功~ 三、传输文件 传输文件只需用鼠…

【Linux杂货铺】调试工具gdb的使用

目录 &#x1f308;前言&#x1f308; &#x1f4c1;背景介绍 &#x1f4c1; 使用 list [行号] / [函数名] run/r break/b [行号] / [函数名] info break disable break enable break delete break [断点编号] next/n step/s continue/c finish print/p [变量…

Leetcode438. 找到字符串中所有字母异位词 -hot100

题目&#xff1a; 代码(首刷看解析 2024年3月2日&#xff09;&#xff1a; 感觉自己这个ac率根本不可能找得到实习 class Solution { public:vector<int> findAnagrams(string s, string p) {int plen p.size(), slen s.size();if (slen < plen) return {};vector…

LeetCode 热题 100 | 图论(一)

目录 1 200. 岛屿数量 2 994. 腐烂的橘子 2.1 智障遍历法 2.2 仿层序遍历法 菜鸟做题&#xff0c;语言是 C 1 200. 岛屿数量 解题思路&#xff1a; 遍历二维数组&#xff0c;寻找 “1”&#xff08;若找到则岛屿数量 1&#xff09;寻找与当前 “1” 直接或间接连接在…

sqlserver保存微信Emoji表情

首先将数据库字段&#xff0c;设置类型为 nvarchar(200)一个emoji表情&#xff0c;占4字节就可以了&#xff0c;web前端展示不用改任何东西&#xff0c;直接提交数据保存&#xff1b;回显也会没有问题&#xff0c;C#代码不用做任何处理&#xff1b; 不哭不闹要睡觉&#x1f31…

基于springboot+vue的抗疫物资管理系统(前后端分离)

博主主页&#xff1a;猫头鹰源码 博主简介&#xff1a;Java领域优质创作者、CSDN博客专家、阿里云专家博主、公司架构师、全网粉丝5万、专注Java技术领域和毕业设计项目实战&#xff0c;欢迎高校老师\讲师\同行交流合作 ​主要内容&#xff1a;毕业设计(Javaweb项目|小程序|Pyt…

2023人机交互期末复习

考试题型及分值分布 1、选择题&#xff08;10题、20分&#xff09; 2、填空题&#xff08;10题、20分&#xff09; 3、判断题&#xff08;可选、5题、10分&#xff09; 4、解答题&#xff08;5~6题、30分&#xff09; 5、分析计算题&#xff08;1~2题、20分&#xff09; 注意&…

maven的私服

什么是maven的私服就是把自己写的工具类共享给别人这样大家都能用到你写的工具类不用重复写提示效率 maven的上传与下载示意图 1.什么是发行版本&#xff1f;发行版本指定的是功能稳定可以共大家使用的版本 2.什么是快照版本&#xff1f;快照版本指定的是指正在开发的版本 3…

[计算机网络]--五种IO模型和select

前言 作者&#xff1a;小蜗牛向前冲 名言&#xff1a;我可以接受失败&#xff0c;但我不能接受放弃 如果觉的博主的文章还不错的话&#xff0c;还请点赞&#xff0c;收藏&#xff0c;关注&#x1f440;支持博主。如果发现有问题的地方欢迎❀大家在评论区指正 目录 一、五种IO…

.idea文件详解

.idea文件的作用&#xff1a; .idea文件夹是存储IntelliJ IDEA项目的配置信息&#xff0c;主要内容有IntelliJ IDEA项目本身的一些编译配置、文件编码信息、jar包的数据源和相关的插件配置信息。一般用git做版本控制的时候会把.idea文件夹排除&#xff0c;因为这个文件下保存的…

qt+opencv 获取图像灰度值并以QTableView展现

思路如下&#xff1a; 先用opencv方法打开并以灰度图像的方式读取一张图片&#xff0c;然后获取整张图所有像素点的灰度值&#xff0c;将这些值存入容器中。然后因为图像为8192*4096的尺寸&#xff0c;像素点灰度值数据量较大。因此采用QTableView加自定义QAbstractTableModel的…

测试需求平台8-Arco组件实现产品增改需求

✍此系列为整理分享已完结入门搭建《TPM提测平台》系列的迭代版&#xff0c;拥抱Vue3.0将前端框架替换成字节最新开源的arco.design&#xff0c;其中约60%重构和20%新增内容&#xff0c;定位为从 0-1手把手实现简单的测试平台开发教程&#xff0c;内容将囊括基础、扩展和实战&a…

栈(顺序栈)实现Language C

###王道考研的学习领悟&#xff0c;个人喜好讲解清晰 何为栈&#xff1f; 定义:栈&#xff08;stack&#xff09;是只允许在一端进行插入或删除的线性表。 其重要术语&#xff1a;栈顶&#xff0c;栈底&#xff0c;空栈。 我们只需要把这个图看明白了&#xff0c;理解起来就…

nest.js使用nest-winston日志一

nest-winston文档 nest-winston - npm 参考&#xff1a;nestjs中winston日志模块使用 - 浮的blog - SegmentFault 思否 安装 cnpm install --save nest-winston winstoncnpm install winston-daily-rotate-file 在main.ts中 import { NestFactory } from nestjs/core; im…

JVM类加载机制以及双亲委派模型的介绍

目录 1.类加载介绍 2.具体步骤 2.1加载 2.2验证 2.3准备 2.4解析 2.5初始化 3.加载过程中的策略-双亲委派模型 1.类加载介绍 类加载,指的是Java进程在运行的时候,把.class文件从硬盘读取到内存,并进行一系列校验解析的过程. .class文件>类对象.硬盘>内村 类加载…

技术栈选型的时候,ruby、go、java、vue、react应该怎么选择?

选择适合项目需求、团队技术背景和偏好、开发速度、性能要求以及可扩展性的技术栈和框架是一个综合考虑的过程&#xff0c;没有一种通用的最佳选择&#xff0c;取决于具体情况。 选择Vue.js或React应该综合考虑项目的需求、团队的技术背景和偏好、生态系统的支持和发展趋势等因…

【Java】面向对象之多态超级详解!!

文章目录 前言一、多态1.1 多态的概念1.2 多态的实现条件1.3 重写1.3.1方法重写的规则1.3.2重写和重载的区别 1.4 向上转型和向下转型1.4.1向上转型1.4.2向下转型 1.5 多态的优缺点1.5.1 使用多态的好处1.5.2 使用多态的缺陷 结语 前言 为了深入了解JAVA的面向对象的特性&…

web开发:如何用Echarts来自动给网页设计各种统计图

很多时候web开发也会需要用到统计图&#xff0c;如果单纯靠我们自己那点拙劣的css和js水平设计的话&#xff0c;又耗时间又做得跟史一样&#xff0c;这时候就需要引入别人设计师为我们设计好的动态统计图——echarts Echarts的官网是&#xff1a;Apache ECharts 1、第一步&…