Fork me on GitHub
余鸢

对Netty加密和HTTP HTTPS应用

使用 SSL/TLS 加密 Netty 程序

今天数据隐私是一个十分关注的问题,作为开发人员,我们需要准备好解决这个问题。至少我们需要熟悉加密协议 SSL 和 TLS 等之上的其他协议实现数据安全。作为一个 HTTPS 网站的用户,你是安全。当然,这些协议是广泛不基于 http 的应用程序,例如安全SMTP(SMTPS)邮件服务,甚至关系数据库系统。

为了支持 SSL/TLS,Java 提供了 javax.net.ssl API 的类SslContext 和 SslEngine 使它相对简单的实现解密和加密。Netty 的利用该 API 命名 SslHandler 的 ChannelHandler 实现, 有一个内部 SslEngine 做实际的工作。

图1显示了一个使用 SslHandler 数据流图。

15

  1. 加密的入站数据被 SslHandler 拦截,并被解密
  2. 前面加密的数据被 SslHandler 解密
  3. 平常数据传过 SslHandler
  4. SslHandler 加密数据并它传递出站

代码2.0所示一个 SslHandler 使用 ChannelInitializer 添加到 ChannelPipeline。(回想一下,
当 Channel 注册时 ChannelInitializer 用于设置 ChannelPipeline 。)

代码2.0 Add SSL/TLS support

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class SslChannelInitializer extends ChannelInitializer<Channel> {
private final SslContext context;
private final boolean startTls;
public SslChannelInitializer(SslContext context, boolean client, boolean startTls) { //1
this.context = context;
this.startTls = startTls;
}
@Override
protected void initChannel(Channel ch) throws Exception {
SSLEngine engine = context.newEngine(ch.alloc()); //2
engine.setUseClientMode(client); //3
ch.pipeline().addFirst("ssl", new SslHandler(engine, startTls)); //4
}
}
  1. 使用构造函数来传递 SSLContext 用于使用(startTls 是否启用)
  2. 从 SslContext 获得一个新的 SslEngine 。给每个 SslHandler 实例使用一个新的SslEngine
  3. 设置 SslEngine 是 client 或者是 server 模式
  4. 添加 SslHandler 到 pipeline 作为第一个处理器

在大多数情况下,SslHandler 将成为 ChannelPipeline 中的第一个 ChannelHandler 。这将确保所有其他 ChannelHandler 应用他们的逻辑到数据后加密后才发生,从而确保他们的变化是安全的。

SslHandler 有很多有用的方法。例如,在握手阶段两端相互验证,商定一个加密方法。您可以配置 SslHandler 修改其行为或提供 在SSL/TLS 握手完成后发送通知,这样所有数据都将被加密。 SSL/TLS 握手将自动执行。

构建 Netty HTTP/HTTPS 应用

HTTP Decoder, Encoder 和 Codec

HTTP 是请求-响应模式,客户端发送一个 HTTP 请求,服务就响应此请求。Netty 提供了简单的编码、解码器来简化基于这个协议的开发工作。图8.2和图8.3显示 HTTP 请求和响应的方法是如何生产和消费的

16

  1. HTTP Request 第一部分是包含的头信息
  2. HttpContent 里面包含的是数据,可以后续有多个 HttpContent 部分
  3. LastHttpContent 标记是 HTTP request 的结束,同时可能包含头的尾部信息
  4. 完整的 HTTP request

HTTP 请求/响应可能包含不止一个数据部分,它总是终止于LastHttpContent 部分。FullHttpRequest 和FullHttpResponse 消息是特殊子类型,分别表示一个完整的请求和响应。所有类型的 HTTP 消息(FullHttpRequest ,LastHttpContent 等)实现 HttpObject 接口。

代码2.1所示的是将支持 HTTP 添加到您的应用程序是多么简单。仅仅添加正确的ChannelHandler 到 ChannelPipeline 中

代码2.1 Add support for HTTP

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class HttpPipelineInitializer extends ChannelInitializer<Channel> {
private final boolean client;
public HttpPipelineInitializer(boolean client) {
this.client = client;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (client) {
pipeline.addLast("decoder", new HttpResponseDecoder()); //1
pipeline.addLast("encoder", new HttpRequestEncoder()); //2
} else {
pipeline.addLast("decoder", new HttpRequestDecoder()); //3
pipeline.addLast("encoder", new HttpResponseEncoder()); //4
}
}
}
  1. client: 添加 HttpResponseDecoder 用于处理来自 server 响应
  2. client: 添加 HttpRequestEncoder 用于发送请求到 server
  3. server: 添加 HttpRequestDecoder 用于接收来自 client 的请求
  4. server: 添加 HttpResponseEncoder 用来发送响应给 client

HTTP消息聚合

安装 ChannelPipeline 中的初始化之后,你能够对不同 HttpObject 消息进行操作。但由于HTTP 请求和响应可以由许多部分组合而成,你需要聚合他们形成完整的消息。为了消除这种繁琐任务, Netty 提供了一个聚合器,合并消息部件到 FullHttpRequest 和 FullHttpResponse消息。这样您总是能够看到完整的消息内容。

这个操作有一个轻微的成本,消息段需要缓冲,直到完全可以将消息转发到下一个ChannelInboundHandler 管道。但好处是,你不必担心消息碎片。

实现自动聚合只需添加另一个 ChannelHandler 到 ChannelPipeline。代码2.2显示了这是如何实现的。

代码2.2 自动聚合HTTP消息片段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class HttpAggregatorInitializer extends ChannelInitializer<Channel> {
private final boolean client;
public HttpAggregatorInitializer(boolean client) {
this.client = client;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (client) {
pipeline.addLast("codec", new HttpClientCodec()); //1
} else {
pipeline.addLast("codec", new HttpServerCodec()); //2
}
pipeline.addLast("aggegator", new HttpObjectAggregator(512 * 1024)); //3
}
}
  1. client: 添加 HttpClientCodec
  2. server: 添加 HttpServerCodec 作为我们是 server 模式时
  3. 添加 HttpObjectAggregator 到 ChannelPipeline, 使用最大消息值是 512kb

HTTP 压缩

使用 HTTP 时建议压缩数据以减少传输流量,压缩数据会增加 CPU 负载,现在的硬件设施都很强大,大多数时候压缩数据时一个好主意。Netty 支持“gzip”和“deflate”,为此提供了两个ChannelHandler 实现分别用于压缩和解压。看下面代码:

HTTP Request Header
客户端可以通过提供下面的头显示支持加密模式。然而服务器不是,所以不得不压缩它发送的数据。

1
2
3
GET /encrypted-area HTTP/1.1
Host: www.example.com
Accept-Encoding: gzip, deflate

例子:

代码2.3 自动压缩HTTP消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class HttpAggregatorInitializer extends ChannelInitializer<Channel> {
private final boolean isClient;
public HttpAggregatorInitializer(boolean isClient) {
this.isClient = isClient;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
if (isClient) {
pipeline.addLast("codec", new HttpClientCodec()); //1
pipeline.addLast("decompressor",new HttpContentDecompressor()); //2
} else {
pipeline.addLast("codec", new HttpServerCodec()); //3
pipeline.addLast("compressor",new HttpContentCompressor()); //4
}
}
}
  1. client: 添加 HttpClientCodec
  2. client: 添加 HttpContentDecompressor 用于处理来自服务器的压缩的内容
  3. server: HttpServerCodec
  4. server: HttpContentCompressor 用于压缩来自 client 支持的 HttpContentCompressor

压缩与依赖
注意,Java 6或者更早版本,如果要压缩数据,需要添加 jzlib 到 classpath

1
2
3
4
5
<dependency>
<groupId>com.jcraft</groupId>
<artifactId>jzlib</artifactId>
<version>1.1.3</version>
</dependency>

使用 HTTPS

启用 HTTPS,只需添加 SslHandler

代码2.4 使用 HTTPS

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class HttpsCodecInitializer extends ChannelInitializer<Channel> {
private final SslContext context;
private final boolean client;
public HttpsCodecInitializer(SslContext context, boolean client) {
this.context = context;
this.client = client;
}
@Override
protected void initChannel(Channel ch) throws Exception {
ChannelPipeline pipeline = ch.pipeline();
SSLEngine engine = context.newEngine(ch.alloc());
pipeline.addFirst("ssl", new SslHandler(engine)); //1
if (client) {
pipeline.addLast("codec", new HttpClientCodec()); //2
} else {
pipeline.addLast("codec", new HttpServerCodec()); //3
}
}
}
  1. 添加 SslHandler 到 pipeline 来启用 HTTPS
  2. client: 添加 HttpClientCodec
  3. server: 添加 HttpServerCodec ,如果是 server 模式的话

上面的代码就是一个很好的例子,解释了 Netty 的架构是如何让“重用”变成了“杠杆”。我们可以添加一个新的功能,甚至是一样重要的加密支持,几乎没有工作量,只需添加一个ChannelHandler 到 ChannelPipeline。

WebSocket

HTTP 是不错的协议,但是如果需要实时发布信息怎么做?有个做法就是客户端一直轮询请求服务器,这种方式虽然可以达到目的,但是其缺点很多,也不是优秀的解决方案,为了解决这个问题,便出现了 WebSocket。

WebSocket 允许数据双向传输,而不需要请求-响应模式。简单的说, 一个WebSocket 提供一个 TCP 连接两个方向的交通。结合 WebSocket API 它提供了一个替代 HTTP 轮询双向通信从页面到远程服务器。也就是说,WebSocket 提供真正的双向客户机和服务器之间的数据交换。

代码2.5 在服务器上支持WebSocket

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public class WebSocketServerInitializer extends ChannelInitializer<Channel> {
@Override
protected void initChannel(Channel ch) throws Exception {
ch.pipeline().addLast(new HttpServerCodec(),
new HttpObjectAggregator(65536), //1
new WebSocketServerProtocolHandler("/websocket"), //2
new TextFrameHandler(), //3
new BinaryFrameHandler(), //4
new ContinuationFrameHandler()); //5
}
public static final class TextFrameHandler extends
SimpleChannelInboundHandler<TextWebSocketFrame> {
@Override
public void channelRead0(ChannelHandlerContext ctx, TextWebSocketFrame msg)
throws Exception {
// Handle text frame
}
}
public static final class BinaryFrameHandler extends
SimpleChannelInboundHandler<BinaryWebSocketFrame> {
@Override
public void channelRead0(ChannelHandlerContext ctx, BinaryWebSocketFrame msg)
throws Exception {
// Handle binary frame
}
}
public static final class ContinuationFrameHandler extends
SimpleChannelInboundHandler<ContinuationWebSocketFrame> {
@Override
public void channelRead0(ChannelHandlerContext ctx, ContinuationWebSocketFrame msg)
throws Exception {
// Handle continuation frame
}
}
}
  1. 添加 HttpObjectAggregator 用于提供在握手时聚合 HttpRequest
  2. 添加 WebSocketServerProtocolHandler 用于处理色好给你寄握手如果请求是发送到”/websocket.” 端点,当升级完成后,它将会处理Ping, Pong 和 Close 帧
  3. TextFrameHandler 将会处理 TextWebSocketFrames
  4. BinaryFrameHandler 将会处理 BinaryWebSocketFrames
  5. ContinuationFrameHandler 将会处理ContinuationWebSocketFrames

加密 WebSocket 只需插入 SslHandler 到作为 pipline 第一个 ChannelHandler