Java通过SSLEngine与NIO实现HTTPS访问
Java使用NIO进行HTTPS协议访问的时候,离不开SSLContext和SSLEngine两个类。我们只需要在Connect操作、Connected操作、Read和Write操作中加入SSL相关的处理即可。
一、连接服务器之前先初始化SSLContext并设置证书相关的操作。
1 public void Connect(String host, int port) { 2 mSSLContext = this.InitSSLContext(); 3 super.Connect(host, port); 4 }
在连接服务器前先创建SSLContext对象,并进行证书相关的设置。如果服务器不是使用外部公认的认证机构生成的密钥,可以使用基于公钥CA的方式进行设置证书。如果是公认的认证证书一般只需要加载Java KeyStore即可。
1.1 基于公钥CA
1 public SSLContext InitSSLContext() throws NoSuchAlgorithmException{ 2 // 创建生成x509证书的对象 3 CertificateFactory caf = CertificateFactory.getInstance("X.509"); 4 // 这里的CA_PATH是服务器的ca证书,可以通过浏览器保存Cer证书(Base64和DER都可以) 5 X509Certificate ca = (X509Certificate)caf.generateCertificate(new FileInputStream(CA_PATH)); 6 KeyStore caKs = KeyStore.getInstance("JKS"); 7 caKs.load(null, null); 8 // 将上面创建好的证书设置到仓库里面,前面的`baidu-ca`只是一个别名可以任意不要出现重复即可。 9 caKs.setCertificateEntry("baidu-ca", ca); 10 TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); 11 tmf.init(caKs); 12 // 最后创建SSLContext,将可信任证书列表传入。 13 SSLContext context = SSLContext.getInstance("TLSv1.2"); 14 context.init(null, tmf.getTrustManagers(), null); 15 return context; 16 }
1.2 加载Java KeyStore
1 public SSLContext InitSSLContext() throws NoSuchAlgorithmException{ 2 // 加载java keystore 仓库 3 KeyStore caKs = KeyStore.getInstance("JKS"); 4 // 把生成好的jks证书加载进来 5 caKs.load(new FileInputStream(CA_PATH), PASSWORD.toCharArray()); 6 // 把加载好的证书放入信任的列表 7 TrustManagerFactory tmf = TrustManagerFactory.getInstance("SunX509"); 8 tmf.init(caKs); 9 // 最后创建SSLContext,将可信任证书列表传入。 10 SSLContext context = SSLContext.getInstance("TLSv1.2"); 11 context.init(null, tmf.getTrustManagers(), null); 12 return context; 13 }
二、连接服务器成功后,需要创建SSLEngine对象,并进行相关设置与握手处理。
通过第一步生成的SSLContext创建SSLSocketFactory并将当前的SocketChannel进行绑定(注:很多别人的例子都没有这步操作,如果只存在一个HTTPS的连接理论上没有问题,但如果希望同时创建大量的HTTPS请求“可能”有问题,因为SSLEngine内部使用哪个Socket进行操作数据是不确定,如果我的理解有误欢迎指正)。
然后调用创建SSLEngine对象,并初始化操作数据的Buffer,然后开始进入握手阶段。(注:这里创建的Buffer主要用于将应用层数据加密为网络数据,将网络数据解密为应用层数据使用:“密文与明文”)。
1 public final void OnConnected() { 2 super.OnConnected(); 3 // 设置socket,并创建SSLEngine,开始握手 4 SSLSocketFactory fx = mSSLContext.getSocketFactory(); 5 // 这里将自己的channel传进去 6 fx.createSocket(mSocketChannel.GetSocket(), mHost, mPort, false); 7 mSSLEngine = this.InitSSLEngine(mSSLContext); 8 // 初始化使用的BUFFER 9 int appBufSize = mSSLEngine.getSession().getApplicationBufferSize(); 10 int netBufSize = mSSLEngine.getSession().getPacketBufferSize(); 11 mAppDataBuf = ByteBuffer.allocate(appBufSize); 12 mNetDataBuf = ByteBuffer.allocate(netBufSize); 13 pAppDataBuf = ByteBuffer.allocate(appBufSize); 14 pNetDataBuf = ByteBuffer.allocate(netBufSize); 15 // 初始化完成,准备开启握手 16 mSSLInitiated = true; 17 mSSLEngine.beginHandshake(); 18 this.ProcessHandShake(null); 19 }
三、进行握手操作
下图简单展示了握手流程,由客户端发起,通过一些列的数据交换最终完成握手操作。要成功与服务器建立连接,握手流程是非常重要的环节,幸好SSEngine内部已经实现了证书验证、交换等步骤,我们只需要在其上层执行特定的行为(握手状态处理)。
3.1 握手相关状态(来自getHandshakeStatus方法)
NEED_WRAP 当前握手状态表示需要加密数据,即将要发送的应用层数据加密输出为网络层数据,并执行发送操作。
NEED_UNWRAP 当前握手状态表示需要对数据进行解密,即将收到的网络层数据解密后成应用层数据。
NEED_TASK 当前握手状态表示需要执行任务,因为有些操作可能比较耗时,如果不希望造成阻塞流程就需要开启异步任务进行执行。
FINISHED 当前握手已完成
NOT_HANDSHAKING 表示不需要握手,这个主要是再次连接时,为了加快速度而跳过握手流程。
3.2 处理握手的方法
以下代码展示了握手流程中的各种状态的处理,主要的逻辑就是如果需要加密就执行加密操作,如果需要执行解密就执行解密操作(废话@_@!)。
1 protected void ProcessHandShake(SSLEngineResult result){ 2 if(this.isClosed() || this.isShutdown()) return; 3 // 区分是来此WRAP UNWRAP调用,还是其他调用 4 SSLEngineResult.HandshakeStatus status; 5 if(result != null){ 6 status = result.getHandshakeStatus(); 7 }else{ 8 status = mSSLEngine.getHandshakeStatus(); 9 } 10 switch(status) 11 { 12 // 需要加密 13 case NEED_WRAP: 14 //判断isOutboundDone,当true时,说明已经不需要再处理任何的NEED_WRAP操作了. 15 // 因为已经显式调用过closeOutbound,且就算执行wrap, 16 // SSLEngineReulst.STATUS也一定是CLOSED,没有任何意义 17 if(mSSLEngine.isOutboundDone()){ 18 // 如果还有数据则发送出去 19 if(mNetDataBuf.position() > 0) { 20 mNetDataBuf.flip(); 21 mSocketChannel.WriteAndFlush(mNetDataBuf); 22 } 23 break; 24 } 25 // 执行加密流程 26 this.ProcessWrapEvent(); 27 break; 28 // 需要解密 29 case NEED_UNWRAP: 30 //判断inboundDone是否为true, true说明peer端发送了close_notify, 31 // peer发送了close_notify也可能被unwrap操作捕获到,结果就是返回的CLOSED 32 if(mSSLEngine.isInboundDone()){ 33 //peer端发送关闭,此时需要判断是否调用closeOutbound 34 if(mSSLEngine.isOutboundDone()){ 35 return; 36 } 37 mSSLEngine.closeOutbound(); 38 } 39 break; 40 case NEED_TASK: 41 // 执行异步任务,我这里是同步执行的,可以弄一个异步线程池进行。 42 Runnable task = mSSLEngine.getDelegatedTask(); 43 if(task != null){ 44 task.run(); 45 // executor.execute(task); 这样使用异步也是可以的, 46 //但是异步就需要对ProcessHandShake的调用做特殊处理,因为异步的,像下面这直接是会导致疯狂调用。 47 } 48 this.ProcessHandShake(null); // 继续处理握手 49 break; 50 case FINISHED: 51 // 握手完成 52 mHandshakeCompleted = true; 53 this.OnHandCompleted(); 54 return; 55 case NOT_HANDSHAKING: 56 // 不需要握手 57 if(!mHandshakeCompleted) 58 { 59 mHandshakeCompleted = true; 60 this.OnHandCompleted(); 61 } 62 return; 63 } 64 }
四、数据的发送与接收
握手成功后就可以进行正常的数据发送与接收,但是需要额外在数据发送的时候进行加密操作,数据接收后进行解密操作。
这里需要额外说明一下,在握手期间也是会需要读取数据的,因为服务器发送过来的数据需要我们执行读取并解密操作。而这个操作在一些其他的例子中直接使用了阻塞的读取方式,我这里则是放在OnRead事件调用后进行处理,这样才符合NIO模型。
4.1 加密操作(SelectionKey.OP_WRITE)
1 protected void ProcessWrapEvent(){ 2 if(this.isClosed() || this.isShutdown()) return; 3 SSLEngineResult result = mSSLEngine.wrap(mAppDataBuf, mNetDataBuf); 4 // 处理result 5 if(ProcessSSLStatus(result, true)){ 6 mNetDataBuf.flip(); 7 mSocketChannel.WriteAndFlush(mNetDataBuf); 8 // 发完成后清空buffer 9 mNetDataBuf.clear(); 10 } 11 mAppDataBuf.clear(); 12 // 如果没有握手完成,则继续调用握手处理 13 if(!mHandshakeCompleted) 14 this.ProcessHandShake(result); 15 }
4.2 解密操作(SelectionKey.OP_READ)
1 protected void ProcessUnWrapEvent(){ 2 if(this.isClosed() || this.isShutdown()) return; 3 do{ 4 // 执行解密操作 5 SSLEngineResult res = mSSLEngine.unwrap(pNetDataBuf, pAppDataBuf); 6 if(!ProcessSSLStatus(res, false)) 7 // 这里不需要对`pNetDataBuf`进行处理,因为ProcessSSLStatus里面已经做好处理了。 8 return; 9 if(res.getStatus() == Status.CLOSED) 10 break; 11 // 未完成握手时,需要继续调用握手处理 12 if(!mHandshakeCompleted) 13 this.ProcessHandShake(res); 14 }while(pNetDataBuf.hasRemaining()); 15 // 数据都解密完了,这个就可以清空了。 16 if(!pNetDataBuf.hasRemaining()) 17 pNetDataBuf.clear(); 18 }
文章来自我的公众号,大家如果有兴趣可以关注,具体扫描关注下图。