Java Nio(五)Java Nio实现HTTPS请求

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位

Enter keystore password:
Keystore password is too short - must be at least 6 characters
Enter keystore password:
Re-enter new password:

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

What is your first and last name?
  [Unknown]:  XXX.XX.com
What is the name of your organizational unit?
  [Unknown]:  XX
What is the name of your organization?
  [Unknown]:  XX
What is the name of your City or Locality?
  [Unknown]:  Dalian
What is the name of your State or Province?
  [Unknown]:  CN
What is the two-letter country code for this unit?
  [Unknown]:  CN
Is CN=XXX.com, OU=XX, O=XX, L=Dalian, ST=CN, C=CN correct?
  [no]:  y

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

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

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

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

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

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

加解密

SSLEngineResult res = sslEngine.wrap(appWBuffer, packetWBuffer);
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

public static void main(String[] args) throws Exception {
        int port = 443;
        String host = "www.ximalaya.com";
        String path = "/";
        SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(host, port));
        socketChannel.configureBlocking(false);

        SSLContext sslCtx = SSLContext.getInstance("TLS");
        sslCtx.init(null, null, null);
        SSLEngine sslEngine = sslCtx.createSSLEngine(host, port);
        sslEngine.setUseClientMode(true);
        sslEngine.beginHandshake();
        SSLSession sslSession = sslEngine.getSession();
        SSLEngineResult.HandshakeStatus handshakeStatus = sslEngine.getHandshakeStatus();
        ByteBuffer appBuffer = ByteBuffer.allocate(sslSession.getApplicationBufferSize());
        ByteBuffer packetBuffer = ByteBuffer.allocate(sslSession.getPacketBufferSize());

        ByteBuffer appWBuffer = ByteBuffer.allocate(sslSession.getApplicationBufferSize());
        ByteBuffer packetWBuffer = ByteBuffer.allocate(sslSession.getPacketBufferSize());

        while (handshakeStatus != SSLEngineResult.HandshakeStatus.FINISHED && handshakeStatus != SSLEngineResult.HandshakeStatus.NOT_HANDSHAKING) {
            switch (handshakeStatus) {
                case NEED_UNWRAP:
                    socketChannel.read(packetBuffer);
                    packetBuffer.flip();
                    SSLEngineResult res = sslEngine.unwrap(packetBuffer, appBuffer);
                    packetBuffer.compact();
                    handshakeStatus = res.getHandshakeStatus();
                    break;
                case NEED_WRAP:
                    packetWBuffer.clear();
                    res = sslEngine.wrap(appWBuffer, packetWBuffer);
                    handshakeStatus = res.getHandshakeStatus();
                    if (res.getStatus() == SSLEngineResult.Status.OK) {
                        packetWBuffer.flip();
                        while (packetWBuffer.hasRemaining()) {
                            socketChannel.write(packetWBuffer);
                        }
                    }
                    break;
                case NEED_TASK:
                    Runnable task;
                    while ((task = sslEngine.getDelegatedTask()) != null) {
                        new Thread(task).start();
                    }
                    handshakeStatus = sslEngine.getHandshakeStatus();
                    break;
            }
        }

        StringBuilder stringBuilder = new StringBuilder("GET " + path + " HTTP/1.1 \r\n");
        stringBuilder.append("Host: " + host + "\r\n");
        stringBuilder.append("Accept-Encoding: gzip, deflate\r\n");
        stringBuilder.append("\r\n");
        ByteBuffer byteBuffer = ByteBuffer.wrap(stringBuilder.toString().getBytes());

        packetBuffer.clear();
        SSLEngineResult res = sslEngine.wrap(byteBuffer, packetBuffer);
        if (res.getStatus() != SSLEngineResult.Status.OK) {
            throw new RuntimeException("SSL加密失败");
        }
        packetBuffer.flip();

        while (packetBuffer.hasRemaining()) {
            socketChannel.write(packetBuffer);
        }

        int num;
        byte[] body = null;
        int bodyIndex = 0;
        int headerIndex = 0;
        Integer contentLength = null;
        byte[] originHeader = new byte[1024];
        LinkedHashMap<String, List<String>> head = null;
        appBuffer.clear();
        packetBuffer.clear();
        while ((num = socketChannel.read(packetBuffer)) > -2) {
            packetBuffer.flip();
            do {
                res = sslEngine.unwrap(packetBuffer, appBuffer);
            } while (res.getStatus() == SSLEngineResult.Status.OK);
            packetBuffer.compact();
            for (int i = 0; i < appBuffer.position(); i++) {
                byte b = appBuffer.get(i);
                if (head == null) {
                    originHeader[headerIndex++] = b;
                    if (originHeader.length == headerIndex) {
                        originHeader = byteExpansion(originHeader, 1024);
                    }
                    if (originHeader[headerIndex - 1] == '\n' && originHeader[headerIndex - 2] == '\r' && originHeader[headerIndex - 3] == '\n' && originHeader[headerIndex - 4] == '\r') {
                        String headerStr = new String(originHeader);
                        String[] headerList = headerStr.split("\r\n");
                        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())));
                        contentLength = Optional.ofNullable(head.get("Content-Length")).map(c -> Integer.parseInt(c.get(0))).orElse(-1);
                    }
                } else {
                    Integer finalContentLength = contentLength;
                    body = Optional.ofNullable(body).orElseGet(() -> new byte[finalContentLength]);
                    body[bodyIndex++] = b;
                    if (bodyIndex == contentLength) {
                        num = -2;
                        break;
                    }
                }
            }
            if (num < 0) {
                socketChannel.close();
                System.out.println(new String(originHeader));
                System.out.println(new String(uncompress(body)));
                return;
            }
            appBuffer.clear();
        }
    }

需要注意

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

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值