創建證書
不管是單向tls還是雙向tls(mTLS),都需要創建證書。
創建證書可以使用openssl或者keytool,openssl 參考 mTLS: openssl創建CA證書
單向/雙向tls需要使用到的相關文件:
文件 | 單向tls | 雙向tls | Server端 | 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有什么方式根據域名設置證書?