【Swift】WKWebViewでCookieを扱いたいならWKWebSiteDataStoreを使おう!というお話

こんにちは!@cosmeアプリ開発をしている yukit です。

@cosmeサービスはWebとアプリで展開されていますが、アプリの一部の画面ではWebViewを使って既存のWebページを表示させている部分があります。WebView上でWebページの回遊を前提とする場合、ネイティブアプリ側で構築した認証情報などをCookieとしてWebViewに注入し、ネイティブ/WebView間の状態同期を図る必要があります。しかしながら、iOSのWebViewとして推奨されているWKWebViewは、Cookieの取り扱いの脆弱性が顕著で、公式フォーラム上で頻繁にワークアラウンドが議論されていたりします。

私たちのプロジェクトでもこのWKWebViewでのCookieの振る舞いに長く苦労させられてきましたが、やっと一つのベストプラクティスにたどり着くことができました。今回はそこにフォーカスした知見を紹介しようと思います。

何が問題か

WKWebViewにおいて、最初のリクエストに対するレスポンスがリダイレクト且つSet-cookieを伴う場合、リダイレクト先のレンダリングページにそのCookieが渡らないという問題です。(過去にこちらでその詳細が紹介されています)

結論どうすれば良いか

タイトルの通りです。WKWebViewでCookieを取り扱いたい場合はWKWebSiteDataStoreを使いましょう。

let webView = WKWebView()
let cookies: [HTTPCookie]

for cookie in cookies {
  webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
}

webView.load(request)

Cookieをセットする手段が複数ある

Cookieを渡す手段は他にも下記の2つがあり、それぞれ一癖あります。
Swift WKWebView set cookieなどでググると結構上位にこれらの方法が上がっていますので、利用する際は注意が必要です。
私のプロジェクトでは下記2つの方法を併用してネイティブからWebViewへCookieをセットしていました。しかしCookieの状態遷移が安定せず、調査を重ねてWKWebSiteDataStoreを用いる方法に辿り着きました。

Javascript操作でdocument.cookieに渡す

let cookieInfo = "document.cookie='hoge=aaaa; domain=bbbb; path=cccc; expires=dddd:"
let script = WKUserScript(source: cookieInfo, injectionTime: .atDocumentStart, forMainFrameOnly: false)

let controller = WKUserContentController()
controller.addUserScript(script)

let configuration = WKWebViewConfiguration()
configuration.userContentController = controller

let webView = WKWebView(frame: .zero, configuration: configuration)
webView.load(request)

色々頑張ってる感がありますが、要はネイティブからJavascriptを操作してCookieをセットしてます。この手段の問題点は先のリダイレクト&Set-cookieの問題に加え、最初のリクエストページにCookieが一切乗らないことです。上のコード例でいうところのhoge=aaaaは渡りません。しかし2ページ目以降(1ページ目内にある同ドメインのリンクを踏んだり)には乗ります。

URLRequestのsetValueを使う

var request = URLRequest(url: url)
request.setValue("hoge=aaaa", forHTTPHeaderField: "Cookie")

let webView = WKWebView()
webView.load(request)

この方法ではこれから実行しようとしているリクエスト単体にしかCookieが乗りません。つまり前述のJavascript操作の例とは逆で、2ページ目以降にはCookieが渡りません。なので当然リダイレクトを伴った場合もダメです。

複数のWKWebViewインスタンスが同時起動しているときは注意が必要

複数のWKWebViewインスタンスが同時に起動していると、片方のインスタンスにCookieがたまにのりません。厄介なのは「たまに」なところです。2つのWebViewがあるだけのシンプルなアプリで実験しましたが、20~30%の確率で初回アクセスページにCookieが渡りませんでした。

この問題への対処法として、片方のWebViewをChromeで言うところのシークレットモードにしました。

let configuration = WKWebViewConfiguration()
configuration.websiteDataStore = .nonPersistent() // <- 💡
let webView = WKWebView(frame: .zero, configuration: configuration)

WKWebSiteDataStoreはアプリ内に一意の存在であり、複数のWKWebViewインスタンスが共通してそれを参照しています。おそらくその過程で別インスタンスのCookieへの干渉がおきていると思われます。
.nonPersistent()は公式ドキュメントによれば

Creates a new data store object that stores website data in memory, and doesn’t write that data to disk.

「新しいデータストアオブジェクトを生成する」とあります。
シークレットモードにすることでCookieやStorageの共有領域から分離したデータストアオブジェクトを生成・参照することができ、結果、別インスタンスに干渉しなくなった、と予想できます。
補足ですが、残念ながらWKWebSiteDataStoreをシークレットモードではない新たなオブジェクトで複製する手段はありませんでした。私たちのプロジェクトではシークレットモードで事足りましたが、もしCookieやStorageを扱う要件の場合は注意が必要です。

WebViewの乱用は避けたいところ

ニッチなTipsではありますが、どこかの誰かの役に立てばと思います。
が、WebViewを使わなくて良いならそれに越したことはありません。プロジェクトの予算、スケジュール、諸々を理由に「WebViewでカバー」とする選択があるかもしれませんが、(これは個人の所感ですが)アプリの中にWebが存在することで保守担当領域が曖昧になったり、何よりネイティブならではのUI/UXの良さが失われると思います。AppleもWebViewアプリのリリースには特別な申請提出を必須にしていたり、WebViewに対して寛容ではありません。乱用は避けたいところです。