おぼえがき

ここには技術に関することを書いていきます。デザイン関連はmediumに書いてます。

ガワネイティブのCookie同期

はじめまして。 僕は普段アウトプットが足りておらず、リスペクトする先輩から「6月中にブログを作って記事を2本書け」と言っていただき、焦りながらなんとか1本目の記事を書きました。 ツッコミ等ございましたら気軽にコメントしていただけると助かります。


現在、WebViewを多用しているiOSアプリを開発しており、複数のWebView間とネイティブのAPI層でのCookieの同期が必要になってきました。 あるアクションをしたあとに特定のCookieを複数のWebViewとネイティブのAPI層に即時に同期させて更新しなければならず、わりと手こずったのでWebViewのCookie周りについてメモを残します。

※ 対応しているバージョンは iOS10 / iOS9 です。

※ iOS11からはこの辺がとてもラクになるらしいです。


複数のWebView間でのCookie同期

同一のWKProcessPoolを使うことで、リアルタイムで各WebViewのCookieが同期されます。 WebView間の同期であればこれでOKです。

class WebViewController: UIViewController {

    static let processPool = WKProcessPool()

    let webview = WKWebView()

    viewDidLoad() {
        super.viewDidLoad()

        // 共通のProcessPoolを設定
        webview.configuration.processPool = sharedProcessPool
    }
}

バグに苛まれて本件を疑ってStackOverflowで自問自答しつつめっちゃ実験したのですがやっぱりちゃんとリアルタイムに同期されていました。 リクエストヘッダのCookieがセキュリティの問題で確認できなかったりして不安になる人もいるかもしれませんが、ここは大丈夫そうです。

WebViewからAPI層へのCookie同期

認証をWebViewで行う場合は認証情報をCookieから抜き出してAPI層に反映する必要があります。 これをやるためには必要なCookie情報を抜き出して保存しておき、API通信時にそのCookieを付与させる必要があります。

自分はこのような形でAPI通信時にCookieを直接指定するようにしました。

// 通信を行ってるリクエスト (URLRequest) のヘッダーに任意のクッキーを追加
let cookie = Cookie(name: "ninsyoID", value: "abc")
urlRequest.setCookieField([cookie])
extension URLRequest {
    
    mutating func setCookieField(_ cookies: [Cookie]) {
        let cookiesString = cookies
            .map { "\($0.name)=\($0.value)" }
            .joined(separator: "; ")
        setValue(cookiesString, forHTTPHeaderField: "Cookie")
    }
}
typealias Cookie = (name: String, value: String)

上記のように直接CookieをsetしなくてもHTTPCookieStorage.sharedに任意のCookieをsetしておけば反映されるそうです。

HTTPCookieStorage.shared.setCookie(cookie)

WebView内の認証から任意のCookieを抜き出す

まず、HttpOnlyCookieかどうかで取得する方法が変わります。

HttpOnly とは、通信のレスポンスヘッダーにしか表示しないというCookieの設定です。これが設定してあるとJavaScriptから見ることができないようになっています。認証などで用いる機密な情報は大抵これが設定してあります。

HttpOnlyじゃないCookieの場合

以下のようにJavaScriptの実行で取得することが可能です。

extension WebView {
    
    /// JavaScriptから得られるのは httpOnly ではないクッキーのみ
    func getCookiesByJavaScript(completion: @escaping ([Cookie]?)->Void) {
        evaluteJavaScript("document.cookie") { res, err in
            guard let res = res as? String else {
                completion(nil)
                return
            }
            var cookies: [Cookie] = []
            let cookieStrings = res.split("; ")
            for cookieStr in cookieStrings {
                let str = cookieStr.split("=")
                if str.count >= 2 {
                    cookies.append((name: str[0], value: str[1]))
                }
            }
            completion(cookies)
        }
    }
}
typealias Cookie = (name: String, value: String)

HttpOnlyなCookieの場合

HttpOnlyCookieJavaScriptから得ることができません。

取得するには WKNavigationDelegate を使ってWebViewへのレスポンスヘッダを監視することで得ることができます。

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
    if
        let url = navigationResponse.response.url,
        let cookies = navigationResponse.cookies(for: url)
    {
        print(cookies)
    }
}
extension WKNavigationResponse {
    
    func cookies(for url: URL) -> [HTTPCookie]? {
        guard
            let response = self.response as? HTTPURLResponse,
            let allHeaderFields = response.allHeaderFields as? [String : String]
        else {
            return nil
        }
        return HTTPCookie.cookies(withResponseHeaderFields: allHeaderFields, for: url)
    }
}

WebView内のJSの通信で得ているHttpOnlyなCookieの場合

HttpOnlyかつ、WebView内のJavaScriptの通信等でそのページがCookieを得ている場合は、そのCookieを取ってくることはできません。 WebView内のJSの通信などで得るCookieはWKNavigationDelegateのレスポンスヘッダにもそのCookieは現れず、 その場合は、WebViewが実行しているJSの通信のレスポンスヘッダを監視する必要がありますが、それもセキュリティ上できないみたいなので、採ることができません。

その他

HTTPCookieStrageとWKProcessPool間で行われる同期

どうやらHTTPCookieStrageとWKProcessPool間では不定期に同期が行われてるそうです。 僕も開発していてそんな感じがしました。 ここはまた実験してみたいと思います。 体感では、即時~30秒で同期されているように感じました。

WKWebView Persistent Storage of Cookies

Getting cookies from NSHTTPCookieStorage.sharedHTTPCookieStorage(). This one seems buggy, apparently the cookies aren’t immediately saved for NSHTTPCookieStorage to find them. People suggest to trigger a save by resetting the process pool, but I don’t know whether that reliably works. You might want to try that out for yourself, though.

WKWebViewの永続化したCookieストレージとしては、NSHTTPCookieStorage.sharedから取り出す方法があるが、これはバグいようだ。どうも(WebViewでクッキーを取得してもそっちには)クッキーが即時に保存されていない。人々はProcessPoolの再代入によってその保存が引き起こせると言っているが、それが信頼できるかどうか私は分からない。

OSから勝手に同期が行われるので、HTTPCookieStorageに意図しない値が入っていた場合にWKProcessPool側へ誤ったCookieが入ってバグを起こすこともあるので注意が必要です。

技術ブログを書いてみてどうだったか

正直、この記事を書こうとしてみたら各知識についてしっかりとした確証がもてず、コードを書いて実験しました。 記事を書くということは書いてある内容に対してそれなりの責任をもたなければいけないという義務感と、間違えてたら恥ずかしいという羞恥心が生まれるので、自信がない知識に関してはちゃんと調べるきっかけになります。特にWebView周りはよくわからない挙動も多いので助かりました。 また、理解していた知識でも、いざ記事にしてみようと思うと意外と頭のなかで整理されてなかったりしたのが整理されました。

実験含め、記事を書くのは少し大変でしたがアウトプットすることで得られるものは様々あると実感しました。 これからもやっていきたいと思います。 よろしくお願いします。