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 协议处于该分层协议栈结构中的位置,其分层结构位置参考如下:
SSL/TLS运行过程
SSL/TLS协议的基本思路是采用公钥加密法,也就是说,客户端先向服务器端索要公钥,然后用公钥加密信息,服务器收到密文后,用自己的私钥解密。
基本概念
- 对称密码:加密和解密使用同一密匙。
- 公钥密码:加密和解密使用不同密钥的方式。
- 数字签名:由于消息认证码使用公钥进行加密的,会出现发送方否认的情况,所以为了防止这种情况出现,发送方使用私钥进行加密散列值。
- 证书:我们必须保证验证数字签名的公钥必须属于发送方,否则数字签名会失效。为了确认自己得到的公钥是否合法,我们需要使用证书。数字证书简称CA,它由权威机构给某网站颁发的一种认可凭证,这个凭证是被浏览器所认可的。
SSL/TLS协议的基本过程是这样的:
(1) 客户端向服务器端索要并验证公钥。
(2) 双方协商生成”对话密钥”。
(3) 双方采用”对话密钥”进行加密通信。
前两步,又称为”握手阶段”(handshake)。
握手阶段的详细过程
“握手阶段”涉及四次通信,我们一个个来看。需要注意的是,”握手阶段”的所有通信都是明文的。
客户端发出请求(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
类的继承关系:
通过 SslHandler 进行解密和加密的数据流:
通过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”输出了两次)
为了消除这项繁琐的任务,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地址