上週出現一個問題,使用集團SDK中的WebView不能打開某個網頁了,打印Log javax.net.ssl.SSLPeerUnverifiedException:*** not vertified。這個錯誤查了一下午,在最終回家的時候找到了答案,記錄下來,是個借鑑,由於項目是使用的集團的網絡庫,代碼不能泄露,這裏就用okhttp進行替換。java
什麼是https,這個概念感受已經爛大街了,沒什麼想說的,但既然標題都給到這了,情緒也到了,好像不說點又有點感受不對,簡單說一下編程
先說傳統http有什麼缺陷:api
(1)數據明文傳播,內容易被竊取監聽安全
(2)不驗證通訊方的身份,遭遇假裝。bash
(3)沒法證實報文完整性,可能被篡改。(好比中間人攻擊,你用的charles就是這樣)服務器
上述及時http的缺陷也是https要解決的問題微信
(1)利用混合加密方式(非對稱加密用來傳遞祕鑰,對稱加密使用傳遞的祕鑰對數據進 行加密),確保數據內容加密。網絡
(2)經過第三方權威機構頒發的證書(包括服務器信息,服務端公鑰,以及經過第三方數字證書頒發機構使用其私鑰對服務器信息,服務器公鑰的hash信息摘要進行的數字簽名)來保證通訊方的身份以及報文是否被修改。session
因此HTTP+加密+完整性保護+認證=HTTPSapp
https的實現其實就是在TCP與HTTP協議層次之間加了一個SSL層。如圖
根據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就會拋錯,最後是對證書的校驗。
回到最開始的那個問題,集團的WebView庫使用了集團的網路庫,咱們的項目也使用了集團的網絡庫,在進行網絡庫設置時,傳入了HostnameVerifier進行主機校驗,可是這個校驗中沒有包含WebView中訪問的域名,致使出現問題。
關注個人微信公衆號