不使用 SSL/TLS 的网络通信,一般都是明文传输,网络传输内容在传输过程中很容易被窃听甚至篡改,非常不安全。SSL/TLS 协议就是为了解决这些安全问题而设计的。SSL/TLS 协议位于 TCP/IP 协议之上,各个应用层协议之下,使网络传输的内容通过加密算法加密,并且只有服务器和客户端可以加密解密,中间人即使抓到数据包也无法解密获取传输的内容,从而避免安全问题。例如广泛使用的 HTTPS 协议即是在 TCP 协议和 HTTP 协议之间加了一层 SSL/TLS 协议,下一篇文章中有 HTTPS 更详细的介绍。

1. 相关术语

在学习SSL/TLS协议之前,首先要了解一些相关概念:
对称加密:加密和解密都采用同一个密钥,常用的算法有DES、3DES、AES,相对于非对称加密算法更简单速度更快。
非对称加密:和对称加密算法不同,非对称加密算法会有两个密钥:公钥(可以公开的)和私钥(私有的),例如客户端如果使用公钥加密,那么即时其他人有公钥也无法解密,只能通过服务器私有的私钥解密。RSA算法即是典型的非对称加密算法。
数字证书:数字证书是一个包含公钥并且通过权威机构发行的一串数据,数字证书很多需要付费购买,也有免费的,另外也可以自己生成数字证书,本文中将会采用自签名的方式生成数字证书。

2. SSL/TLS流程

使用SSL/TLS协议的服务器和客户端开始通信之前,会先进行一个握手阶段:
1)客户端发出请求:这一步客户端会生成一个随机数传给服务器。
2)服务器回应:这一步服务器会返回给客户端一个服务器数字证书(证书中包含用于加密的公钥),另外服务器也会生成一个随机数给客户端。
3)客户端回应:这一步客户端首先会校验数字证书的合法性,然后会再生成一个随机数,这个随机数会使用第2步中的公钥采用非对称加密算法(例如RSA算法)进行加密后传给服务器,密文只能通过服务器的私钥来解密。
4)服务器最后回应:握手结束。
握手结束后,客户端和服务器都有上面握手阶段的三个随机数。客户端和服务器都通过这三个随机生成一个密钥,接下来所有的通信内容都使用这个密钥通过对称加密算法加密传输,服务器和客户端才开始进行安全的通信。

3. 生成私钥和证书

使用openssl来生成私钥和证书:
openssl req -x509 -newkey rsa:2048 -nodes -days 365 -keyout private.pem -out cert.crt
运行以上命令后,会在当前目录下生成一个私钥文件(private.pem)和一个证书文件(cert.crt)。
生成的私钥和证书Twisted、Netty可以直接使用,然而MINA对私钥文件的格式的要求,要将pem格式转换成der格式,实际上就是将文本文件私钥转成二进制文件私钥。openssl将private.pem转成private.der私钥文件:
openssl pkcs8 -topk8 -inform PEM -in private.pem -outform DER -nocrypt -out private.der

4. SSL/TLS服务器

接下来在TCP消息边界问题及按行分割消息 一文的基础上,加上SSL/TLS层。Netty 通过添加一个 SslHandler 来实现 SSL/TLS,相对 MINA 来说代码就比较简单

  1. public class NettyServer {
  2. public static void main(String[] args) throws InterruptedException, SSLException {
  3. File certificate = new File("/Users/wucao/Desktop/ssl/cert.crt"); // 证书
  4. File privateKey = new File("/Users/wucao/Desktop/ssl/private.pem"); // 私钥
  5. final SslContext sslContext = SslContextBuilder.forServer(certificate, privateKey).build();
  6. EventLoopGroup bossGroup = new NioEventLoopGroup();
  7. EventLoopGroup workerGroup = new NioEventLoopGroup();
  8. try {
  9. ServerBootstrap b = new ServerBootstrap();
  10. b.group(bossGroup, workerGroup)
  11. .channel(NioServerSocketChannel.class)
  12. .childHandler(new ChannelInitializer<SocketChannel>() {
  13. @Override
  14. public void initChannel(SocketChannel ch)
  15. throws Exception {
  16. ChannelPipeline pipeline = ch.pipeline();
  17. // SslHandler要放在最前面
  18. SslHandler sslHandler = sslContext.newHandler(ch.alloc());
  19. pipeline.addLast(sslHandler);
  20. pipeline.addLast(new LineBasedFrameDecoder(80));
  21. pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8));
  22. pipeline.addLast(new TcpServerHandler());
  23. }
  24. });
  25. ChannelFuture f = b.bind(8080).sync();
  26. f.channel().closeFuture().sync();
  27. } finally {
  28. workerGroup.shutdownGracefully();
  29. bossGroup.shutdownGracefully();
  30. }
  31. }
  32. }
  33. class TcpServerHandler extends ChannelInboundHandlerAdapter {
  34. @Override
  35. public void channelRead(ChannelHandlerContext ctx, Object msg) {
  36. String line = (String) msg;
  37. System.out.println("channelRead:" + line);
  38. }
  39. @Override
  40. public void channelActive(ChannelHandlerContext ctx) {
  41. System.out.println("channelActive");
  42. }
  43. @Override
  44. public void channelInactive(ChannelHandlerContext ctx) {
  45. System.out.println("channelInactive");
  46. }
  47. @Override
  48. public void exceptionCaught(ChannelHandlerContext ctx, Throwable cause) {
  49. cause.printStackTrace();
  50. ctx.close();
  51. }
  52. }
  1. SSL/TLS客户端
    这里还是使用Java来写一个SSL/TLS客户端,用来测试以上三个服务器程序。需要注意的是,在上面SSL/TLS流程的介绍中,SSL/TLS握手阶段的第2步服务器会将证书传给客户端,第3步客户端会校验证书的合法性,所以下面的代码首先会让客户端信任openssl生成的证书,才能正确的完成SSL/TLS握手。
    1. public class SSLClient {
    2. public static void main(String args[]) throws Exception {
    3. // 客户端信任改证书,将用于校验服务器传过来的证书的合法性
    4. String certPath = "/Users/wucao/Desktop/ssl/cert.crt";
    5. InputStream inStream = null;
    6. Certificate certificate = null;
    7. try {
    8. inStream = new FileInputStream(certPath);
    9. CertificateFactory cf = CertificateFactory.getInstance("X.509");
    10. certificate = cf.generateCertificate(inStream);
    11. } finally {
    12. if (inStream != null) {
    13. inStream.close();
    14. }
    15. }
    16. KeyStore ks = KeyStore.getInstance(KeyStore.getDefaultType());
    17. ks.load(null, null);
    18. ks.setCertificateEntry("cert", certificate);
    19. TrustManagerFactory tmf = TrustManagerFactory.getInstance("sunx509");
    20. tmf.init(ks);
    21. SSLContext sslContext = SSLContext.getInstance("TLS");
    22. sslContext.init(null, tmf.getTrustManagers(), null);
    23. SSLSocketFactory socketFactory = sslContext.getSocketFactory();
    24. Socket socket = null;
    25. OutputStream out = null;
    26. try {
    27. socket = socketFactory.createSocket("localhost", 8080);
    28. out = socket.getOutputStream();
    29. // 请求服务器
    30. String lines = "床前明月光\r\n疑是地上霜\r\n举头望明月\r\n低头思故乡\r\n";
    31. byte[] outputBytes = lines.getBytes("UTF-8");
    32. out.write(outputBytes);
    33. out.flush();
    34. } finally {
    35. // 关闭连接
    36. out.close();
    37. socket.close();
    38. }
    39. }
    40. }