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.pswp.cn/news/715567.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/715567.shtml
英文地址,請注明出處:http://en.pswp.cn/news/715567.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

MySQL知識點歸納總結(二)

10、MVCC實現原理&#xff1f; 事務ID&#xff08;Transaction ID&#xff09;&#xff1a;每個事務在執行時都會被分配一個唯一的事務ID&#xff0c;用于標識該事務的開始時間順序。事務ID是一個遞增的整數&#xff0c;隨著每個新事務的開始而遞增。 Undo日志&#xff08;Un…

【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 在樹中有一條邊。 請你返回樹中的 合法路…

Nginx 反向代理入門教程

Nginx 反向代理入門教程 一、什么是反向代理 反向代理&#xff08;Reverse Proxy&#xff09;方式是指以代理服務器來接受Internet上的連接請求&#xff0c;然后將請求轉發給內部網絡上的服務器&#xff1b;并將從服務器上得到的結果返回給Internet上請求連接的客戶端&#x…

Vue 2.0 與 Vue 3.0 的主要差異

Vue 2.0 與 Vue 3.0 的主要差異 在前端框架的世界中&#xff0c;Vue.js 已經成為了一股不可忽視的力量。自從 Vue.js 首次亮相以來&#xff0c;它便以其輕量級、靈活性和易用性贏得了開發者的喜愛。然而&#xff0c;隨著技術的不斷進步和開發者需求的不斷變化&#xff0c;Vue.…

Android AppCompatActivity 方法詳解

在 Android 開發中&#xff0c;AppCompatActivity 是一個常用的類&#xff0c;它提供了對新版 Android 特性在舊版 Android 上的兼容支持。作為 Android 支持庫的一部分&#xff0c;它通常被用作活動&#xff08;Activity&#xff09;的基類。下面我們將介紹 AppCompatActivity…