上周出现一个问题,使用集团SDK中的WebView不能打开某个网页了,打印Log javax.net.ssl.SSLPeerUnverifiedException:*** not vertified。这个错误查了一下午,在最终回家的时候找到了答案,记录下来,是个借鉴,因为项目是使用的集团的网络库,代码不能泄露,这里就用okhttp进行替换。
什么是https,这个概念感觉已经烂大街了,没什么想说的,但既然标题都给到这了,情绪也到了,好像不说点又有点感觉不对,简单说一下
先说传统http有什么缺陷:
(1)数据明文传播,内容易被窃取监听
(2)不验证通信方的身份,遭遇伪装。
(3)无法证明报文完整性,可能被篡改。(比如中间人攻击,你用的charles就是这样)
上述及时http的缺陷也是https要解决的问题
(1)利用混合加密方式(非对称加密用来传递秘钥,对称加密使用传递的秘钥对数据进 行加密),确保数据内容加密。
(2)通过第三方权威机构颁发的证书(包括服务器信息,服务端公钥,以及通过第三方数字证书颁发机构使用其私钥对服务器信息,服务器公钥的hash信息摘要进行的数字签名)来保证通信方的身份以及报文是否被修改。
所以HTTP+加密+完整性保护+认证=HTTPS
https的实现其实就是在TCP与HTTP协议层次之间加了一个SSL层。如图
(HTTPS 使用 SSL(Secure Socket Layer) 和 TLS(Transport Layer Security)这两个协议。TSL是以 SSL为原型开发的协议,有时会统一称该协议为 SSL) SSL协议不仅可以被http使用其他应用层协议也可以使用
根据api文档,先对这几个翻译一下
SSLContext:
此类的实例代表SSL协议实现,该实现充当安全套接字工厂或SSLEngines的工厂。此类由一组可选的密钥和信任管理器以及安全随机字节的源初始化。
SSLScoketFactory:
SSLSocket的工厂,SSLSocket继承自Socket,提供SSL协议和TLS协议的安全套接字通信。
TrustManager:信任管理器,我们一般使用它的子接口X509TrustManager,管理X509证书,验证远程安全套接字(x.509标准规定了证书可以包含什么信息,并说明了记录信息的方法)
HostnameVerifier:
以下解释来自阿里编程规约: 在实现的HostnameVerifier子类中,需要使用verify函数效验服务器主机名的合法性,否则会导致恶意程序利用中间人攻击绕过主机名效验。在握手期间,如果URL的主机名和服务器的标识主机名不匹配,则验证机制可以回调此接口实现程序来确定是否应该允许此连接,如果回调内实现不恰当,默认接受所有域名,则有安全风险。
众所周知,Okhttp中真正打开网络连接和发送网络数据是在拦截器里做的,直接看ConnectInterceptor,这是OkHttp中内置的拦截器,用来打开网络链接。
@Override public Response intercept(Chain chain) throws IOException { ...... HttpCodec httpCodec = streamAllocation.newStream(client, chain, doExtensiveHealthChecks); RealConnection connection = streamAllocation.connection(); return realChain.proceed(request, streamAllocation, httpCodec, connection); } 复制代码
无关代码省略, 首先StreamA了location在调用newStream的时候会找到一个健康的Connection,这个就是streamAllocation.connection返回的RealConnection,生成RealConnection对象后会调用它的connect方法
public void connect(int connectTimeout, int readTimeout, int writeTimeout, int pingIntervalMillis, boolean connectionRetryEnabled, Call call, EventListener eventListener) { .... connectSocket(connectTimeout, readTimeout, call, eventListener); establishProtocol(connectionSpecSelector, pingIntervalMillis, call, .... } 复制代码
留下关键代码,首先建立一个tcp的socket连接,因为ssl层进行握手是也是通过tcp连接进行的,所以先建立socket连接,然后再看establishProtocol方法
private void establishProtocol(ConnectionSpecSelector ... connectTls(connectionSpecSelector); ... } 复制代码
这里会调用connectTls,从名字就可以看出这里是进行SSL握手的地方
private void connectTls(ConnectionSpecSelector connectionSpecSelector) throws IOException { Address address = route.address(); SSLSocketFactory sslSocketFactory = address.sslSocketFactory(); boolean success = false; SSLSocket sslSocket = null; try { // Create the wrapper over the connected socket. sslSocket = (SSLSocket) sslSocketFactory.createSocket( rawSocket, address.url().host(), address.url().port(), true /* autoClose */); // Configure the socket's ciphers, TLS versions, and extensions. ConnectionSpec connectionSpec = connectionSpecSelector.configureSecureSocket(sslSocket); if (connectionSpec.supportsTlsExtensions()) { Platform.get().configureTlsExtensions( sslSocket, address.url().host(), address.protocols()); } // Force handshake. This can throw! sslSocket.startHandshake(); // block for session establishment SSLSession sslSocketSession = sslSocket.getSession(); Handshake unverifiedHandshake = Handshake.get(sslSocketSession); // Verify that the socket's certificates are acceptable for the target host. if (!address.hostnameVerifier().verify(address.url().host(), sslSocketSession)) { X509Certificate cert = (X509Certificate) unverifiedHandshake.peerCertificates().get(0); throw new SSLPeerUnverifiedException("Hostname " + address.url().host() + " not verified:" + "/n certificate: " + CertificatePinner.pin(cert) + "/n DN: " + cert.getSubjectDN().getName() + "/n subjectAltNames: " + OkHostnameVerifier.allSubjectAltNames(cert)); } // Check that the certificate pinner is satisfied by the certificates presented. address.certificatePinner().check(address.url().host(), unverifiedHandshake.peerCertificates()); // Success! Save the handshake and the ALPN protocol. String maybeProtocol = connectionSpec.supportsTlsExtensions() ? Platform.get().getSelectedProtocol(sslSocket) : null; socket = sslSocket; source = Okio.buffer(Okio.source(socket)); sink = Okio.buffer(Okio.sink(socket)); handshake = unverifiedHandshake; protocol = maybeProtocol != null ? Protocol.get(maybeProtocol) : Protocol.HTTP_1_1; success = true; } catch (AssertionError e) { if (Util.isAndroidGetsocknameError(e)) throw new IOException(e); throw e; } finally { if (sslSocket != null) { Platform.get().afterHandshake(sslSocket); } if (!success) { closeQuietly(sslSocket); } } } 复制代码
通过代码可以看到,首先通过SSLSocketFactory创建SSLSocket,这个SSLSocketFactory就是我们在构件OkHttpClient的时候传进去的,然后会调用handshake方法进行握手,然后会回HostnameVerifier进行主机验证,这个时候如果返回false就会抛错,最后是对证书的校验。