本篇使用Swift 并附上官方文檔
前陣子接了(公司A)一個專案,再加上要畢業(yè)了,學(xué)校各種忙碌,距離上一篇文章也有好大一段時間...,也是因?yàn)檫@個專案碰到了些小問題,才想來寫寫筆記。
一、起因
公司A有個只限內(nèi)網(wǎng)用的公文系統(tǒng)(似乎是用java寫的網(wǎng)頁?),到目前為止都是單純在Windows上的小程式利用『URL Scheme』跟這個系統(tǒng)互相丟接資料,我則是要負(fù)責(zé)寫一個IOS App 跟此系統(tǒng)做一樣的事。但我是承包的,無法在公司A的內(nèi)網(wǎng)下測試,經(jīng)過一番討論,他們決定將系統(tǒng)做個測試用的灌在windows虛擬機(jī)上,我在Mac上執(zhí)行就等于我跟這個系統(tǒng)相同內(nèi)網(wǎng)。
二、前置測試 ???
假設(shè)各個內(nèi)網(wǎng)ip如下:
(A)Mac:192.168.1.1
(B)Mac上的Windows虛擬機(jī):192.168.1.2
(C)實(shí)機(jī)Iphone:192.168.1.3
(D)系統(tǒng)網(wǎng)址:https://192.168.1.2:8888/Domain
A ping B,C -> OK ??、 B ping A,C -> OK ??
A 預(yù)覽 D -> OK ??、 B 預(yù)覽 D -> OK ??
接下來讓我們看看我要說的 C 預(yù)覽 D 的狀況
三、問題一 (UIWebView 預(yù)覽 D) ??
-
第一種嘗試是利用WebView, 為什么首選它呢?
因?yàn)闃I(yè)主不希望在兩個App之間做過多的切換(一整個流程下來可能跳轉(zhuǎn)兩~三次),也不希望使用者在下次開啟Safari時,還停留在系統(tǒng)的頁面。
-
NSURLErrorDomain: -1003?
WebView就在我LoadRequest時跳出這么一個錯誤,看到ErrorDomain時,我也沒有多想,就直接認(rèn)為應(yīng)該是我DNS沒設(shè)定好之類的問題,于是又重開了虛擬機(jī),重啟站臺...!@#!$????
-
Safari可開啟?
會突然使用Safari是因?yàn)槲也幌胍恢敝匦翨uild,內(nèi)心想說結(jié)果一樣,沒想到,跳出來一個這樣的提示:
Safari開啟系統(tǒng)按下"Continue"后,就顯示出系統(tǒng)的頁面了!
-
不信任的的憑證
有了這個彈窗的提醒,我才明白問題的癥結(jié)點(diǎn)原來是“憑證”,原來是之前IOS9的新設(shè)定:App Transport Security (ATS)
基本上ATS就是為了要確保你的App更安全,會擋兩樣?xùn)|西一個就是沒有Security的Http一個是不信任憑證的Https,不會無意間跑到釣魚網(wǎng)站之類的。
三、問題一解法??
-
設(shè)定Info.plist
第一個方法最簡單就是直接關(guān)掉ATS,直接在Info.plist增加以下其中一對Key Value Pair
1.Allow Arbitary Loads (NSAllowArbitrayLoads)Info.plist
用途是?解除所有連線的ATS限制,這里指的所有連線包括URLRequest,URLConnection,URLSession,UIWebView....等。但是官方文件里也很表明寫了,只要Allow Arbitary Loads這個值被設(shè)為True,就沒有辦法通過上架審核,所以我不采用此方法。
2.Exception Domain (NSExceptionDomains)
用途是?排除某個Domain使其不受ATS的限制。
個人認(rèn)為這個Key比較適合拿來使用,如果你明確知道自己會在某個Domain之下的話。
這個Key我不知道會不會影響上架?
-
HTTPS Server Trust Evaluation
另一種方法就是實(shí)作官方文檔里的『Trust Customization for Specific APIs』,也就是自定義某個要求的憑證檢查
1.URLSession
依照官方文檔,我們需要實(shí)作 urlSession(_:task:didReceive:completionHandler:),如果憑證無法驗(yàn)證,會發(fā)出一個Challenge供你判斷你要拒絕這個連線還是提供一個憑證來解決:
-
遵守URLSessionDelegate
let session = Foundation.URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil) -
處理 - 『Challenge』
challenge : URLAuthenticationChallenge
Challenge這個詞并不是ios或CocoaFrameWork才有的,而是互聯(lián)網(wǎng)中伺服器向使用者端發(fā)出的『Challenge–response authentication』,是一個用來驗(yàn)證用戶或網(wǎng)絡(luò)提供者的協(xié)議,會要求使用者回傳一些資訊,帳號、密碼、憑證...等,在下面會說說有哪些。
https://developer.apple.com/reference/foundation/urlauthenticationchallenge
func urlSession(_ session: URLSession, didReceive challenge: URLAuthenticationChallenge, completionHandler: @escaping (URLSession.AuthChallengeDisposition, URLCredential?) -> Swift.Void)
{
//1
let protectionspace = challenge.protectionSpace
//2
let authMethod = protectionspace.authenticationMethod
if authMethod == NSURLAuthenticationMethodServerTrust
{
//3
let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
//4
let input:URLSession.AuthChallengeDisposition = .useCredential
//5
completionHandler(input, credential)
}
}
```1.***Challenge.protectionSpace*** : URLProtectionSpace -> 包含了這個 authentication request的host,port...等,讓你判斷該用什么**credential(證書)**應(yīng)付。 2.***authenticationMethod*** : 這是我剛剛提到的,伺服器要求認(rèn)證的方法,這個方法就會決定接下來要做的事,像是:NSURLAuthenticationMethodHTTPBasic:要求使用者回傳帳號跟密碼,而我們要處理的是NSURLAuthenticationMethodServerTrust,要求使用者回傳一個憑證。 3.***URLCredential*** : 這個類有幾種差異滿大的Init(),主要就是看authenticationMethod來決定你要回傳的是什么。我們這次要做的是憑證的處理,所以用第二個建構(gòu)。 >https://developer.apple.com/reference/foundation/urlprotectionspace/nsurlprotectionspace_authentication_methods 4.URLSession.***AuthChallengeDisposition*** : 表示行為,包括要使用憑證、取消此次要求、執(zhí)行預(yù)設(shè)動作...等列舉,我們要用憑證所以用.useCredential。 5.利用completionHandle告知結(jié)果:(使用憑證,這個憑證)
2.UIWebView - 回歸正題我們要用UIWebView,但官方文檔即提到無法自定義Https server trust,但我們還是要用上面的方法解決。棘手的地方是UIWebView只能單純LoadingRequest跟管理URLSession,必須繞道而行。
-
實(shí)作UIWebViewDelegate記住失敗的Request,并且建立一個URLConnection。
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool
{
LastRequest = request
return true
}
func webView(_ webView: UIWebView, didFailLoadWithError error: Error)
{
FailRequest = LastRequest
if(FailRequest != nil)
{
let _:NSURLConnection = NSURLConnection(request: FailRequest! , delegate: self)!
}
}
```
* 雖然剛剛講的是URLSession,但兩個用法其實(shí)是相同的,所以我們遵守NSURLConnectionDelegate,并在最后重新Loading一次Request,這樣同個Request就可以通了。
```swift
func connection(_ connection: NSURLConnection, willSendRequestFor challenge: URLAuthenticationChallenge)
{
if challenge.protectionSpace.authenticationMethod == NSURLAuthenticationMethodServerTrust {
print("send credential Server Trust")
let credential = URLCredential(trust: challenge.protectionSpace.serverTrust!)
challenge.sender!.use(credential, for: challenge)
}
else{
challenge.sender!.performDefaultHandling!(for: challenge)
}
connection.cancel()
SystemWebView.loadRequest(FailRequest!)
}
```





