作为一个高性能的NIO通讯框架,基于Netty的行业应用非常广泛,面临的安全挑战也不同。
下面分析Netty面临的安全挑战。
应用场景一:目前高性能的NIO框架成为RPC的分布式服务框架的基石,内部的各个模块需要进行高性能通信,各模块之间往往采用长链接通信,通过心跳检测保证链路的可靠性。由于RPC框架通常是在内部各模块之间使用,运行在授信的内部安全域中,不直接对外开放接口。因此不需要做握手、黑白名单、SSL/TLS等。
在这种应用场景下,Netty的安全性是依托企业的防火墙、安全加固操作系统等系统级别安全来保障,它本身并不需要做额外的安全性保护工作。
应用场景二:对于第三方开放的通信框架。如果使用Netty做RPC框架或者私有协议栈,RPC框架面向非授信的第三方开放,例如将内部的一些功能通过服务对外开放出去,此时需要进行安全认证,如果开放的是公网IP,对于安全性要求非常高的服务,例如在线支付、订购等,需要通过SSL/TLS进行通信。原理图如下:
对第三方开放通信框架的接口调用存在三种场景:
1. 在企业内网,开放给内部其它模块调用的服务,通常不需要进行安全认证和SSL/TLS传输;
2. 在企业内网,被外部其它模块调用的服务,往往需要利用IP黑名单、握手登录等方式进行安全认证,认证通过后双方使用普通的Socket进行通信,如果认证失败,则拒绝客户端连接;
3. 开放给企业外部第三方应用访问的服务,往往需要监听公网IP(通常是防火墙的IP地址),由于对第三方服务调用者的监管存在诸多困难,或者无法有效监听,这些第三方应用实际是非授信的,为了有效应对安全风险,对于敏感的服务往往通过SSL/TSL进行安全传输。
应用场景三:应用层协议的安全性,作为高性能、异步事件驱动的NIO框架,Netty非常适合构建上层的应用层协议。由于绝大多数应用层协议都是公有的,这意味着底层的Netty需要向上层提供通信层的安全传输,也就是需要支持SSL/TLS。
Netty通过SslHandler提供了对SSL的支持,它支持的SSL协议类型包括:SSL V2、SSL V3和TLS
单向认证:客户端只验证服务端的合法性,服务端不验证客户端。
对应keytool的使用及命令说明可参考JDK自带工具keytool生成ssl证书搭建tomcat+https协议
(1)生成Netty服务器私钥和证书仓库:
keytool -genkey -alias securechat -keysize 2048 -validity 365 -keyalg RSA -dname "CN=localhost" -keypass sNetty -storepass sNetty -keystore sChat.jks
(2)生成Netty服务端自签名证书:
keytool -genkey -alias smcc -keysize 2048 -validity 365 -keyalg RSA -dname "CN=localhost" -keypass cNetty -storepass cNetty -keystore cChat.jks
(3)生成客户端的密钥对和证书仓库,用于服务端的证书保存到客户端的授信证书仓库中:
keytool -export -alias securechat -keystore sChat.jks -storepass sNetty -file sChat.cer
(4)将Netty服务端的证书导入到客户端的证书仓库中:
keytool -import -trustcacerts -alias securechat -file sChat.cer -storepass cNetty -keystore cChat.jks
(1)因为是客户端认证服务端,因此服务端需要设置和加载私钥仓库KeyStore,创建服务端的上下文(SSLContext)
//密钥管理器
KeyManagerFactory kmf = null;
if(pkPath!=null){
//密钥库KeyStore
KeyStore ks = KeyStore.getInstance("JKS");
//加载客户端证书
in = new FileInputStream(pkPath);
//加载服务端的KeyStore ;sNetty是生成仓库时设置的密码,用于检查密钥库完整性的密码
ks.load(in, "sNetty".toCharArray());
kmf = KeyManagerFactory.getInstance("SunX509");
//初始化密钥管理器
kmf.init(ks, "sNetty".toCharArray());
}
//获取安全套接字协议(TLS协议)的对象
SERVER_CONTEXT= SSLContext.getInstance(PROTOCOL);
//初始化此上下文
//参数一:认证的密钥 参数二:对等信任认证 参数三:伪随机数生成器 。 由于单向认证,服务端不用验证客户端,所以第二个参数为null
SERVER_CONTEXT.init(kmf.getKeyManagers(), null, null);
(2)服务端的SSLContext创建完成后,利用SSLContext创建SSL引擎SSLEngine,设置SSLEngine为服务器模式。由于不需要对客户端进行认证,因此NeedClientAuth不需要额外设置,使用默认值false。
engine.setUseClientMode(false);//设置为服务器模式
//engine.setNeedClientAuth(false);//不需要客户端认证,默认为false,故不需要写这行。
(3)把SslHandler添加到管道中
pipeline.addLast("ssl", new SslHandler(engine));
(1)客户端和服务端类似,由于是客户端认证服务端,因此,客户端只需要加载存放服务端CA的证书仓库即可。
加载证书仓库完成后,初始化SSLContext,设置信任证书TrustManager。
//信任库
TrustManagerFactory tf = null;
if (caPath != null) {
//密钥库KeyStore
KeyStore tks = KeyStore.getInstance("JKS");
//加载客户端证书
tIN = new FileInputStream(caPath);
tks.load(tIN, "cNetty".toCharArray());
tf = TrustManagerFactory.getInstance("SunX509");
// 初始化信任库
tf.init(tks);
}
CLIENT_CONTEXT = SSLContext.getInstance(PROTOCOL);
//设置信任证书
CLIENT_CONTEXT.init(null,tf == null ? null : tf.getTrustManagers(), null);
(2)客户端的SSLContext创建完成后,利用SSLContext创建SSL引擎SSLEngine,设置SSLEngine为客户方模式。
engine.setUseClientMode(true);//客户方模式
(3)把SslHandler添加到管道中
pipeline.addLast("ssl", new SslHandler(engine));
分别运行服务端、客户端即可。
与单向认证不同的是服务端也需要对客户端进行安全认证,这就意味着客户端的自签名证书
也需要导入到服务器的数组证书仓库中。
前面的生成和单向一样,然后继续
(1)生成客户端的自签名证书:
keytool -export -alias smcc -keystore cChat.jks -storepass cNetty -file cChat.cer
(2)将客户端的自签名证书导入到服务端的信任证书仓库
keytool -import -trustcacerts -alias smcc -file cChat.cer -storepass sNetty -keystore sChat.jks
(1)由于服务端需要对客户端进行验证,所以在初始化服务端SSLContext的时候需要加载证书仓库。
//信任库
TrustManagerFactory tf = null;
if (caPath != null) {
KeyStore tks = KeyStore.getInstance("JKS");
tIN = new FileInputStream(caPath);
tks.load(tIN, "sNetty".toCharArray());
tf = TrustManagerFactory.getInstance("SunX509");
tf.init(tks);
}
SERVER_CONTEXT= SSLContext.getInstance(PROTOCOL);
//初始化此上下文
//参数一:认证的密钥 参数二:对等信任认证 参数三:伪随机数生成器 。 由于单向认证,服务端不用验证客户端,所以第二个参数为null
SERVER_CONTEXT.init(kmf.getKeyManagers(),tf.getTrustManagers(), null);
(2)、创建SSLEngine后,设置客户端认证
engine.setUseClientMode(false);//设置服务端模式
engine.setNeedClientAuth(true);//需要客户端验证
(1)由于服务端需要认证客户端的证书,因此,需要初始化和加载私钥仓库,向服务端发送公钥。初始化KeyStore的代码如下:
KeyManagerFactory kmf = null;
if (pkPath != null) {
KeyStore ks = KeyStore.getInstance("JKS");
in = new FileInputStream(pkPath);
ks.load(in, "cNetty".toCharArray());
kmf = KeyManagerFactory.getInstance("SunX509");
kmf.init(ks, "cNetty".toCharArray());
}
SERVER_CONTEXT= SSLContext.getInstance(PROTOCOL);
//初始化此上下文
//参数一:认证的密钥 参数二:对等信任认证 参数三:伪随机数生成器 。 由于单向认证,服务端不用验证客户端,所以第二个参数为null
CLIENT_CONTEXT.init(kmf.getKeyManagers(),tf.getTrustManagers(), null);
除了以上得代码,双向认证的其余代码与单向基本一样。
10、SSL双向认证相比单向认证,多了一步服务端发送认证请求消息给客户端,客户端发送自签名证书给服务端进行安全认证的过程。
1、单向认证完整代码
2、双向认证完整代码