本篇使用Swift 並附上官方文件
前陣子接了(公司A)一個專案,再加上要畢業了,學校各種忙碌,距離上一篇文章也有好大一段時間...,也是因為這個專案碰到了些小問題,才想來寫寫筆記。
一、起因
公司A有個只限內網用的公文系統(似乎是用java寫的網頁?),到目前為止都是單純在Windows上的小程式利用『URL Scheme』跟這個系統互相丟接資料,我則是要負責寫一個IOS App 跟此係統做一樣的事。但我是承包的,無法在公司A的內網下測試,經過一番討論,他們決定將系統做個測試用的灌在windows虛擬機器上,我在Mac上執行就等於我跟這個系統相同內網。
二、前置測試 ?️
假設各個內網ip如下:
(A)Mac:192.168.1.1
(B)Mac上的Windows虛擬機器:192.168.1.2
(C)實機Iphone:192.168.1.3
(D)系統網址:https://192.168.1.2:8888/Domain
A ping B,C -> OK ?、 B ping A,C -> OK ?
A 預覽 D -> OK ?、 B 預覽 D -> OK ?
接下來讓我們看看我要說的 C 預覽 D 的狀況
三、問題一 (UIWebView 預覽 D) ?
-
第一種嘗試是利用WebView, 為什麼首選它呢?
因為業主不希望在兩個App之間做過多的切換(一整個流程下來可能跳轉兩~三次),也不希望使用者在下次開啟Safari時,還停留在系統的頁面。
-
NSURLErrorDomain: -1003?
WebView就在我LoadRequest時跳出這麼一個錯誤,看到ErrorDomain時,我也沒有多想,就直接認為應該是我DNS沒設定好之類的問題,於是又重開了虛擬機器,重啟站臺...!@#!$??
-
Safari可開啟?
會突然使用Safari是因為我不想一直重新Build,內心想說結果一樣,沒想到,跳出來一個這樣的提示:
按下"Continue"後,就顯示出系統的頁面了!
-
不信任的的憑證
有了這個彈窗的提醒,我才明白問題的癥結點原來是“憑證”,原來是之前IOS9的新設定:App Transport Security (ATS)
ATS參考網址:https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW33
基本上ATS就是為了要確保你的App更安全,會擋兩樣東西一個就是沒有Security的Http一個是不信任憑證的Https,不會無意間跑到釣魚網站之類的。
三、問題一解法?
-
設定Info.plist
第一個方法最簡單就是直接關掉ATS,直接在Info.plist增加以下其中一對Key Value Pair
1.Allow Arbitary Loads (NSAllowArbitrayLoads) 用途是?解除所有連線的ATS限制,這裡指的所有連線包括URLRequest,URLConnection,URLSession,UIWebView....等。但是官方檔案裡也很表明寫了,只要Allow Arbitary Loads這個值被設為True,就沒有辦法通過上架稽核,所以我不採用此方法。
2.Exception Domain (NSExceptionDomains)
用途是?排除某個Domain使其不受ATS的限制。 個人認為這個Key比較適合拿來使用,如果你明確知道自己會在某個Domain之下的話。 這個Key我不知道會不會影響上架?官方文件:https://developer.apple.com/library/content/documentation/General/Reference/InfoPlistKeyReference/Articles/CocoaKeys.html#//apple_ref/doc/uid/TP40009251-SW35
-
HTTPS Server Trust Evaluation
https://developer.apple.com/library/content/technotes/tn2232/_index.html#//apple_ref/doc/uid/DTS40012884-CH1-SECWEBVIEW
另一種方法就是實作官方文件裡的『Trust Customization for Specific APIs』,也就是自定義某個要求的憑證檢查
1.URLSession
依照官方文件,我們需要實作 urlSession(_:task:didReceive:completionHandler:),如果憑證無法驗證,會發出一個Challenge供你判斷你要拒絕這個連線還是提供一個憑證來解決:-
遵守URLSessionDelegate
let session = Foundation.URLSession(configuration: sessionConfig, delegate: self, delegateQueue: nil) 複製程式碼
-
處理 - 『Challenge』
challenge : URLAuthenticationChallenge
Challenge這個詞並不是ios或CocoaFrameWork才有的,而是網際網路中伺服器向使用者端發出的『Challenge–response authentication』,是一個用來驗證使用者或網路提供者的協議,會要求使用者回傳一些資訊,帳號、密碼、憑證...等,在下面會說說有哪些。
https://developer.apple.com/reference/foundation/urlauthenticationchallenge
https://zh.wikipedia.org/zh-cn/CHAP
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(證照)**應付。 2.***authenticationMethod*** : 這是我剛剛提到的,伺服器要求認證的方法,這個方法就會決定接下來要做的事,像是:NSURLAuthenticationMethodHTTPBasic:要求使用者回傳帳號跟密碼,而我們要處理的是NSURLAuthenticationMethodServerTrust,要求使用者回傳一個憑證。 3.***URLCredential*** : 這個類有幾種差異滿大的Init(),主要就是看authenticationMethod來決定你要回傳的是什麼。![image.png](http://upload-images.jianshu.io/upload_images/3776017-a0fe3c2e078cbed5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)我們這次要做的是憑證的處理,所以用第二個建構。 >https://developer.apple.com/reference/foundation/urlprotectionspace/nsurlprotectionspace_authentication_methods 4.URLSession.***AuthChallengeDisposition*** : 表示行為,包括要使用憑證、取消此次要求、執行預設動作...等列舉,我們要用憑證所以用.useCredential。 5.利用completionHandle告知結果:(使用憑證,這個憑證) 複製程式碼
2.UIWebView - 迴歸正題我們要用UIWebView,但官方文件即提到無法自定義Https server trust,但我們還是要用上面的方法解決。
棘手的地方是UIWebView只能單純LoadingRequest跟管理URLSession,必須繞道而行。 -
-
實作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,但兩個用法其實是相同的,所以我們遵守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!)
}
```
複製程式碼