当前位置:   article > 正文

Java Nio(五)Java Nio实现HTTPS请求_java nio ssl

java nio ssl

HTTPS其实就是在TCP到应用层之间加了一层加解密的环节。那一层就是SSL。

SSL原理简介

  1. 交换密钥
  2. 利用密钥做对称加解密
  3. 详细原理https://blog.csdn.net/qq_38265137/article/details/90112705

交换密钥(SSL四次握手)

  1. 客户端向服务端发送 Client Hello 消息,这个消息里包含了一个客服端生成一个随机数 Random1、客户端支持的加密套件(Support Ciphers)和 SSL Version 等信息,服务端收到后向客户端发送 Server Hello 消息,这个消息会从 Client Hello 传过来的 Support Ciphers 里确定一份加密套件,这个套件决定了后续加密和生成摘要时具体使用哪些算法,另外还会生成一份随机数 Random2。注意,至此客户端和服务端都拥有了两个随机数(Random1+ Random2)
  2. 服务器将发送4个数据包。1.数字证书和到根CA整个链(使客户端能用服务器证书中的服务器公钥认证服务器)2.服务器密钥交换(可选,这里视密钥交换算法而定)。3.证书请求:服务端可能会要求客户自身进行验证。4.服务器握手完成:第二阶段的结束,第三阶段开始的信号。
  3. 客户端发送3个数据包。1.证书(可选,为了对服务器证明自身,客户要发送一个证书信息,这是可选的,在IIS中可以配置强制客户端证书认证)。2.客户机密钥交换(Pre-master-secret),生成一个随机数Random3使用服务端的公钥进行加密(当然这个数是根据密钥交换算法而定,是发送随机数还是加密参数)。3.证书验证(可选),对预备秘密和随机数进行签名,证明拥有(a)证书的公钥。至此双方都有了Random1,Random2,Random3,双方通过这三个参数和加密方式计算出同一个对称密钥。
  4. 客户端发送一个Change Cipher Spec消息,告诉服务器以后的消息使用新的加密算法。然后,客户端用新的算法、密钥参数发送一个Finished消息,这条消息可以检查密钥交换和认证过程是否已经成功。其中包括一个校验值,对客户端整个握手过程的消息进行校验。服务器同样发送Change Cipher Spec消息和Finished消息。握手过程完成,客户端和服务器可以交换应用层数据进行通信。

加解密

  • 应用层生成的数据,通过上述的对称密钥和协定好的加密算法加密,发送给TCP进行传输
  • TCP接受到的数据,通过上述的对称密钥和协定好的加密算法解密,传递给应用层

 

SSLEngine

对于SSL的操作java已经有了实现SSLEngine,我们也就不用再造轮子了,使用方法在https://nowjava.com/docs/java-api-11/java.base/javax/net/ssl/SSLEngine.html

握手

可以通过SSLEngineResult.HandshakeStatus来判断握手的状态,它会指导我们下一步应该要做什么。

FINISHED:握手完成。
NEED_TASK:需要等待一些task的完成,否则handshake无法继续,出现这个情况时,后续engine的wrap和unwrap方法都会阻塞直到task完成。
NEED_UNWRAP:需要从peer端读取新的数据,否则handshake无法继续。
NEED_UNWRAP_AGAIN:与NEED_UNWRAP类似,但表示从peer读取的数据已经存在于本地了,这个状态下,不需要再重新走一遍网络,只要解析已经接收到的数据就可以了。NOTE:在java8_u151中,并没有这个枚举类型。
NEED_WRAP:需要向peer端发送数据,否则handshake无法继续。
NOT_HANDSHAKING:当前没有处于handshake阶段。

双向认证和自签名证书

对于需要认证客户端的这种情况,要求客户端也得有CA证书,当然我们可以花钱去受信任的机构去申请证书,也可以用java自带的工具生成证书。

执行下面命令

keytool -genkey -keyalg RSA -keysize 2048 -keystore /home/XXX.jks

注意, 此时需要输入此keystore的密码.
密码长度为至少6位

  1. Enter keystore password:
  2. Keystore password is too short - must be at least 6 characters
  3. Enter keystore password:
  4. Re-enter new password:

输入密码后,下一步需输入一些与此key相关的信息.
需要注意的部分是first and last name, 因为它表示应用这个证书的域名

  1. What is your first and last name?
  2. [Unknown]: XXX.XX.com
  3. What is the name of your organizational unit?
  4. [Unknown]: XX
  5. What is the name of your organization?
  6. [Unknown]: XX
  7. What is the name of your City or Locality?
  8. [Unknown]: Dalian
  9. What is the name of your State or Province?
  10. [Unknown]: CN
  11. What is the two-letter country code for this unit?
  12. [Unknown]: CN
  13. Is CN=XXX.com, OU=XX, O=XX, L=Dalian, ST=CN, C=CN correct?
  14. [no]: y

最后一步, 密码置空直接回车即可

  1. Enter key password for <mykey>
  2. (RETURN if same as keystore password):

这样, 在指定的目录下,jks证书已经被生成出来了.

然后我们可以用SSLContext来配置我们的证书(第一个参数)和受信任的证书列表(第二个参数)

sslContext.init(keyManagers(), trustManagers(), null);

如果服务器的ca证书不是受信任的机构颁发的,客户端的受信任的证书列表配置服务器的证书。同理服务器也一样。

加解密

  1. SSLEngineResult res = sslEngine.wrap(appWBuffer, packetWBuffer);
  2. SSLEngineResult res = sslEngine.unwrap(packetBuffer, appBuffer)

通过SSLEngineResult来判断加解密的状态

OK:wrap(发送数据)或者unwrap(接受数据)成功,没有错误。
CLOSED:对于handshake NEED_WRAP操作来说,就是当前端主动关闭了TLS通信;对于NEED_UNWRAP来说,就是peer主动调用了TLS通信,当前端获取到了peer发送过来的close_notify message。
BUFFER_UNDERFLOW(buffer空闲):理论上来说,这个情况不会出现在handshake NEED_WRAP阶段;对于NEED_UNWRAP阶段来说,(1)packetBuffer空间不足,需要扩容(可以初始化大小为sslSession.getPacketBufferSize());(2)packetBuffer读取的数据出现了半包问题,需要继续从socket中read(可以执行packetBuffer.compact(),然后继续读)。
BUFFER_OVERFLOW(buffer溢出):对于NEED_WRAP来说,myNetBuf空间不足,需要扩充或者清空;对于NEED_UNWRAP,peerAppBuf不足,需要扩容或者清空。

示例代码

因为我们是模仿浏览器发送https,所以不用加载我们自己的证书,也不用加受信任的证书。如果是访问私有网站,则需要把其证书放到受信任证书里面,如果对方要求双向认证,那么还需要加载自己的证书,并且把自己的证书发给对方配置为受信任(如果证书是受信任的ca机构颁发的就不用配置受信任证书了)

那么我们改一下上一篇的代码,以Content-Length为例(Transfer-Encoding等于chunked也一样,只是进行的数据的加解密)。这里的代码是非阻塞的,io多路复用的完整代码在github上 https://github.com/cxsummer/net-nio

  1. public static void main(String[] args) throws Exception {
  2. int port = 443;
  3. String host = "www.ximalaya.com";
  4. String path = "/";
  5. SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(host, port));
  6. socketChannel.configureBlocking(false);
  7. SSLContext sslCtx = SSLContext.getInstance("TLS");
  8. sslCtx.init(null, null, null);
  9. SSLEngine sslEngine = sslCtx.createSSLEngine(host, port);
  10. sslEngine.setUseClientMode(true);
  11. sslEngine.beginHandshake();
  12. SSLSession sslSession = sslEngine.getSession();
  13. SSLEngineResult.HandshakeStatus handshakeStatus = sslEngine.getHandshakeStatus();
  14. ByteBuffer appBuffer = ByteBuffer.allocate(sslSession.getApplicationBufferSize());
  15. ByteBuffer packetBuffer = ByteBuffer.allocate(sslSession.getPacketBufferSize());
  16. ByteBuffer appWBuffer = ByteBuffer.allocate(sslSession.getApplicationBufferSize());
  17. ByteBuffer packetWBuffer = ByteBuffer.allocate(sslSession.getPacketBufferSize());
  18. while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED && handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
  19. switch (handshakeStatus) {
  20. case NEED_UNWRAP:
  21. socketChannel.read(packetBuffer);
  22. packetBuffer.flip();
  23. SSLEngineResult res = sslEngine.unwrap(packetBuffer, appBuffer);
  24. packetBuffer.compact();
  25. handshakeStatus = res.getHandshakeStatus();
  26. break;
  27. case NEED_WRAP:
  28. packetWBuffer.clear();
  29. res = sslEngine.wrap(appWBuffer, packetWBuffer);
  30. handshakeStatus = res.getHandshakeStatus();
  31. if (res.getStatus() == SSLEngineResult.Status.OK) {
  32. packetWBuffer.flip();
  33. while (packetWBuffer.hasRemaining()) {
  34. socketChannel.write(packetWBuffer);
  35. }
  36. }
  37. break;
  38. case NEED_TASK:
  39. Runnable task;
  40. while ((task = sslEngine.getDelegatedTask()) != null) {
  41. new Thread(task).start();
  42. }
  43. handshakeStatus = sslEngine.getHandshakeStatus();
  44. break;
  45. }
  46. }
  47. StringBuilder stringBuilder = new StringBuilder("GET " + path + " HTTP/1.1 \r\n");
  48. stringBuilder.append("Host: " + host + "\r\n");
  49. stringBuilder.append("Accept-Encoding: gzip, deflate\r\n");
  50. stringBuilder.append("\r\n");
  51. ByteBuffer byteBuffer = ByteBuffer.wrap(stringBuilder.toString().getBytes());
  52. packetBuffer.clear();
  53. SSLEngineResult res = sslEngine.wrap(byteBuffer, packetBuffer);
  54. if (res.getStatus() != SSLEngineResult.Status.OK) {
  55. throw new RuntimeException("SSL加密失败");
  56. }
  57. packetBuffer.flip();
  58. while (packetBuffer.hasRemaining()) {
  59. socketChannel.write(packetBuffer);
  60. }
  61. int num;
  62. byte[] body = null;
  63. int bodyIndex = 0;
  64. int headerIndex = 0;
  65. Integer contentLength = null;
  66. byte[] originHeader = new byte[1024];
  67. LinkedHashMap<String, List<String>> head = null;
  68. appBuffer.clear();
  69. packetBuffer.clear();
  70. while ((num = socketChannel.read(packetBuffer)) > -2) {
  71. packetBuffer.flip();
  72. do {
  73. res = sslEngine.unwrap(packetBuffer, appBuffer);
  74. } while (res.getStatus() == SSLEngineResult.Status.OK);
  75. packetBuffer.compact();
  76. for (int i = 0; i < appBuffer.position(); i++) {
  77. byte b = appBuffer.get(i);
  78. if (head == null) {
  79. originHeader[headerIndex++] = b;
  80. if (originHeader.length == headerIndex) {
  81. originHeader = byteExpansion(originHeader, 1024);
  82. }
  83. if (originHeader[headerIndex - 1] == '\n' && originHeader[headerIndex - 2] == '\r' && originHeader[headerIndex - 3] == '\n' && originHeader[headerIndex - 4] == '\r') {
  84. String headerStr = new String(originHeader);
  85. String[] headerList = headerStr.split("\r\n");
  86. head = Arrays.stream(headerList).skip(1).filter(h -> h.contains(":")).collect(Collectors.groupingBy(h -> h.split(":")[0].trim(), LinkedHashMap::new, Collectors.mapping(h -> h.split(":")[1].trim(), Collectors.toList())));
  87. contentLength = Optional.ofNullable(head.get("Content-Length")).map(c -> Integer.parseInt(c.get(0))).orElse(-1);
  88. }
  89. } else {
  90. Integer finalContentLength = contentLength;
  91. body = Optional.ofNullable(body).orElseGet(() -> new byte[finalContentLength]);
  92. body[bodyIndex++] = b;
  93. if (bodyIndex == contentLength) {
  94. num = -2;
  95. break;
  96. }
  97. }
  98. }
  99. if (num < 0) {
  100. socketChannel.close();
  101. System.out.println(new String(originHeader));
  102. System.out.println(new String(uncompress(body)));
  103. return;
  104. }
  105. appBuffer.clear();
  106. }
  107. }

需要注意

  • 循环执行sslEngine.unwrap(packetBuffer, appBuffer)的原因是,有时候ssl解码,一次只会解密一部分,如果我们不循环执行的话,就会导致继续去执行read操作,浪费性能。比如访问哔哩哔哩总会出现,数据已经读完了,但是解码的时候会剩下31个字节,然后我们再执行read操作时服务器因为没数据了就会等超过5秒(超时)的时候返回断开,然后客服端会读到-1。也就是说我们浪费了很多时间直到超时。解决办法就再解码一次,就会讲剩下的31个字节解码出来。
  • 如果出现Unsupported record version Unknown-12(任意数字).49(任意数字)或者出现Tag mismatch 报错,就需要检查一下包数据是否完整,即packetBuffer中的数据不完整或者position,limit的位置不对,检查下是否错误的执行了packetBuffer的相关方法。

 

声明:本文内容由网友自发贡献,转载请注明出处:【wpsshop博客】
推荐阅读
相关标签
  

闽ICP备14008679号