Cookie trong WKWebView
Ra mắt từ iOS 8.0, WKWebView
được sử dụng để thay thế một UIWebView
đã quá già cỗi và thiếu bảo mật. Khác với UIWebView
, tất các các tác vụ của WKWebView
như load content, render hình ảnh, hay xử lý Javascript đều chạy trên một process riêng biệt với process của ứng dụng. Việc này giúp tăng tính bảo mật và hiệu năng, tuy nhiên nó cũng đem lại khá nhiều rắc rối trong việc đồng bộ hoá và quản lí cookie. Trong bài viết ngày hôm nay hãy cùng mình tìm hiểu cách làm việc với cookie và WKWebView
cũng như những điểm cần lưu ý để tránh các vấn đề thường gặp của framework này.
Tạo cookie
Ta sử dụng HTTPCookie
để tạo cookie bằng cách truyền vào một dictionary các thuộc tính của cookie đấy. Một vài thuộc tính cơ bản có thể kể đến như domain, tên cookie, giá trị của cookie, và thời gian hết hạn.
let cookie = HTTPCookie(properties: [
HTTPCookiePropertyKey.domain: "domain name",
HTTPCookiePropertyKey.path: "\",
HTTPCookiePropertyKey.name: "cookie name",
HTTPCookiePropertyKey.value: "cookie value",
HTTPCookiePropertyKey.expires: Date()
])
Set cookie
Để làm việc với cookie và các tính năng nâng cao ta phải khởi tạo WKWebView
từ code bằng cách truyền vào một custom WKWebViewConfiguration
- đây là tập thuộc tính khởi tạo của WKWebView
dùng để quản lý các tác vụ như chạy Javascript script, render hình ảnh, hay xử lý media playback. Lưu ý rằng bạn sẽ không thể thay đổi configuration của một WKWebView
sau khi web view đấy được khởi tạo.
Cách 1: Sử dụng Javascript
Cách đơn giản nhất để set cookie là dùng một đoạn mã Javascript và inject nó vào web view thông qua WKWebViewConfiguration
.
lazy var webView: WKWebView = {
//setup WKWebViewConfiguration
let webConfig = WKWebViewConfiguration()
let cookieScript = WKUserScript(source: "document.cookie = 'COOKIE_NAME=COOKIE_VALUE; domain=DOMAIN_NAME';",
injectionTime: .atDocumentStart, forMainFrameOnly: false)
webConfig.userContentController.addUserScript(cookieScript)
//Khởi tạo webView
let mainWebView = WKWebView(frame: .zero, configuration: webConfig)
mainWebView.navigationDelegate = self
return mainWebView
}()
Ta dùng WKUserScript
để bọc đoạn script chứa cookie và gán nó cho webConfig
. Đoạn script này sẽ được inject vào web view ngay khi document element được tạo ra và trước khi bất kì web content nào được load. Sau đó ta đã có thể khởi tạo web view và load dữ liệu như bình thường. Điểm trừ của phương pháp này là bạn sẽ không thể dùng nó để set HTTP-only cookie.
Cách 2: Sử dụng WKWebsiteDataStore
Đối với HTTP-only cookie ta sẽ sử dụng một phương pháp khác phức tạp hơn. Với phương pháp này ta không thể khởi tạo web view như ở trên mà phải thực hiện theo trình tự ba bước:
- Bước 1: Tạo một
WKWebsiteDataStore
để quản lý cookie - Bước 2: Dùng biến dataStore vừa tạo để set cookie
- Bước 3: Khởi tạo configuration và web view trong completion handler của function
setCookie
var mainWebView: WKWebView!
override func viewDidLoad() {
super.viewDidLoad()
//Bước 1: tạo WKWebsiteDataStore
let websiteDataStore = WKWebsiteDataStore.nonPersistent()
//Bước 2: dùng websiteDataStore vừa tạo để set cookie
websiteDataStore.httpCookieStore.setCookie(YOUR_COOKIE, completionHandler: {
//Bước 3: khởi tạo configuration và webView
let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = websiteDataStore
self.mainWebView = WKWebView(frame: .zero, configuration: configuration)
self.mainWebView.navigationDelegate = self
self.mainWebView.frame = self.view.frame
self.view.addSubview(self.mainWebView)
self.mainWebView.load(YOUR_REQUEST)
})
}
Vì process của ứng dụng và WKWebView
tách biệt với nhau nên để đảm bảo cookie được đồng bộ giữa các process ta phải thực hiện việc setup web view và load request đầu tiên trong completion handler - ngay khi WebKit thông báo rằng cookie đã được set thành công. Ta có thể dùng phương pháp này để set mọi loại cookie chứ không chỉ riêng HTTP-only cookie, tuy nhiên so với việc sử dụng Javascript thì việc chuyển toàn bộ phần setup của web view vào async function khiến logic và luồng code trở nên phức tạp hơn rất nhiều.
Share cookie giữa nhiều WKWebView
Chia sẻ cookie giữa các web view được thực hiện thông qua WKProcessPool
. WKProcessPool
là object đại diện cho bể chứa của một tập hợp các process nhất định. Do web content có thể chạy trên nhiều process riêng lẻ, WebKit sẽ nhóm chúng lại vào những bể chứa (pool) theo một cách có nghĩa và sử dụng các WKProcessPool
chỉ định để thuận tiện cho việc quản lý. Tất cả web view sử dụng chung WKProcessPool
đều sẽ mặc định chia sẻ mọi web content process và cả cookie với nhau. Chính vì vậy để có thể sử dụng lại cookie giữa các web view ta chỉ cần lưu lại một biến WKProcessPool
object và truyền vào configuration trước khi khởi tạo webView.
//Tạo một WKProcessPool object để có thể sử dụng lại
Environment.shared.processPool = WKProcessPool()
//webView1 và webView2 đều sẽ dùng chung cookie
let webConfig = WKWebViewConfiguration()
webConfig.processPool = Environment.shared.processPool
let webView1 = WKWebView(frame: .zero, configuration: webConfig)
let configuration = WKWebViewConfiguration()
configuration.processPool = Environment.shared.processPool
let webView2 = WKWebView(frame: .zero, configuration: configuration)
Gửi cookie kèm request
Để đính kèm cookie với URL request, ta cần gán chúng vào HTTP header fields của request trước khi load với web view
extension WKWebView {
func load(_ request: URLRequest, with cookies: [HTTPCookie]) {
var request = request
let headers = HTTPCookie.requestHeaderFields(with: cookies)
for (name, value) in headers {
request.addValue(value, forHTTPHeaderField: name)
}
load(request)
}
}
//Tạo request và gán cookie bằng extension ở trên
let request = URLRequest(url: YOUR_URL)
mainWebView.load(request, with: [YOUR_COOKIE])
Xoá cookie
Ta có thể sử dụng singleton WKWebsiteDataStore.default()
để xoá cookie một cách chọn lọc
WKWebsiteDataStore.default().httpCookieStore.getAllCookies { (cookies) in
for cookie in cookies {
if cookie.name == YOUR_COOKIE_NAME && cookie.value == YOUR_COOKIE_VALUE {
WKWebsiteDataStore.default().httpCookieStore.delete(cookie, completionHandler: nil)
}
}
}
Cần lưu ý rằng cả getAllCookies
và delete
đều được thực hiện async nên không có gì đảm bảo rằng cookie của web view đã được xoá tại thời điểm bạn load request tiếp theo. Bạn có thể dùng completionHandler
của delete
hoặc DispatchGroup
nếu bắt buộc phải đợi cookie được xoá thành công.
Để xoá toàn bộ cookie và các record của một domain nhất định, ta có thể làm theo các bước dưới đây:
let dataTypes = WKWebsiteDataStore.allWebsiteDataTypes()
let defaultStore = WKWebsiteDataStore.default()
defaultStore.fetchDataRecords(ofTypes: dataTypes) { (records) in
for record in records {
if record.displayName == YOUR_DOMAIN_NAME {
defaultStore.removeData(ofTypes: record.dataTypes, for: [record], completionHandler: {})
}
}
}
Việc chuyển process của WKWebView
ra ngoài ứng dụng giúp hiệu năng và tính bảo mật app được cải thiện rất nhiều. Tuy nhiên, chính đặc tính này cũng khiến quá trình quản lý và làm việc với cookie trở nên khó khăn hơn. Khi set cookie cho WKWebView
, hãy luôn nhớ rằng trình tự thực hiện các bước như được nêu ở phần đầu bài viết là tối quan trọng. Bạn cũng không thể thay đổi configuration của web view sau khi nó được khởi tạo do đó các thuộc tính của WKWebViewConfiguration
phải được cân nhắc thật kĩ lưỡng trong quá trình setup. Lưu ý cuối cùng là bạn không thể sử dụng HTTPCookieStorage
để quản lý WKWebView
cookie.
Mong rằng bài viết này sẽ giúp các bạn tránh được những vấn đề không đáng có khi làm việc với WKWebView
.