创建证书
不管是单向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有什么方式根据域名设置证书?