mTSL: netty单向/双向TLS连接

创建证书

不管是单向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>() {@Overrideprotected 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() {@Overridepublic void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {log.info("received message from client: {}", msg);ctx.writeAndFlush("server response: " + msg);}@Overridepublic 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>() {@Overrideprotected 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() {@Overridepublic 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.crtSslContext 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对象的childHandlerbootstrap.childHandler(new ChannelInitializer<SocketChannel>() {// SocketChannel 初始化方法,该方法在Channel注册后只会被调用一次@Overrideprotected 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(){@Overrideprotected 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.crtSslContext 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>() {@Overrideprotected 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(){@Overrideprotected 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;}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ctx.pipeline().get(SslHandler.class).handshakeFuture().addListener(new GenericFutureListener<Future<Channel>>() {@Overridepublic 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对象的childHandlerbootstrap.childHandler(new ChannelInitializer<SocketChannel>() {// SocketChannel 初始化方法,该方法在Channel注册后只会被调用一次@Overrideprotected 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(){@Overrideprotected 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>() {@Overrideprotected 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(){@Overrideprotected 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;}@Overridepublic void channelActive(ChannelHandlerContext ctx) throws Exception {ctx.pipeline().get(SslHandler.class).handshakeFuture().addListener(new GenericFutureListener<Future<Channel>>() {@Overridepublic 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.rhkb.cn/news/267745.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Visual Studio C++项目远程断点调试客户现场程序方法

前言 程序开发一个很常见的场景&#xff0c;就是程序在自己本地部署调试明明一点问题都没有&#xff0c;但是部署到客户现场就问题百出&#xff0c;要调试起来还很困难&#xff0c;在自己本地也没有条件复现&#xff0c;很多时候只能靠日志一点点排查和猜测&#xff0c;耗费大…

[设计模式Java实现附plantuml源码~行为型] 对象状态及其转换——状态模式

前言&#xff1a; 为什么之前写过Golang 版的设计模式&#xff0c;还在重新写Java 版&#xff1f; 答&#xff1a;因为对于我而言&#xff0c;当然也希望对正在学习的大伙有帮助。Java作为一门纯面向对象的语言&#xff0c;更适合用于学习设计模式。 为什么类图要附上uml 因为很…

SparkStreaming在实时处理的两个场景示例

简介 Spark Streaming是Apache Spark生态系统中的一个组件&#xff0c;用于实时流式数据处理。它提供了类似于Spark的API&#xff0c;使开发者可以使用相似的编程模型来处理实时数据流。 Spark Streaming的工作原理是将连续的数据流划分成小的批次&#xff0c;并将每个批次作…

(C语言)函数详解上

&#xff08;C语言&#xff09;函数详解上 目录&#xff1a; 1. 函数的概念 2. 库函数 2.1 标准库和头文件 2.2 库函数的使用方法 2.2.1 sqrt 功能 2.2.2 头文件包含 2.2.3 实践 2.2.4 库函数文档的一般格式 3. 自定义函数 3.1 函数的语法形式 3.2 函数的举例 4. 形参和实参 4.…

Redis 命令全解析之 List类型

文章目录 命令RedisTemplate API使用场景 Redis 的 List 是一种有序、可重复、可变动的数据结构&#xff0c;它基于双向链表实现。在Redis中&#xff0c;List可以存储多个相同或不同类型的元素&#xff0c;每个元素在List中都有一个对应的索引位置。这使得List可以用来实现队列…

【计算机网络_应用层】协议定制序列化反序列化

文章目录 1. TCP协议的通信流程2. 应用层协议定制3. 通过“网络计算器”的实现来实现应用层协议定制和序列化3.1 protocol3.2 序列化和反序列化3.2.1 手写序列化和反序列化3.2.2 使用Json库 3.3 数据包读取3.4 服务端设计3.5 最后的源代码和运行结果 1. TCP协议的通信流程 在之…

oppo手机备忘录记录怎么转移到华为手机?

oppo手机备忘录记录怎么转移到华为手机?使用oppo手机已经有三四年了&#xff0c;因为平时习惯&#xff0c;在手机系统的备忘录中记录了很多重要的笔记&#xff0c;比如工作会议的要点、读书笔记、购物清单、朋友的生日提醒等。这些记录对我来说非常重要&#xff0c;我可以通过…

ElasticSearch搜索引擎使用指南

一、ES数据基础类型 1、数据类型 字符串 主要包括: text和keyword两种类型&#xff0c;keyword代表精确值不会参与分词&#xff0c;text类型的字符串会参与分词处理 数值 包括: long, integer, short, byte, double, float 布尔值 boolean 时间 date 数组 数组类型不…

独立游戏《星尘异变》UE5 C++程序开发日志1——项目与代码管理

写在前面&#xff1a;本日志系列将会向大家介绍在《星尘异变》这款模拟经营游戏&#xff0c;在开发时用到的与C相关的泛用代码与算法&#xff0c;主要记录UE5C与原生C的用法区别&#xff0c;以及遇到的问题和解决办法&#xff0c;因为这是我本人从ACM退役以后第一个从头开始的项…

【数据结构】知识点一:线性表之顺序表

内容导航 一、什么是线性表&#xff1f;二、什么是顺序表&#xff1f;1、顺序表的概念2、顺序表的结构a. 静态顺序表&#xff1a;使用定长数组存储元素。b. 动态顺序表&#xff1a;使用动态开辟的数组存储。 三、顺序表的接口实现精讲1.接口一&#xff1a;打印数据2.接口二&…

持安科技亮相张江高科895创业营,总评分第三名荣获「最具创新性企业」!

近日&#xff0c;张江高科895创业营&#xff08;第十三季&#xff09;信息安全专场Demo day&结营仪式在上海集成电路设计产业园圆满落幕。本季创业营通过多种渠道在海内外甄选优秀创业项目&#xff0c;一共择优录取了29家入营&#xff0c;最终甄选出9家代表参加Demo day路演…

Django后端开发——中间件

文章目录 参考资料中间件注册中间件settings.pymiddleware/mymiddleware.pymysite3/views.pymysite3/urls.py 练习 参考资料 B站网课&#xff1a;点击蓝色字体跳转 或复制链接至浏览器&#xff1a;https://www.bilibili.com/video/BV1vK4y1o7jH?p39&vd_source597e21cf34f…

数据可视化原理-腾讯-热力图

在做数据分析类的产品功能设计时&#xff0c;经常用到可视化方式&#xff0c;挖掘数据价值&#xff0c;表达数据的内在规律与特征展示给客户。 可是作为一个产品经理&#xff0c;&#xff08;1&#xff09;如果不能够掌握各类可视化图形的含义&#xff0c;就不知道哪类数据该用…

Mybatis plus扩展功能-Db静态工具

目录 1 前言 2 使用方法 2.1 Db静态工具拥有的部分方法 2.2 举例 1 前言 在我们的服务层中&#xff0c;有时为了实现一个方法需要引入其它的Mapper层方法&#xff0c;但是&#xff0c;这样可能出现循环依赖。虽然Spring已经给我们解决了简单的循环依赖问题&#xff0c;但是…

深入剖析k8s-Pod篇

为什么需要Pod&#xff1f; 进程是以进程组的方式组织在一起。受限制容器的“单进程模型”&#xff0c; 成组调用没有被妥善处理&#xff08;资源调用有限&#xff09;&#xff0c;使用资源囤积则导致复杂度上升。 在k8s项目中&#xff0c;Pod的实现需要使用一个中间容器——…

web组态(BY组态)接入流程

技术文档 官网网站&#xff1a;www.hcy-soft.com 体验地址&#xff1a; www.byzt.net:60/sm 一、数据流向图及嵌入原理 数据流向 嵌入原理 二、编辑器调用业务流程图 三、集成前需要了解的 1、后台Websocket端往前台监控画面端传输数据规则 后台websocket向客户端监控画面…

基于CNN-LSTM-Attention的时间序列回归预测matlab仿真

目录 1.算法运行效果图预览 2.算法运行软件版本 3.部分核心程序 4.算法理论概述 4.1卷积神经网络&#xff08;CNN&#xff09;在时间序列中的应用 4.2 长短时记忆网络&#xff08;LSTM&#xff09;处理序列依赖关系 4.3 注意力机制&#xff08;Attention&#xff09; 5…

python自动化之项目架构搭建与思路讲解(第二天)

1.自动化测试的概念 自动化测试是指使用自动化工具和脚本来执行测试任务&#xff0c;以验证软件或系统的正确性和稳定性。它可以提高测试的效率和准确性&#xff0c;并节约时间和成本。 2.自动化脚本编写的思路 xmind文档如有需要&#xff0c;可在资源里自行下载 3.项目代码…

MySQL NDB Cluster 分布式架构搭建 自定义启动与重启Shell脚本

此次NDB Cluster使用三台虚拟机进行搭建&#xff0c;一台作为管理节点&#xff1b;而对于另外两台服务器&#xff0c;每一台都充当着数据节点和SQL节点的角色。注意不是MGR主从复制架构&#xff0c;而是分布式MySQL架构。 创建 /var/lib/mysql-cluster/config.ini Cluster全局…

(面试题)数据结构:链表相交

问题&#xff1a;有两个链表&#xff0c;如何判断是否相交&#xff0c;若相交&#xff0c;找出相交的起始节点 一、介绍 链表相交&#xff1a; 若两个链表相交&#xff0c;则两个链表有共同的节点&#xff0c;那从这个节点之后&#xff0c;后面的节点都会重叠&#xff0c;知道…