【Netty权威指南】20-安全性

1、Netty SSL安全特性

JDK的安全类库提供了 javax.net.ssl.SSLSocket和javax.net.ssl.SSLServerSocket类库用于支持SSL/TLS安全传输,对于NIO非阻塞 Socket通信,JDK并没有提供现成可用的类库简化用户开发。
Netty通过JDK的 SSLEngine,以 SslHandler的方式提供对SSL/TLS安全传输的支持,极大的简化了用户的开发工作量,降低开发难度。对于Netty默认提供的HTTP协议,Netty利用 SsIHandler,同样支持 Https协议。
Netty通过 SsIHandler提供了对SSL的支持,它支持的SSL协议类型包括:SSLV2、SSLV3和TLS。

1.1、SSL单向认证

单向认证,即客户端只验证服务端的合法性,服务端不验证客户端。下面我们通过Netty的SSL单向认证代码开发来掌握基于 Netty的SSL单向认证。

1.1.1、单向认证开发

首先,利用JDK的 keytool工具,Netty服务端依次生成服务端的密钥对和证书仓库、服务端自签名证书。
生成 Netty服务端私钥和证书仓库命令:

keytool -genkey -alias securechat -keysize 2048 -validity 365 -keyalg RSA -dname "CN=localhost" -keypass sNetty -storepass sNetty -keystore sChat.jks

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore sChat.jks -destkeystore sChat.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

生成 Netty服务端自签名证书:

keytool -export -alias securechat -keystore sChat.jks -storepass sNetty -file sChat.cer
存储在文件 <sChat.cer> 中的证书

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore sChat.jks -destkeystore sChat.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

生成客户端的密钥对和证书仓库,用于将服务端的证书保存到客户端的授信证书仓库中,命令如下:
keytool -genkey -alias smcc -keysize 2048 -validity 365 -keyalg RSA -dname "CN=localhost" -keypass cNetty -storepass cNetty -keystore cChat.jks

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore cChat.jks -destkeystore cChat.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

随后,将 Netty服务端的证书导入到客户端的证书仓库中,命令如下:

keytool -import -trustcacerts -alias securechat -file sChat.cer -storepass cNetty -keystore cChat.jks
所有者: CN=localhost
发布者: CN=localhost
序列号: 7c1ead41
有效期为 Tue May 14 16:53:36 CST 2019 至 Wed May 13 16:53:36 CST 2020
证书指纹:
     MD5:  BB:A0:14:41:D5:E5:11:71:0A:15:CE:F4:32:6A:3B:01
     SHA1: DC:C0:C9:9E:16:C1:61:18:D3:D6:92:8B:DA:2F:26:57:6C:FC:3C:1C
     SHA256: A9:FE:33:B3:27:21:42:B2:8D:A6:9E:1B:81:72:90:0E:40:A5:1A:05:85:F5:E5:B5:B0:6F:BC:69:CC:38:E7:10
签名算法名称: SHA256withRSA
主体公共密钥算法: 2048 位 RSA 密钥
版本: 3

扩展:

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 6C 57 EE A4 00 0F BA 26   E4 5A D3 8B A8 10 4D 7D  lW.....&.Z....M.
0010: A9 F8 23 FA                                        ..#.
]
]

是否信任此证书? [否]:  y
证书已添加到密钥库中

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore cChat.jks -destkeystore cChat.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

上述工作完成之后,我们就开始编写SSL服务端和客户端的代码,下面我们对核心代码进行讲解。
因为是客户端认证服务端,因此服务端需要正确的设置和加载私钥仓库 KeyStore,相关代码如下:

public static SSLContext getServerContext(String tlsMode, String pkPath,
                                          String caPath) {
    if (SERVER_CONTEXT == null) {
        InputStream in = null;
        InputStream tIN = null;
        try {
            // Set up key manager factory to use our key store
            KeyManagerFactory kmf = null;
            if (pkPath != null) {
                KeyStore ks = KeyStore.getInstance("JKS");
                in = new FileInputStream(pkPath);
                ks.load(in, "sNetty".toCharArray());
                kmf = KeyManagerFactory.getInstance("SunX509");
                kmf.init(ks, "sNetty".toCharArray());
            }
            TrustManagerFactory tf = null;
            if (caPath != null) {
                KeyStore tks = KeyStore.getInstance("JKS");
                tIN = new FileInputStream(caPath);
                tks.load(tIN, "sNetty".toCharArray());
                // tks.load(tIN, "123456".toCharArray());
                tf = TrustManagerFactory.getInstance("SunX509");
                tf.init(tks);
            }
            // Initialize the SSLContext to work with our key managers.
            SERVER_CONTEXT = SSLContext.getInstance(PROTOCOL);
            if (SSLMODE.CA.toString().equals(tlsMode))
                SERVER_CONTEXT.init(kmf.getKeyManagers(), null, null);
            else if (SSLMODE.CSA.toString().equals(tlsMode)) {
                SERVER_CONTEXT.init(kmf.getKeyManagers(),
                        tf.getTrustManagers(), null);
            } else {
                throw new Error(
                        "Failed to initialize the server-side SSLContext"
                                + tlsMode);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new Error(
                    "Failed to initialize the server-side SSLContext", e);
        } finally {
            if (in != null)
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            in = null;
            if (tIN != null)
                try {
                    tIN.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            tIN = null;
        }
    }
    return SERVER_CONTEXT;
}

由于是单向认证,服务端不需要验证客户端的合法性,因此, TrustManager为空,安全随机数不需要设置,使用JDK默认创建的即可。
服务端的 SSLContext创建完成之后,利用 SSLContext创建SSL引擎 SSLEngine,设置 SSLEngine为服务端模式,由于不需要对客户端进行认证,因此 NeedClientAuth不需要额外设置,使用默认值 False。相关代码如下:
engine.setUseClientMode(false);

SSL服务端创建完成之后,下面继续看客户端的创建,它的原理同服务端类似,也是在初始化TCP链路的时候创建并设置 SSLEngine,代码如下:

public static SSLContext getClientContext(String tlsMode, String pkPath,
                                          String caPath) {
    if (CLIENT_CONTEXT == null) {
        InputStream in = null;
        InputStream tIN = null;
        try {
            // Set up key manager factory to use our key store
            KeyManagerFactory kmf = null;
            if (pkPath != null) {
                KeyStore ks = KeyStore.getInstance("JKS");
                in = new FileInputStream(pkPath);
                ks.load(in, "cNetty".toCharArray());
                // ks.load(in, "123456".toCharArray());
                kmf = KeyManagerFactory.getInstance("SunX509");
                kmf.init(ks, "cNetty".toCharArray());
                // kmf.init(ks, "123456".toCharArray());
            }

            // Set up trust manager factory to use our key store
            // TrustManagerFactory tmf = TrustManagerFactory
            // .getInstance("SunX509");
            // tmf.init(ks);
            TrustManagerFactory tf = null;
            if (caPath != null) {
                KeyStore tks = KeyStore.getInstance("JKS");
                tIN = new FileInputStream(caPath);
                tks.load(tIN, "cNetty".toCharArray());
                tf = TrustManagerFactory.getInstance("SunX509");
                tf.init(tks);
            }
            // Initialize the SSLContext to work with our key managers.
            CLIENT_CONTEXT = SSLContext.getInstance(PROTOCOL);
            if (SSLMODE.CA.toString().equals(tlsMode))
                CLIENT_CONTEXT.init(null,
                        tf == null ? null : tf.getTrustManagers(), null);
            else if (SSLMODE.CSA.toString().equals(tlsMode)) {
                CLIENT_CONTEXT.init(kmf.getKeyManagers(),
                        tf.getTrustManagers(), null);
            } else {
                throw new Error(
                        "Failed to initialize the client-side SSLContext"
                                + tlsMode);
            }
        } catch (Exception e) {
            e.printStackTrace();
            throw new Error(
                    "Failed to initialize the client-side SSLContext", e);
        } finally {
            if (in != null)
                try {
                    in.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            in = null;
        }
    }
    return CLIENT_CONTEXT;
}

由于是客户端认证服务端,因此,客户端只需要加载存放服务端CA的证书仓库即可加载证书仓库完成之后,初始化 SSLContext,代码如下:对于客户端只需要设置信任证书 TrustManager:

客户端 SSLContext初始化完成之后,创建 SSLEngine并将其设置为客户端工作模式,代码如下:

engine.setUseClientMode(true);

将 SsIHandler添加到 pipeline中,利用 SsIHandler实现 Socket安全传输,代码如下

pipeline.addLast("ssl",new SslHandler(engine));

客户端和服务端创建完成之后,测试下SSL单向认证功能是否OK,为了査看SSL握手过程,我们打开SSL握手的调测日志, Eclipse设置如图24-3所示。

分别运行服务端和客户端,运行结果如图24-4和图24-5所示。

在客户端输入信息,服务端原样返回,测试结果如图24-6所示。

2.1.2、单向认证原理

SSL单向认证的过程总结如下:
1.SSL客户端向服务端传送客户端SSL协议的版本号、支持的加密算法种类、产生的随机数,以及其他可选信息;
2.服务端返回握手应答,向客户端传送确认SSL协议的版本号、加密算法的种类随机数以及其他相关信息;
3.服务端向客户端发送自己的公钥;
4.客户端对服务端的证书进行认证,服务端的合法性校验包括:证书是否过期、发行服务器证书的CA是否可靠、发行者证书的公钥能否正确解开服务器证书的“发行者的数字签名”、服务器证书上的域名是否和服务器的实际域名相匹配等;
5.客户端随机产生一个用于后面通讯的“对称密码”,然后用服务端的公钥对其加密,将加密后的“预主密码”传给服务端;
6.服务端将用自己的私钥解开加密的“预主密码”,然后执行一系列步骤来产生主密码;
7.客户端向服务端发岀信息,指明后面的数据通讯将使用主密码为对称密钥,同时通知服务器客户端的握手过程结束;
8.服务端向客户端发出信息,指明后面的数据通讯将使用主密码为对称密钥,同时通知客户端服务器端的握手过程结束;
9.SSL的握手部分结束,SSL安全通道建立,客户端和服务端开始使用相同的对称密钥对数据进行加密,然后通过 Socket进行传输。

下面,我们结合JDK的SSL工作原理对Netty的SSL单向认证过程进行讲解,首先,我们看下 JDK SSL单向认证的流程图如图24-7所示。

下面结合 JDK SSL引擎的调测日志信息我们对SsSL单向认证的流程进行详细讲解,对于比较简单的流程会进行步骤合并。
步骤1:客户端使用TLS协议版本发送一个 ClientHello消息,这个消息包含一个随机数、建议的加密算法套件和压缩方法列表,如下所示:

步骤2:服务端使用 Serverhello消息来响应,这个消息包含由客户提供的信息基础上的另一个随机数和一个可选的会话ID,以及服务端选择的加密套件算法,响应消息如下:

步骤3:服务端发送自签名的证书消息,包含完整的证书链:

步骤4:服务端向客户端发送自己的公钥信息,最后发送 ServerHelloDone:

步骤5:客户端对服务端自签名的证书进行认证,如果客户端的信任证书列表中包含了服务端发送的证书,对证书进行合法性认证,相关信息如下:

步骤6:客户端通知服务器改变加密算法,通过 ChangeCipherSpec消息发给服务端,随后发送 Finished消息,告知服务器请检査加密算法的变更请求:

步骤7:服务端读取到 ChangeCipherSpec变更请求消息,向客户端返回确认密钥变更消息,最后通过发送 Finished消息表示SSL/TLS握手结束:

2.2、SSL双向认证

与单向认证不同的是服务端也需要对客户端进行安全认证。这就意味着客户端的自签名证书也需要导入到服务端的数字证书仓库中。

2.2.1、双向认证开发

首先,生成客户端的自签名证书:

keytool -export -alias smcc -keystore cChat.jks -storepass cNetty -file cChat.cer
存储在文件 <cChat.cer> 中的证书

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore cChat.jks -destkeystore cChat.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

然后将客户端的自签名证书导入到服务端的信任证书仓库中:

keytool -import -trustcacerts -alias smcc -file cChat.cer -storepass sNetty -keystore sChat.jks
所有者: CN=localhost
发布者: CN=localhost
序列号: 37afd3f0
有效期为 Tue May 14 17:05:35 CST 2019 至 Wed May 13 17:05:35 CST 2020
证书指纹:
     MD5:  F9:B6:EF:4A:83:55:A8:13:03:7C:DF:D0:95:EB:39:04
     SHA1: 69:15:51:47:5E:A8:AD:E8:77:AC:68:EB:BC:D9:E1:53:13:3D:78:59
     SHA256: B0:FD:59:0A:8F:D0:91:BB:E0:85:87:AE:9A:E7:00:DA:79:44:76:17:DF:6E:3C:E2:ED:1E:38:5D:3B:9F:D2:7B
签名算法名称: SHA256withRSA
主体公共密钥算法: 2048 位 RSA 密钥
版本: 3

扩展:

#1: ObjectId: 2.5.29.14 Criticality=false
SubjectKeyIdentifier [
KeyIdentifier [
0000: 51 89 75 FB B7 15 F5 EE   D3 C0 33 C7 F5 9A 25 CE  Q.u.......3...%.
0010: 42 5A A6 29                                        BZ.)
]
]

是否信任此证书? [否]:  y
证书已添加到密钥库中

Warning:
JKS 密钥库使用专用格式。建议使用 "keytool -importkeystore -srckeystore sChat.jks -destkeystore sChat.jks -deststoretype pkcs12" 迁移到行业标准格式 PKCS12。

证书导入之后,需要对SSL客户端和服务端的代码同时进行修改,首先我们看下服务端如何修改。
由于服务端需要对客户端进行验证,因此在初始化服务端 SSLContext的时候需要加载证书仓库。首先需要对 TrustManagerFactory进行初始化,代码如下:

初始化 SSLContext的时候根据 TrustManagerFactory获取 TrustManager数组,代码如下:

最后,创建 SSLEngine之后,设置需要进行客户端认证,代码如下:

完成服务端修改之后,再回头看下客户端的修改,由于服务端需要认证客户端的证书,因此,需要初始化和加载私钥仓库,向服务端发送公钥,初始化 KeyStore的代码如下:

初始化 SSLContext的时候需要传入 KeyManager数组,代码如下:

客户端开发完成之后,测试下程序是否能够正常工作,运行结果如下所示。

2.2.2、双向认证原理

SSL双向认证相比单向认证,多了一步服务端发送认证请求消息给客户端,客户端发送自签名证书给服务端进行安全认证的过程。下面,我们结合Netty SSL调测日志,对双向认证的差异点进行分析。
相比于客户端,服务端在发送 ServerHello时携带了要求客户端认证的请求信息,如下所示:

客户端接收到服务端要求客户端认证的请求消息之后,发送自己的证书信息给服务端,信息如下:

服务端对客户端的自签名证书进行认证,信息如下:

2.3、第三方CA认证

使用 jdk keytool生成的数字证书是自签名的。自签名就是指证书只能保证自己是完整且没有经过非法修改,但是无法保证这个证书是属于谁的。为了对自签名证书进行认证,需要每个客户端和服务端都交换自己自签名的私有证书,对于一个大型网站或者应用服务器,这种工作量是非常大的。

基于自签名的SSL双向认证,只要客户端或者服务端修改了密钥和证书,就需要重新进行签名和证书交换,这种调试和维护工作量是非常大的。因此,在实际的商用系统中往往会使用第三方CA证书颁发机构进行签名和验证。我们的浏览器就保存了几个常用的CA_ROOT。每次连接到网站时只要这个网站的证书是经过这些 CA_ROOT签名过的。就可以通过验证了。

CA数字证书认证服务往往是收费的,国内有很多数字认证中心都提供相关的服务,有需要的可以通过这些商业机构获取认证。

作为示例,我们自己生成一个 CA_ROOT的密钥对,部署应用时,把这个 CA_ROOT的私钥部署在所有需要SSL传输的节点就可以完成安全认证。作为示例,如果要生成CA_ROOT,我们使用开源的 OpenSSL。

下面我们对基于第三方CA认证的步骤进行详细介绍。

2.3.1、服务端证书制作

步骤1:利用 OpenSsL生成CA证书:
openssl req -new -x509 -keyout ca.key -out ca.crt -days 365
步骤2:生成服务端密钥对:
keytool -genkey -alias securechat -keysize 2048 -validity 365 -keyalg RSA -dname "CN=localhost" -keypass sNetty -storepass sNetty -keystore sChat.jks
步骤3:生成证书签名请求
keytool -certreq -alias securechat -sigalg MD5withRSA -file sChat.csr -keypass sNetty -storepass sNetty -keystore sChat.jks
步骤4:用CA私钥进行签名:
openssl ca -in sChat.csr -out sChat.crt -cert ca.crt -keyfile ca.key -notext
步骤5:导入信任的CA根证书到 keystore:
keytool -import -v -trustcacerts -alias ca_root -file ca.crt -storepass sNetty -keystore sChat.jks
步骤6:将CA签名后的 server端证书导入 keystore:

keytool -import -v -alias securechat -file server.crt -keypass sNetty -storepass sNetty -keystore sChat.jks

2.3.2、客户端证书制作

步骤1:生成客户端密钥对:
keytool -genkey -alias smcc -keysize 2048 -validity 365 -keyalg RSA -dname "CN=localhost" -keypass cNetty -storepass cNetty -keystore cChat.jks
步骤2:生成证书签名请求
keytool -certreq -alias smcc -sigalg MD5withRSA -file cChat.csr -keypass cNetty -storepass cNetty -keystore cChat.jks
步骤3:用CA私钥进行签名:
openssl ca -in cChat.csr -out cNetty.crt -cert ca.crt -keyfile ca.key -notext
步骤4:导入信任的CA根证书到 keystore:
keytool -import -v -trustcacerts -alias ca_root -file ca.crt -storepass cNetty -keystore cChat.jks
步骤5:将CA签名后的 client端证书导入 keystore:
keytool -import -v -alias smcc -file cNetty.crt -keypass cNetty -storepass cNetty -keystore cChat.jks
基于CA认证的开发和测试与SSL双向和单向认证代码相同,此处不再赘述。

3、Netty SSL源码分析

3.1、客户端

当客户端和服务端的TCP链路建立成功之后, SsIHandler的 channelActive被触发,SSL客户端通过SSL引擎发起握手请求消息,代码如下:

发起握手请求之后,需要将 SSLEngine创建的握手请求消息进行SSL编码,发送给服务端,因此,握手之后立即调用 wrapNonAppData方法,下面具体对该方法进行分析:

因为只需要发送握手请求消息,因此 SourceByteBuf为空,下面看下wrap方法的具体实现。

将SSL引擎中创建的握手请求消息编码到目标 ByteBuffer中,然后对写索引进行更新。
判断写入操作是否越界,如果越界说明out容量不足,需要调用 ensureWritable对 ByteBuf进行动态扩展,扩展之后继续尝试编码操作。如果编码成功,返回SSL引擎操作结果。

对编码结果进行判断,如果编码字节数大于0,则将编码后的结果发送给服务端,然后释放临时变量out。
判断SSL引擎的操作结果,SSL引擎的操作结果定义如下:
1. FINISHED: SSLEngine已经完成握手:
2. NEED_TASK: SSLEngine在继续进行握手前需要一个(或多个)代理任务的结果
3. NEED_UNWRAP:在继续进行握手前, SSLEngine需要从远端接收数据,所以应带调用SSLEngine.unwrap()
4. NEED_WRAP在继续进行握手前, SSLEngine必须向远端发送数据,所以应该调用 SSLEngine.wrap();
5. NOT_HANDSHAKING: SSLEngine当前没有进行握手。
下面我们分别对5种操作的代码进行分析:

如果握手成功,则设置 handshakePromise的操作结果为成功,同时发送SslHandshakeCompletionEvent.SUCCES给SSL监听器,代码如下:

如果是 NEED_TASK,说明异步执行 SSL Task,完成后续可能耗时的操作或者任务Netty封装了一个任务立即执行线程池专门处理SSL的代理任务,代码如下

如果是 NEED_UNWRAP,则判断是否由 UNWRAP发起,如果不是则执行 UNWRAP操作。
如果是 NOT_HANDSHAKING,则调用 unwrap,继续接收服务端的消息服务端应答消息的接收跟服务端接收客户端的代码类似,唯一不同之处在于SSL引擎的客户端模式设置不同,一个是服务端,一个是客户端。上层的代码处理是相同的,下面我们在SSL服务端章节分析握手消息的接收。

3.2、服务端

SSL服务端接收客户端握手请求消息的入口方法是 decode方法,下面对它进行详细分析。
首先获取接收缓冲区的读写索引,并对读取的偏移量指针进行备份:

对半包标识进行判断,如果上一个消息是半包消息,则判断当前可读的字节数是否小于整包消息的长度,如果小于整包长度,则说明本次读取操作仍然没有把SSL整包消息读取完整,需要返回IO线程继续读取,代码如下:

如果消息读取完整,则修改偏移量:同时置位半包长度标识。

下面在for循环中读取SSL消息,因为TCP存在拆包和粘包,因此一个 ByteBuf可能包含多条完整的SSL消息。
首先判断可读的字节数是否小于协议消息头长度,如果是则退出循环继续由IO线程接收后续的报文:

获取SSL消息包的报文长度,具体算法不再介绍,可以参考SSL的规范文档进行解读,代码如下:

对长度进行判断,如果SSL报文长度大于可读的字节数,说明是个半包消息,将半包标识长度置位,返回IO线程继续读取后续的数据报,代码如下:

对消息进行解码,将SSL加密的消息解码为加密前的原始数据, unwrap方法如下

调用 SSLEngine的 unwrap方法对SSL原始消息进行解码,对解码结果进行判断,如果越界,说明out缓冲区不够,需要进行动态扩展。如果是首次越界,为了尽量节约内存,使用SSL最大缓冲区长度和SSL原始缓冲区可读的字节数中较小的。如果再次发生缓冲区越界,说明扩张后的缓冲区仍然不够用,直接使用SSL缓冲区的最大长度,保证下次解码成功。

解码成功之后,对SSL引擎的操作结果进行判断:如果需要继续接收数据,则继续执行解码操作;如果需要发送握手消息,则调用 wrapNonAppData发送握手消息;如果需要异步执行SSL代理任务,则调用立即执行线程池执行代理任务;如果是握手成功,则设置SSL操作结果,发送SSL握手成功事件;如果是应用层的业务数据,则继续执行解码操作,其他操作结果,抛出操作类型异常。

需要指出的是,SSL客户端和服务端接收对方SSL握手消息的代码是相同的,那为什么SSL服务端和客户端发送的握手消息不同呢?这些是SSL引擎负责区分和处理的,我们在创建SSL引擎的时候设置了客户端模式,SSL引擎就是根据这个来进行区分的,代码如下

3.3、消息读取

SSL的消息读取实际就是 ByteToMessageDecoder将接收到的SSL加密后的报文解码为原始报文,然后将整包消息投递给后续的消息解码器,对消息做二次解码。基于SSL的消息解码模型如下:

SSL消息读取的入口都是 decode,因为是非握手消息,它的处理非常简单,就是循环调用引擎的 unwrap方法,将SSL报文解码为原始的报文,代码如下

握手成功之后的所有消息都是应用数据,因此它的操作结果为 NOT_HANDSHAKING,遇到此标识之后继续读取消息,直到没有可读的字节,退出循环,代码如下:

如果读取到了可用的字节,则将读取到的缓冲区加到输出结果列表中,代码如下:

ByteToMessageDecoder判断解码结果List,如果非空,则循环调用后续的 Handler,由后续的解码器对解密后的报文进行二次解码。

3.4、消息发送

SSL消息发送时,由 SsIHandler对消息进行编码,编码后的消息实际就是SSL加密后的消息,它的入口是fush方法,代码如下:

从待加密的消息队列中弹出消息,调用SSL引擎的wrap方法进行编码,代码如下:

wrap方法很简单,就是调用SsL引擎的编码方法,然后对写索引进行修改,如果缓冲区越界,则动态扩展缓冲区。对SSL操作结果进行判断,因为已经握手成功,因此返回结果是 NOT_HANDSHAKING,执行 finishWrap方法,调用 ChannelHandlerContext的write方法,将消息写入发送缓冲区中,如果待发送的消息为空,则构造空的 ByteBuf写入:

编码后,调用 ChannelHandlerContext的flush方法消息发送给对方,即可完成消息的SSL加密发送。

4、Netty扩展的安全特性

利用Netty的 ChannelHandler接口提供的网络切面,用户可以非常容易的扩展Netty的安全策略,下面对比较典型的安全扩展特性进行讲解。

4.1、IP地址黑名单机制

IP地址黑名单是比较常用的弱安全保护策略,它的特点就是服务端在与客户端通信的过程中,对客户端的IP地址进行校验,如果发现对方IP在黑名单列表中,则拒绝与其通信,关闭链路。
下面我们对基于IP地址的黑名单在 Netty中的实现进行介绍。
首先定义 BlacklistHandler继承自 ChannelHandlerAdapter,然后定义IP地址黑名单列表,如下所示:
private final List<InetAddress> blacklist = new CopyOnWriteArrayList<InetAddress>();
提供对 public的黑名单管理接口,用于设置黑名单、删除或者添加黑名单,示例代码如下:
public void setBlacklist(InetAddress[] addresses) (//TODO
public boolean removeBlacklist(InetAddress address) (//TODO
public boolean addBlacklist(InetAddress address) (//TODO
public void clearBlacklist()//TODO 1
链路注册、链路激活、消息读取、消息发送的时候对对端的IP地址进行校验,如果在黑名单列表中,则拒绝当前操作,并关闭链路,打印日志,相关伪代码如下:

为什么要在消息读取和发送的时候也要对黑名单进行判断呢?原因是黑名单支持动态添加策略,一些黑名单是通过业务逻辑判断和执行过程中动态添加进去的,如果是长链接,只在链路首次建立的时候判断是不够的。

4.2、接入认证

接入认证策略非常多,通常是较强的安全认证策略,例如基于用户名+密码的认证,认证内容往往采用加密的方式,例如Base64+AES等
在Netty中如果要支持安全认证,往往是通过定制 ChannelHandler接口来实现,具体策略如下:
1.在链路首次激活的时候,客户端发送认证信息给服务端,伪代码如下:

2.服务端接收到客户端消息之后,根据消息内容进行判断,如果是首次接入的认证消息,则进行认证,认证失败,打印日志,关闭链接;认证成功,继续业务逻辑处理,伪代码如下:

3.客户端接收到服务端消息,对消息类型进行判断,对于认证应答消息,如果认证成功,则继续业务逻辑处理;如果认证失败,则关闭链路,打印异常日志:

通常情况下,接入认证失败服务端都会返回认证失败应答消息,给出错误码或者认证失败原因,如果服务端不返回失败应答而是直接关闭链路,客户端接收到链路关闭通知之后直接关闭链路即可。
 

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 3
    评论
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值