本篇使用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就是为了要确保你的App更安全,会挡两样东西一个就是没有Security的Http一个是不信任凭证的Https,不会无意间跑到钓鱼网站之类的。
三、问题一解法📖
-
设定Info.plist
第一个方法最简单就是直接关掉ATS,直接在Info.plist增加以下其中一对Key Value Pair
但是官方文件里也很表明写了,只要Allow Arbitary Loads这个值被设为True,就没有办法通过上架审核,所以我不采用此方法。
2.Exception Domain (NSExceptionDomains)
个人认为这个Key比较适合拿来使用,如果你明确知道自己会在某个Domain之下的话。
这个Key我不知道会不会影响上架?
-
HTTPS Server Trust Evaluation
另一种方法就是实作官方文档里的『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
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!)
}
```