陈建华的博客
专注web开发
Netty实战:HTTPS传输原理分析及Netty的实现
2019-01-28 17:17:03   阅读274次

1.从SSL/TLS说起

众所周知HTTP是超文本传输协议,信息是明文传输,因此就有了HTTPS(Hyper Text Transfer Protocol over Secure Socket Layer),它也是一种超文本传送协议。HTTPS 在 HTTP 的基础上加入了 SSL/TLS协议,SSL/TLS依靠证书来验证服务器的身份,并为浏览器和服务器之间的通信加密。


SSL/TLS is what?

SSL 是“Secure Sockets Layer”的缩写,中文叫做“安全套接层”。它是在上世纪90年代中期,由网景公司设计的。


到了1999年,SSL 因为应用广泛,已经成为互联网上的事实标准。IETF 就在那年把 SSL 标准化。标准化之后的名称改为 TLS(Transport Layer Security),中文叫做“传输层安全协议”。


SSL/TLS可以视作同一个东西的不同阶段。总之 HTTPS = HTTP + SSL/TLS


SSL/TLS 协议处于该分层协议栈结构中的位置,其分层结构位置参考如下:

20180903224014864.jpg

SSL/TLS运行过程

SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。


基本概念


对称密码:加密和解密使用同一密匙。

公钥密码:加密和解密使用不同密钥的方式。

数字签名:由于消息认证码使用公钥进行加密的,会出现发送方否认的情况,所以为了防止这种情况出现,发送方使用私钥进行加密散列值。

证书:我们必须保证验证数字签名的公钥必须属于发送方,否则数字签名会失效。为了确认自己得到的公钥是否合法,我们需要使用证书。数字证书简称CA,它由权威机构给某网站颁发的一种认可凭证,这个凭证是被浏览器所认可的。

SSL/TLS协议的基本过程是这样的:


(1) 客户端向服务器端索要并验证公钥。


(2) 双方协商生成"对话密钥"。


(3) 双方采用"对话密钥"进行加密通信。


前两步,又称为"握手阶段"(handshake)。


1.1 握手阶段的详细过程

"握手阶段"涉及四次通信,我们一个个来看。需要注意的是,"握手阶段"的所有通信都是明文的。


20180903224041229.png

客户端发出请求(ClientHello)

客户端(通常是浏览器)先向服务器发出加密通信的请求,这被叫做ClientHello请求。在这一步,客户端主要向服务器提供以下信息。

(1) 支持的协议版本,比如TLS 1.0版。 
(2) 一个客户端生成的随机数,稍后用于生成"对话密钥"。 
(3) 支持的加密方法,比如RSA公钥加密。 
(4) 支持的压缩方法。

服务器回应(SeverHello)

服务器收到客户端请求后,向客户端发出回应,这叫做SeverHello。服务器的回应包含以下内容。

(1) 确认使用的加密通信协议版本,比如TLS 1.0版本。如果浏览器与服务器支持的版本不一致,服务器关闭加密通信。 
(2) 一个服务器生成的随机数,稍后用于生成"对话密钥"。 
(3) 确认使用的加密方法,比如RSA公钥加密。 
(4) 服务器证书。

除了上面这些信息,如果服务器需要确认客户端的身份,就会再包含一项请求,要求客户端提供"客户端证书"。比如,金融机构往往只允许认证客户连入自己的网络,就会向正式客户提供USB密钥,里面就包含了一张客户端证书。


客户端回应


客户端收到服务器回应以后,首先验证服务器证书。如果证书不是可信机构颁布、或者证书中的域名与实际域名不一致、或者证书已经过期,就会向访问者显示一个警告,由其选择是否还要继续通信。


如果证书没有问题,客户端就会从证书中取出服务器的公钥。然后,向服务器发送下面三项信息

(1) 一个随机数。该随机数用服务器公钥加密,防止被窃听。 
(2) 编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。
(3) 客户端握手结束通知,表示客户端的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供服务器校验。

上面第一项的随机数,是整个握手阶段出现的第三个随机数,又称"pre-master key"。有了它以后,客户端和服务器就同时有了三个随机数,接着双方就用事先商定的加密方法,各自生成本次会话所用的同一把"会话密钥"。


服务器的最后回应

服务器收到客户端的第三个随机数pre-master key之后,计算生成本次会话所用的"会话密钥"。然后,向客户端最后发送下面信息。

(1)编码改变通知,表示随后的信息都将用双方商定的加密方法和密钥发送。 
(2)服务器握手结束通知,表示服务器的握手阶段已经结束。这一项同时也是前面发送的所有内容的hash值,用来供客户端校验。

至此,整个握手阶段全部结束。接下来,客户端与服务器进入加密通信,就完全是使用普通的HTTP协议,只不过用"会话密钥"加密内容。


2.使用JDK工具keytool

使用ssl双向验证,就必须先要生成服务端和客户端的证书,并相互添加信任,具体流程如下:


生成Netty服务端私钥和证书仓库


keytool -genkey -alias server_cert -keypass 123456 -keyalg RSA -keysize 1024 -validity 365 -keystore i:\server.jks -storepass 123456 -dname “CN=localhost”


keysize 2048 密钥长度2048位(这个长度的密钥目前可认为无法被暴力破解)

validity 365 证书有效期365天

keyalg RSA 使用RSA非对称加密算法

dname “CN=localhost” 设置Common Name为localhost

keypass 密钥的访问密码为123456

storepass 密钥库的访问密码为123456(其实这两个密码也可以设置一样,通常都设置一样,方便记)

keystore 指定生成的密钥库文件为i:\server.jks如果只制定文件名,那么会生成至当前用户目录下

生成Netty服务端自签名证书


keytool -export -alias server_cert -keystore i:/server.jks -storepass 123456 -file i:/server.cer


生成客户端的密钥对和证书仓库,用于将服务端的证书保存到客户端的授信证书仓库中


keytool -genkey -alias client_cert -keysize 2048 -validity 365 -keyalg RSA -dname “CN=localhost” -keypass 123456 -storepass 123456 -keystore i:/client.jks


将服务端证书导入到客户端的证书仓库中


keytool -import -trustcacerts -alias securechat -file i:/server.cer -storepass 123456 -keystore clinet.jks


如果你只做单向认证,则到此就可以结束了,如果是双响认证,则还需继续往下走


生成客户端自签名证书


keytool -export -alias client_cert -keystore i:/client.jks -storepass 123456 -file client.cer


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


keytool -import -trustcacerts -alias securechat -file i:/client.cer -storepass 123456 -keystore i:/server.jks


3.通过 SSL/TLS 保护 Netty 通信

为了支持 SSL/TLS,Java 提供了 javax.net.ssl包,它的 SSLContext和SSLEngine类使得实现解密和加密相当简单直接。Netty 通过一个名为SslHandler的ChannelHandler实现利用了这个 API,其中SslHandler在内部使用SSLEngine来完成实际的工作。


下图为SslHandler类的继承关系:

20180903224041229.png

通过 SslHandler 进行解密和加密的数据流:

20180903224041229.png

通过Factory创建SSLContext并对其进行正确的初始化

public class SSLContextFactory  {
    public static SSLContext getSslContext() throws Exception {
        char[] passArray = "123456".toCharArray();
        SSLContext sslContext = SSLContext.getInstance("TLSv1");
        KeyStore ks = KeyStore.getInstance("JKS");
        //加载keytool 生成的文件
        FileInputStream inputStream = new FileInputStream("i:/server.jks");
        ks.load(inputStream, passArray);
        KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
        kmf.init(ks, passArray);
        sslContext.init(kmf.getKeyManagers(), null, null);
        inputStream.close();
        return sslContext;
    }
}

添加handler,利用SSLContext创建SSL引擎SSLEngine,设置SSLEngine为服务端模式,由于不需要对客户端进行认证,因此NeedClientAuth不需要额外设置,将SslHandler添加到pipeline中,利用SslHandler实现Socket安全传输

.childHandler(new ChannelInitializer() {
                        @Override
                        protected void initChannel(Channel ch) throws Exception {
                            ChannelPipeline pipeline = ch.pipeline() ;
                            SSLEngine sslEngine = SSLContextFactory.getSslContext().createSSLEngine();
                            sslEngine.setUseClientMode(false);
                            sslEngine.setNeedClientAuth(true);
                            pipeline.addFirst("ssl", new SslHandler(sslEngine));
                        }
                    });

4.基于Netty的Http通信

HTTP 是基于请求/响应模式的:客户端向服务器发送一个 HTTP 请求,然后服务器将会返回一个 HTTP 响应。经过HttpServerCodec解码之后,一个HTTP请求会导致:ParseRequestHandler的 channelRead()方法调用多次(测试时 "received message"输出了两次)

20180903224146377.jpg

为了消除这项繁琐的任务,Netty 提供了一个聚合器,它可以将多个消息部分合并为 FullHttpRequest 或者 FullHttpResponse 消息。FullHttpRequest 和 FullHttpResponse 消息是特殊的子类型,分别代表了完整的请求和响应。


用HttpObjectAggregator 将多个消息转换为单一的一个FullHttpRequest,如下:

ph.addLast("decoder",new HttpRequestDecoder());
  ph.addLast("encoder",new HttpResponseEncoder());
  ph.addLast("aggregator", new HttpObjectAggregator(10*1024*1024));

接下来添加业务逻辑代码,在上面的基础上 添加ph.addLast(“http”,HttpServerHandler)

public class HttpServerHandler extends ChannelInboundHandlerAdapter {
    private String result = "";
    @Override
    public void channelRead(ChannelHandlerContext ctx, Object msg) throws Exception {
        if (!(msg instanceof FullHttpRequest)) {
            result = "未知请求!";
            send(ctx, result, HttpResponseStatus.BAD_REQUEST);
            return;
        }
        FullHttpRequest httpRequest = (FullHttpRequest) msg;
        try {
            String path = httpRequest.uri();          //获取路径
            String body = getBody(httpRequest);     //获取参数
            HttpMethod method = httpRequest.method();//获取请求方法
            System.out.println("接收到:" + method + " 请求");
            //如果是GET请求
            if (HttpMethod.GET.equals(method)) {
                //接受到的消息,做业务逻辑处理...
                System.out.println("body:" + body);
                result = "GET请求";
                send(ctx, result, HttpResponseStatus.OK);
                return;
            }
            //如果是POST请求
            if (HttpMethod.POST.equals(method)) {
                //接受到的消息,做业务逻辑处理...
                System.out.println("body:" + body);
                result = "POST请求";
                send(ctx, result, HttpResponseStatus.OK);
                return;
            }
        } catch (Exception e) {
            System.out.println("处理请求失败!");
            e.printStackTrace();
        } finally {
            //释放请求
            httpRequest.release();
        }
    }
     //获取body参数
    private String getBody(FullHttpRequest request) {
        ByteBuf buf = request.content();
        return buf.toString(CharsetUtil.UTF_8);
    }
     //发送的返回值
    private void send(ChannelHandlerContext ctx, String context, HttpResponseStatus status) {
        FullHttpResponse response = new DefaultFullHttpResponse(HttpVersion.HTTP_1_1, status, Unpooled.copiedBuffer(context, CharsetUtil.UTF_8));
        response.headers().set(HttpHeaderNames.CONTENT_TYPE, "text/plain; charset=UTF-8");
        ctx.writeAndFlush(response).addListener(ChannelFutureListener.CLOSE);
    }
     //建立连接时,返回消息
    @Override
    public void channelActive(ChannelHandlerContext ctx) throws Exception {
        System.out.println("连接的客户端地址:" + ctx.channel().remoteAddress());
        ctx.writeAndFlush("客户端" + InetAddress.getLocalHost().getHostName() + "成功与服务端建立连接! ");
        super.channelActive(ctx);
    }
}

5.基于Netty的HTTPS通信

哈哈,终于到了收获阶段_

有了上面SSL/TLS和HTTP的基础,启用 HTTPS 只需要将 SslHandler 添加到 ChannelPipeline 的ChannelHandler 组合中。

public class HttpServerInitializer extends ChannelInitializer<SocketChannel> {
    @Override
    protected void initChannel(SocketChannel ch) throws Exception {
        ChannelPipeline ph = ch.pipeline();
        //添加sslhandler
        SSLEngine sslEngine = SSLContextFactory.getSslContext().createSSLEngine();
        sslEngine.setUseClientMode(false);
        ch.pipeline().addLast(new SslHandler(sslEngine));
        //处理http服务的关键handler
        //一个组合的HttpRequestDecoder和HttpResponseEncoder使得服务器端HTTP实现更容易。
        ph.addLast("codec", new HttpServerCodec());
        //ph.addLast("decoder",new HttpRequestDecoder());
        //ph.addLast("encoder",new HttpResponseEncoder());
        ph.addLast("aggregator", new HttpObjectAggregator(10*1024*1024));
        ph.addLast("handler", new HttpServerHandler());// 服务端业务逻辑
    }
}

至此,通过通过浏览器使用https方式访问。本文谈到SSL/TLS协议的基本原理,以及基于Netty的实现,然后说了HTTP的通信,最后实现基于Netty的HTTPS通信,下一篇介绍PipleLine的源码分析_

本文的所有代码参考:git地址




-----------------------------------------------------
转载请注明来源此处
原地址:#

-----网友评论----
暂无评论
-----发表评论----
微网聚博客乐园 ©2014 blog.mn886.net 鲁ICP备14012923号   网站导航