目录
  • 一、连接服务器之前先初始化sslcontext并设置证书相关的操作。
    • 1.1 基于公钥ca
    • 1.2 加载java keystore
  • 二、连接服务器成功后,需要创建sslengine对象,并进行相关设置与握手处理。
      • 3.1 握手相关状态(来自gethandshakestatus方法)
      • 4.1加密操作(selectionkey.op_write)
      • 4.2 解密操作(selectionkey.op_read)

    java使用nio进行https协议访问的时候,离不开sslcontext和sslengine两个类。我们只需要在connect操作、connected操作、read和write操作中加入ssl相关的处理即可。

    一、连接服务器之前先初始化sslcontext并设置证书相关的操作。

    public void connect(string host, int port) {
         msslcontext = this.initsslcontext();
         super.connect(host, port);  
     }

    在连接服务器前先创建sslcontext对象,并进行证书相关的设置。如果服务器不是使用外部公认的认证机构生成的密钥,可以使用基于公钥ca的方式进行设置证书。如果是公认的认证证书一般只需要加载java keystore即可。

    1.1 基于公钥ca

    public sslcontext initsslcontext() throws nosuchalgorithmexception{
      // 创建生成x509证书的对象
      certificatefactory caf = certificatefactory.getinstance("x.509");
      // 这里的ca_path是服务器的ca证书,可以通过浏览器保存cer证书(base64和der都可以)
      x509certificate ca = (x509certificate)caf.generatecertificate(new fileinputstream(ca_path));
      keystore caks = keystore.getinstance("jks");
      caks.load(null, null);
      // 将上面创建好的证书设置到仓库里面,前面的`baidu-ca`只是一个别名可以任意不要出现重复即可。
      caks.setcertificateentry("baidu-ca", ca);
      trustmanagerfactory tmf = trustmanagerfactory.getinstance("sunx509");
          tmf.init(caks);
      // 最后创建sslcontext,将可信任证书列表传入。
      sslcontext context = sslcontext.getinstance("tlsv1.2");
      context.init(null, tmf.gettrustmanagers(), null);
      return context;
    }

    1.2 加载java keystore

    public sslcontext initsslcontext() throws nosuchalgorithmexception{
      // 加载java keystore 仓库
      keystore caks = keystore.getinstance("jks");
      // 把生成好的jks证书加载进来
      caks.load(new fileinputstream(ca_path), password.tochararray());
      // 把加载好的证书放入信任的列表
      trustmanagerfactory tmf = trustmanagerfactory.getinstance("sunx509");
      tmf.init(caks);
      // 最后创建sslcontext,将可信任证书列表传入。
      sslcontext context = sslcontext.getinstance("tlsv1.2");
      context.init(null, tmf.gettrustmanagers(), null);
      return context;
    }

    二、连接服务器成功后,需要创建sslengine对象,并进行相关设置与握手处理。

    通过第一步生成的sslcontext创建sslsocketfactory并将当前的socketchannel进行绑定(注:很多别人的例子都没有这步操作,如果只存在一个https的连接理论上没有问题,但如果希望同时创建大量的https请求“可能”有问题,因为sslengine内部使用哪个socket进行操作数据是不确定,如果我的理解有误欢迎指正)。

    然后调用创建sslengine对象,并初始化操作数据的buffer,然后开始进入握手阶段。(注:这里创建的buffer主要用于将应用层数据加密为网络数据,将网络数据解密为应用层数据使用:“密文与明文”)。

    public final void onconnected() {
      super.onconnected();
      // 设置socket,并创建sslengine,开始握手
      sslsocketfactory fx = msslcontext.getsocketfactory();
      // 这里将自己的channel传进去
      fx.createsocket(msocketchannel.getsocket(), mhost, mport, false);
      msslengine = this.initsslengine(msslcontext);
      // 初始化使用的buffer
      int appbufsize = msslengine.getsession().getapplicationbuffersize();
      int netbufsize = msslengine.getsession().getpacketbuffersize();
      mappdatabuf = bytebuffer.allocate(appbufsize);
      mnetdatabuf = bytebuffer.allocate(netbufsize);
      pappdatabuf = bytebuffer.allocate(appbufsize);
      pnetdatabuf = bytebuffer.allocate(netbufsize);
      // 初始化完成,准备开启握手
      msslinitiated = true;
      msslengine.beginhandshake();
      this.processhandshake(null);
    }

    三、进行握手操作

    下图简单展示了握手流程,由客户端发起,通过一些列的数据交换最终完成握手操作。要成功与服务器建立连接,握手流程是非常重要的环节,幸好ssengine内部已经实现了证书验证、交换等步骤,我们只需要在其上层执行特定的行为(握手状态处理)。

    3.1 握手相关状态(来自gethandshakestatus方法)

    need_wrap当前握手状态表示需要加密数据,即将要发送的应用层数据加密输出为网络层数据,并执行发送操作。

    need_unwrap当前握手状态表示需要对数据进行解密,即将收到的网络层数据解密后成应用层数据。

    need_task当前握手状态表示需要执行任务,因为有些操作可能比较耗时,如果不希望造成阻塞流程就需要开启异步任务进行执行。

    finished当前握手已完成

    not_handshaking表示不需要握手,这个主要是再次连接时,为了加快速度而跳过握手流程。

    3.2处理握手的方法

    以下代码展示了握手流程中的各种状态的处理,主要的逻辑就是如果需要加密就执行加密操作,如果需要执行解密就执行解密操作(废话@_@!)。

    protected void processhandshake(sslengineresult result){
     if(this.isclosed() || this.isshutdown()) return;
     // 区分是来此wrap unwrap调用,还是其他调用
     sslengineresult.handshakestatus status;
     if(result != null){
      status = result.gethandshakestatus();
     }else{
      status = msslengine.gethandshakestatus();
     }
     switch(status)
     {
      // 需要加密
      case need_wrap:
          //判断isoutbounddone,当true时,说明已经不需要再处理任何的need_wrap操作了.
          // 因为已经显式调用过closeoutbound,且就算执行wrap,
          // sslenginereulst.status也一定是closed,没有任何意义
          if(msslengine.isoutbounddone()){
            // 如果还有数据则发送出去
            if(mnetdatabuf.position() > 0) {
                mnetdatabuf.flip();
                msocketchannel.writeandflush(mnetdatabuf);
            }
            break;
          }
          // 执行加密流程
          this.processwrapevent();
          break;
      // 需要解密
      case need_unwrap:
       //判断inbounddone是否为true, true说明peer端发送了close_notify,
       // peer发送了close_notify也可能被unwrap操作捕获到,结果就是返回的closed
       if(msslengine.isinbounddone()){
        //peer端发送关闭,此时需要判断是否调用closeoutbound
        if(msslengine.isoutbounddone()){
         return;
        }
        msslengine.closeoutbound();
       }
       break;
      case need_task:
       // 执行异步任务,我这里是同步执行的,可以弄一个异步线程池进行。
       runnable task = msslengine.getdelegatedtask();
       if(task != null){
        task.run();
        // executor.execute(task); 这样使用异步也是可以的,
        //但是异步就需要对processhandshake的调用做特殊处理,因为异步的,像下面这直接是会导致疯狂调用。
       }
       this.processhandshake(null);  // 继续处理握手
       break;
      case finished:
       // 握手完成
       mhandshakecompleted = true;
       this.onhandcompleted();
       return;
      case not_handshaking:
       // 不需要握手
       if(!mhandshakecompleted)
       {
        mhandshakecompleted = true;
        this.onhandcompleted();
       }
       return;
     }
    }

    四、数据的发送与接收

    握手成功后就可以进行正常的数据发送与接收,但是需要额外在数据发送的时候进行加密操作,数据接收后进行解密操作。

    这里需要额外说明一下,在握手期间也是会需要读取数据的,因为服务器发送过来的数据需要我们执行读取并解密操作。而这个操作在一些其他的例子中直接使用了阻塞的读取方式,我这里则是放在onread事件调用后进行处理,这样才符合nio模型。

    4.1加密操作(selectionkey.op_write)

    protected void processwrapevent(){
     if(this.isclosed() || this.isshutdown()) return;
     sslengineresult result = msslengine.wrap(mappdatabuf, mnetdatabuf);
     // 处理result
     if(processsslstatus(result, true)){
      mnetdatabuf.flip();
      msocketchannel.writeandflush(mnetdatabuf);
      // 发完成后清空buffer
      mnetdatabuf.clear();
     }
     mappdatabuf.clear();
     // 如果没有握手完成,则继续调用握手处理
     if(!mhandshakecompleted)
       this.processhandshake(result);
    }

    4.2 解密操作(selectionkey.op_read)

    protected void processunwrapevent(){
     if(this.isclosed() || this.isshutdown()) return;
     do{
      // 执行解密操作
      sslengineresult res = msslengine.unwrap(pnetdatabuf, pappdatabuf);
      if(!processsslstatus(res, false))
          // 这里不需要对`pnetdatabuf`进行处理,因为processsslstatus里面已经做好处理了。
       return;
      if(res.getstatus() == status.closed)
       break;
      // 未完成握手时,需要继续调用握手处理
      if(!mhandshakecompleted)
       this.processhandshake(res);
     }while(pnetdatabuf.hasremaining());
     // 数据都解密完了,这个就可以清空了。
     if(!pnetdatabuf.hasremaining())
       pnetdatabuf.clear();
    }

    到此这篇关于java通过sslengine与nio实现https访问的文章就介绍到这了,更多相关java通过sslengine与nio实现https访问内容请搜索www.887551.com以前的文章或继续浏览下面的相关文章希望大家以后多多支持www.887551.com!