创建证书
不管是单向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>() {
@Override
protected 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() {
@Override
public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
log.info("received message from client: {}", msg);
ctx.writeAndFlush("server response: " + msg);
}
@Override
public 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>() {
@Override
protected 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() {
@Override
public 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.crt
SslContext 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对象的childHandler
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
// SocketChannel 初始化方法,该方法在Channel注册后只会被调用一次
@Override
protected 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(){
@Override
protected 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.crt
SslContext 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>() {
@Override
protected 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(){
@Override
protected 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;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.pipeline()
.get(SslHandler.class)
.handshakeFuture()
.addListener(
new GenericFutureListener<Future<Channel>>() {
@Override
public 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对象的childHandler
bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
// SocketChannel 初始化方法,该方法在Channel注册后只会被调用一次
@Override
protected 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(){
@Override
protected 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>() {
@Override
protected 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(){
@Override
protected 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;
}
@Override
public void channelActive(ChannelHandlerContext ctx) throws Exception {
ctx.pipeline().get(SslHandler.class).handshakeFuture().addListener(
new GenericFutureListener<Future<Channel>>() {
@Override
public 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有什么方式根据域名设置证书?