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.mzph.cn/news/715567.shtml

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

相关文章

【Web安全靶场】sqli-labs-master 38-53 Stacked-Injections

sqli-labs-master 38-53 Stacked-Injections 其他关卡和靶场看专栏… 文章目录 sqli-labs-master 38-53 Stacked-Injections第三十八关-报错注入第三十九关-报错注入第四十关-盲注第四十一关-盲注第四十二关-联合报错双查询注入第四十三关-报错注入第四十四关-盲注第四十五关-…

「爬虫职海录」三镇爬虫

HI&#xff0c;朋友们好 「爬虫职海录」第三期更新啦&#xff01; 本栏目的内容方向会以爬虫相关的“岗位分析”和“职场访谈”为主&#xff0c;方便大家了解一下当下的市场行情。 本栏目持续更新&#xff0c;暂定收集国内主要城市的爬虫岗位相关招聘信息&#xff0c;有求职…

【高级数据结构】Trie树

原理 介绍 高效地存储和查询字符串的数据结构。所以其重点在于&#xff1a;存储、查询两个操作。 存储操作 示例和图片来自&#xff1a;https://blog.csdn.net/qq_42024195/article/details/88364485 假设有这么几个字符串&#xff1a;b&#xff0c;abc&#xff0c;abd&…

Vue中如何实现条件渲染?

在Vue中实现条件渲染非常简单且灵活&#xff0c;主要通过Vue的指令来实现。在Vue中&#xff0c;我们可以使用v-if和v-else指令来根据条件来渲染不同的内容。下面就让我们通过一个简单的示例来演示如何在Vue中实现条件渲染&#xff1a; <!DOCTYPE html> <html lang&qu…

GO泛型相关

通过引入 类型形参 和 类型实参 这两个概念&#xff0c;我们让一个函数获得了处理多种不同类型数据的能力&#xff0c;这种编程方式被称为 泛型编程。 2. Go的泛型 类型形参 (Type parameter)类型实参(Type argument)类型形参列表( Type parameter list)类型约束(Type constr…

Pake 轻松构建轻量级多端桌面应用

Pake 利用 Rust 轻松构建轻量级多端桌面应用&#xff0c;支持 Mac / Windows / Linux。 小白用户&#xff1a;可以使用 「常用包下载」 方式来体验 Pake 的能力&#xff0c;也可试试 Action 方式。 开发用户&#xff1a;可以使用 「命令行一键打包」&#xff0c;对 Mac 比较友…

Matlab 机器人工具箱 动力学

文章目录 R.dynR.fdynR.accelR.rneR.gravloadR.inertiaR.coriolisR.payload官网:Robotics Toolbox - Peter Corke R.dyn 查看动力学参数 mdl_puma560; p560.dyn;%查看puma560机械臂所有连杆的动力学参数 p560.dyn(2);%查看puma560机械臂第二连杆的动力学参数 p560.links(2)…

react父子组件传参demo

父组件代码 /* eslint-disable next/next/no-img-element */ "use client"; import React, { useEffect, useState } from "react"; import WxTip from ../components/WxTipconst Download () > {const [showTip, setshowTip] useState<boolean…

javaweb day9 day10

昨天序号标错了 vue的组件库Elent 快速入门 写法 常见组件 复制粘贴 打包部署

高斯消元法解线性方程组

高斯消元法 基本性质&#xff1a; 把某一行乘一个非 0 0 0的数 (方程的两边同时乘上一个非 0 0 0数不改变方程的解) 交换某两行 (交换两个方程的位置) 把某行的若干倍加到另一行上去 &#xff08;把一个方程的若干倍加到另一个方程上去&#xff09; 算法步骤 枚举每一列c …

洛谷p1225 c++(使用高精度)

题解: 一开始我这个代码想到的是使用递归来求解 int digui(int n){int sum=0;if(n==1)sum=1;if(n==2)sum=2;if(n==1||n==2)return sum;if(n>2){return sum+=digui(n-1)+digui(n-2);} } 但是后面发现明显超时,我试图用记忆化搜索来抢救一下,所以就有了下面代码 int di…

图论 - DFS深度优先遍历、BFS广度优先遍历、拓扑排序

文章目录 前言Part 1&#xff1a;DFS&#xff08;深度优先遍历&#xff09;一、排列数字1.题目描述输入格式输出格式数据范围输入样例输出样例 2.算法 二、n皇后问题1.问题描述输入格式输出格式数据范围输入样例输出样例 2.算法 三、树的重心1.问题描述输入格式输出格式数据范围…

计算机二级Python刷题笔记------基本操作题23、33、35、37(考察字符串)

文章目录 第二十三题&#xff08;字符串替换&#xff1a;replace(old,new)&#xff09;第三十三题&#xff08;字符串遍历&#xff09;第三十五题&#xff08;字符串与列表&#xff09;第三十七题&#xff08;拼接字符串&#xff09; 第二十三题&#xff08;字符串替换&#xf…

第19章-IPv6基础

1. IPv4的缺陷 2. IPv6的优势 3. 地址格式 3.1 格式 3.2 长度 4. 地址书写压缩 4.1 段内前导0压缩 4.2 全0段压缩 4.3 例子1 4.4 例子 5. 网段划分 5.1 前缀 5.2 接口标识符 5.3 前缀长度 5.4 地址规模分类 6. 地址分类 6.1 单播地址 6.2 组播地址 6.3 任播地址 6.4 例子 …

Redis学习------实战篇----2024/02/29----缓存穿透,雪崩,击穿

1.缓存穿透 Overridepublic Result queryById(Long id) {//1.从redis中查询缓存String key CACHE_SHOP_KEY id;String shopJson stringRedisTemplate.opsForValue().get(key);//2.判断是否存在//3.存在则直接返回if (StrUtil.isNotBlank(shopJson)){Shop shop JSONUtil.toB…

每日一题 2867统计树中的合法路径

2867. 统计树中的合法路径数目 题目描述&#xff1a; 给你一棵 n 个节点的无向树&#xff0c;节点编号为 1 到 n 。给你一个整数 n 和一个长度为 n - 1 的二维整数数组 edges &#xff0c;其中 edges[i] [ui, vi] 表示节点 ui 和 vi 在树中有一条边。 请你返回树中的 合法路…

Vins-Moon配准运行

Vins-Moon运行 源码地址电脑配置环境配置编译适配Kitti数据集运行结果Euroc数据集kitti数据集 evo评估&#xff08;KITTI数据&#xff09;输出轨迹(tum格式)结果 源码地址 源码链接&#xff1a;https://github.com/HKUST-Aerial-Robotics/VINS-Mono.git 电脑配置 Ubuntu 18.…

破解SQL Server迷局,彻底解决“管道的另一端无任何进程错误233”

问题描述&#xff1a;在使用 SQL Server 2014的时候&#xff0c;想用 SQL Server 身份方式登录 SQL Servcer Manager&#xff0c;结果报错&#xff1a; 此错误消息&#xff1a;表示SQL Server未侦听共享内存或命名管道协议。 问题原因&#xff1a;此问题的原因有多种可能 管道…

人才测评系统在企业中的作用有哪些?

一个企业除了产出价值给社会&#xff0c;它还有自己的工作架构体系&#xff0c;无论的工作时间制度上&#xff0c;还是工资组成方向&#xff0c;这样公司才能正常运转&#xff0c;那么人才测评系统可以在企业中充当一个什么角色呢&#xff1f;又或者说它起着什么作用呢&#xf…

走进SQL审计视图——《OceanBase诊断系列》之二

1. 前言 在SQL性能诊断上&#xff0c;OceanBase有一个非常实用的功能 —— SQL审计视图(gv$sql_audit)。在OceanBase 4.0.0及更高版本中&#xff0c;该功能是 gv$ob_sql_audit。它可以使开发和运维人员更方便地排查在OceanBase上运行过的任意一条SQL&#xff0c;无论这些SQL是成…