Fork me on GitHub

WKWebView进阶填坑指南-Swift

Cookie处理

在设置Cookie的时候,我们经常做的是在请求的请求头里添加Cookie,但是这只是把Cookie发送给了服务端,我们本地并没有保存CookieCookie最终要写到WebView的一个Cookie文件目录里面,后续WebView里面自己的发起的请求或者跳转才能在发起请求的时候在对应的域名下面取到Cookie传出去。

Webview加载 H5 页面,实际上是把页面相关的.html、js、css文件下载到本地,然后再加载,这时页面去获取Cookie 的时候,是去本地WebView里的Cookie文件目录里查找,如果没有设置的话肯定就找不到了。所以在设置Cookie的时候,服务端和客户端都要设置。

在使用UIWebView的时候,我们是通过NSHTTPCookieStorage来管理 Cookie的,下面我们给devqiaoyu.tech这个域名添加一个名为userCookie

1
2
3
4
5
6
7
8
9
10
var props = Dictionary<HTTPCookiePropertyKey, Any>()
props[HTTPCookiePropertyKey.name] = "user"
props[HTTPCookiePropertyKey.value] = "admin"
props[HTTPCookiePropertyKey.path] = "/"
props[HTTPCookiePropertyKey.domain] = "devqiaoyu.tech"
props[HTTPCookiePropertyKey.version] = "0"
props[HTTPCookiePropertyKey.originURL] = "devqiaoyu.tech"
if let cookie = HTTPCookie(properties: props) {
HTTPCookieStorage.shared.setCookie(cookie)
}

WKWebView Cookie问题在于WKWebView发起的请求不会自动带上存储于NSHTTPCookieStorage容器中的Cookie

解决办法也很简单,就是在WKWebView发起请求之前,先从NSHTTPCookieStorage读取Cookie,然后手动往URLRequest的请求头里添加一下Cookie

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func getCookie() -> String {
var cookieString = ""
if let cookies = HTTPCookieStorage.shared.cookies {
for cookie in cookies {
if cookie.domain == cookieDomain {
let str = "\(cookie.name)=\(cookie.value)"
cookieString.append("\(str);")
}
}
return cookieString
}

var request = URLRequest(url: URL(string: "https://devqiaoyu.com"))
request.addValue(getCookie(), forHTTPHeaderField: "Cookie")

当服务器页面发生重定向的时候,此时第一次在RequestHeader中写入的Cookie会丢失,还需要对重定向的请求重新做添加Cookie的处理。具体方法请往下看~

上面这么写完了,当页面加载的时候,后端无论是啥语言,都能从请求头里看到Cookie了,但是后端渲染返回页面后,在客户端的WebView里运行的时候,JS 在执行的时候调用document.cookie API 是读取不到Cookie的,所以还得针对客户端Cookie进行处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
var cookieString = ""
if let cookies = HTTPCookieStorage.shared.cookies {
for cookie in cookies {
if cookie.domain == "devqiaoyu.tech" {
let str = "\(cookie.name)=\(cookie.value)"
cookieString.append("document.cookie='\(str);path=/;domain=devqiaoyu.tech';")
}
}
}
let cookieScript = WKUserScript(source: cookieString, injectionTime: .atDocumentStart, forMainFrameOnly: false)
let userContentController = WKUserContentController()
userContentController.addUserScript(cookieScript)

let webViewConfig = WKWebViewConfiguration()
webViewConfig.userContentController = userContentController

let webV = WKWebView(frame: CGRect.zero, configuration: webViewConfig)

客户端Cookie注入实际上就是创建一个 JS 脚本,让WebView去执行,推荐在.atDocumentStart这个时机进行预置静态 JS 的注入。这样WebView在加载后端返回的静态页面的时候,就可以拿到保存着客户端的Cookie了。

注意:document.cookie() 无法跨域设置 Cookie,比如你第一次加载的请求时 www.baidu.com ,在重定向的时候跳转到了 www.google.com ,那么第二个请求就可能因为没有携带 Cookie而无法访问。当然啦,解决办法还是有的,请往下看~

URL拦截

WKWebView中,每一次页面跳转之前,都会调用下面的回调函数:

1
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)

Web 页面重定向问题

重定向问题有两种:

  • 服务器页面重定向,需要对新发起的请求重新种Cookie
  • 本地页面重定向,只要客户端设置了Cookie,那么就不需要处理了

所以如果是服务器页面重定向,那么判断此时Request是否有你要的Cookie没有就Cancel掉,修改Request重新发起。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
{
var shouldCancelLoadURL = false
if let cookie = navigationAction.request.value(forHTTPHeaderField: "Cookie") {
if cookie.contains("user") {
shouldCancelLoadURL = false
} else {
var request = URLRequest(url: URL(string: (navigationAction.request.url?.absoluteString)!)!)
request.addValue(getCookie(), forHTTPHeaderField: "Cookie")
webView.load(request)
shouldCancelLoadURL = true
}
} else {
var request = URLRequest(url: URL(string: (navigationAction.request.url?.absoluteString)!)!)
request.addValue(getCookie(), forHTTPHeaderField: "Cookie")
webView.load(request)
shouldCancelLoadURL = true
}

if shouldCancelLoadURL {
decisionHandler(WKNavigationActionPolicy.cancel)
} else {
decisionHandler(WKNavigationActionPolicy.allow)
}
}

跨域问题

针对跨域的问题,解决办法和上面的方法类似,仅仅是判断条件不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void)
{
var shouldCancelLoadURL = false
if let url = navigationAction.request.url?.absoluteString {
if url.contains("devqiaoyu.tech") { // 原来的域名
shouldCancelLoadURL = false
} else {
// 重新发起请求,种Cookie
shouldCancelLoadURL = true
}
} else {
// 重新发起请求,种Cookie
shouldCancelLoadURL = true
}

if shouldCancelLoadURL {
decisionHandler(WKNavigationActionPolicy.cancel)
} else {
decisionHandler(WKNavigationActionPolicy.allow)
}
}

假跳转的请求拦截

一种 JS 调用 Native 的通信方案,详细介绍可以看从零收拾一个hybrid框架(一)– 从选择JS通信方案开始。下面内容是从该文章内摘录的。

何谓 假跳转的请求拦截 就是由网页发出一条新的跳转请求,跳转的目的地是一个非法的压根就不存在的地址,比如

1
2
3
4
//常规的Http地址
https://wenku.baidu.com/xxxx?xx=xx
//假的请求通信地址
wakaka://wahahalalala/action?param=paramobj

看我下面写的那条假跳转地址,这么一条什么都不是的扯淡地址,直接放到浏览器里,直接扔到WebView里,肯定是妥妥的什么都打不开的,而如果在经过我们改造过的Hybrid WebView里,进行拦截不进行跳转

url 地址分为这么几个部分

  • 协议:也就是 http/https/file 等,上面用了wakaka
  • 域名:上面的 wenku.baidu.comwahahalalala
  • 路径:上面的 xxxx?action?
  • 参数:上面的 xx=xxparam=paramobj

如果我们构建一条假url

  • 用协议与域名当做通信识别
  • 用路径当做指令识别
  • 用参数当做数据传递

客户端会无差别拦截所有请求,真正的 url 地址应该照常放过,只有协议域名匹配的 url 地址才应该被客户端拦截,拦截下来的 url 不会导致 WebView 继续跳转错误地址,因此无感知,相反拦截下来的 url 我们可以读取其中路径当做指令,读取其中参数当做数据,从而根据约定调用对应的 Native 原生代码

以上其实是一种 协议约定 只要 JS 侧按着这个约定协议生成假 url,Native 按着约定协议拦截/读取假 url,整个流程就能跑通。

User-Agent设置

全局设置

就是App内所有Web请求的User-Agent全部被修改。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// UIWebView
let webView = UIWebView(frame: CGRect.zero)
let userAgent = webView.stringByEvaluatingJavaScript(from: "navigator.userAgent")
if let agent = userAgent {
let user = "@\(agent);extra_user_agent"
let dict = ["UserAgent":user]
UserDefaults.standard.register(defaults: dict)
}

// WKWebView
let webV = WKWebView(frame: CGRect.zero)
webV.evaluateJavaScript("navigator.userAgent") { (result, error) in
if let oldAgent = result as? String {
let user = "@\(oldAgent);extra_user_agent"
let dict = ["UserAgent":user]
UserDefaults.standard.register(defaults: dict)
}
}

单个WebView设置

iOS9WKWebView提供了一个非常便捷的属性去更改User-Agent,就是customUserAgent属性。这样使用起来不仅方便,也不会全局更改User-Agent,可惜的是iOS9才有,如果适配iOS8,还是要使用上面的方法。

1
2
3
4
5
6
let webView = UIWebView(frame: CGRect.zero)
let userAgent = webView.stringByEvaluatingJavaScript(from: "navigator.userAgent")
if let agent = userAgent {
let user = "@\(agent);extra_user_agent"
webView.customUserAgent = user
}

参考文章

WKWebView 那些坑

-------------本文结束感谢您的阅读-------------

本文作者:乔羽 / FightingJoey

发布时间:2018年11月15日 - 11:47

最后更新:2018年11月15日 - 18:28

原始链接:https://fightingjoey.github.io/2018/11/15/开发/WKWebView进阶填坑指南-Swift/

许可协议: 署名-非商业性使用-禁止演绎 4.0 国际 转载请保留原文链接及作者。

坚持原创技术分享,您的支持将鼓励我继续创作!