Linh Tạ

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.

WKWebView internal
WKWebView chạy trên một process riêng biệt (ảnh lấy từ WWDC 2017)

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()
])


Để 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:


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.

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)


Để đí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])


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ả getAllCookiesdelete đề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.