61
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

ClassiAdvent Calendar 2020

Day 13

WebサイトからSet-Cookieを返したとき、WKWebViewはそれを永続化するのか(する)

Last updated at Posted at 2020-12-12

お世話になります、 @sussan0416 です。
Classi Advent Calendar 2020 は13日目です。昨日は、 @kitaharamikiya さんによる エンジニアが顧客へ提供したい価値を見つけるまでにやっていること でした。

さて今日は、iOSのWebViewとCookieの話題です。

サーバサイドのエンジニアに説明する気持ちで書きたい

iOS(iPadOS)のアプリでは、Webコンテンツを画面に表示するとき、WKWebViewというViewコンポーネントを使用します。しかしこのWKWebView、iOSアプリエンジニアにとっては曲者というか、サーバサイドのエンジニアにとっても、挙動がわかりにくい・デバッグが難しい対象だなと感じています。

そこで今回は、WKWebViewにおけるCookieの永続化に関する挙動について、この1年で理解したことをまとめておきたいと思います。iOSの具体的な実装の話題は控えめにしつつ、iOSのWKWebViewの挙動をまとめたいと思います。

WKWebViewとは...

WKWebViewとは、iOS(iPadOS)アプリで、Webコンテンツを表示する際に使用するViewオブジェクトです。ブラウザでイメージするとわかりやすいでしょうか、下にイメージ画像をおいておきます。まぁとにかく、シンプルに、Webコンテンツを表示するだけの、ペラペラとしたスクリーンみたいなものです。

ブラウザだと……このあたり!
スクリーンショット 2020-12-12 14.34.23.png

アプリケーションにはいろんな画面要素(UI)がありますが、WebViewは、もう本当にここだけ。

念のため、地味に重要なことなので書いておきますが、このように画面上にWebViewが存在すれば、それはWebViewのインスタンスがあるということです。画面から消えると、当然ながらインスタンスは消えます
(あー……画面に見えていなくても、裏に隠れているとかであれば、インスタンスは残りますが……)

ちなみに、WKWebViewWKというプレフィックスは、iOS SDKのWebKitフレームワークのクラスであることを示しています。

では、WKWebViewの挙動を以下にまとめていきます。

Webサイトから返すSet-Cookieは、永続化される(期限のあるものは)

Webサイトのレスポンスに、Set-Cookieヘッダをつけることがあると思います。このCookieが永続化されているかというと……ちゃんと永続化されています!
このあたりは、普通のブラウザと一緒ですね。

有効期限の有無 永続化 WKWebViewのインスタンスが新たに生成されたとき
あり 永続化される 永続化してあるCookieが、リクエストにセットされる
なし 永続化されない(WKWebViewインスタンスが削除されると消える) リクエストにセットされない1

このように、有効期限が設定されているCookieは、新たにWebViewの画面を開いたときにセットされた状態になります。

有効期限のないセッションオンリーのCookieは、WebViewが画面から消えることで(インスタンスが削除されることで)Cookieは消えてしまいます2

期限切れのCookieは、リクエストにセットされない

アプリ内に永続化される期限付きのCookieですが、当然ながら、期限が切れたCookieはリクエストにセットされません。なお、Cookieデータが削除されるタイミングは不明です。プログラムから明示的に削除することも可能です。

スクリーンショット 2020-12-12 17.21.46.png

Cookieは、アプリ内のLibrary/Cookiesに永続化されている(らしい)

このあたりは…さほど詳しくないのですが、永続化されたCookieは、アプリのサンドボックス内にあるLibrary/Cookiesに永続化されるそうです3。サンドボックスというのは、アプリ固有のディレクトリで、他のアプリからは見ることのできない専用の領域のことです4

ここで、iOSのファイル構成を、ちょっと覗いてみたいと思います5

参考にしたサイトの情報をまとめると、こんな感じのファイル構成になっているようです。

スクリーンショット 2020-12-12 17.49.25.png

Library/Cookiesは安全だが完全ではない

Library/CookiesはCookieの保存場所として、あらゆるアプリが使用しています。この領域は、他のアプリからアクセスできない安全な領域です。ですが、完全なものではありません。……色々調べていくと、アプリのサンドボックス内のデータを抽出する方法もあるにはあるようですので。

また、Cookieの保存場所として、OS領域でありますKeychainを使用しても、これは同じことです。暗号化されるという意味ではより安全とは思いますが。

ちなみに、Libraryディレクトリは、MacやiCloudにバックアップされるようです(ユーザーの指定によるはず)。

アプリを削除すると、Library/Cookiesも削除される

Cookieは、アプリケーションのサンドボックス内に永続化されます。そのため、アプリを削除すると、一緒にCookieも端末から削除されます。
ただし、CookieをOSのKeychain領域に保存するなどしていた場合は、アプリを削除してもKeychain領域にはCookieが残ることになります。ちなみに、Cookieの保存場所としてKeychainを指定することは、デフォルトではできません(おそらく普通はしない)。Keychainに保存する処理を別途実装することになります。

WebViewのリクエストと、ネイティブ側(APIとの通信等)のリクエストは、クライアントが異なる

前提として、WebViewとしての通信(JavaScriptの通信を含む)と、ネイティブアプリケーションとしての通信(例: JSONでやり取りするAPIリクエスト)は、互いに独立しています。要するに、クライアントが違うということです。それを示すように、WebViewの通信と、アプリとしてのAPI通信は、User-Agentが異なります。

クライアント User-Agent(例)
WKWebView Mozilla/5.0 (iPhone; CPU iPhone OS 12_4 like Mac OS X) AppleWebKit/605.1.15 (KHTML, like Gecko)
CFNetwork(iOS SDKのフレームワーク) MyApplication/1 CFNetwork/1128.0.1 Darwin/19.6.

たとえばWebViewからのリクエストに対してSet-Cookieした場合、WebViewを使っている限りはCookieが適用されますが、ネイティブ側からのリクエストに対しては、Cookieが適用されないということになります(逆もしかり)。

「『アプリ』に対してSet-Cookieしたんだから、すべてのリクエストにCookieがついているだろう」と思ってはいけないということです。 :sob:

もしこれを読んでいる人で、**いや〜うちのアプリはWebViewでログインさせてCookie使うんだけど、それ以降はネイティブのリクエストもあるのよ……**なんていうプロジェクトの人がいたら、やっぱりCookieをWebViewとネイティブ間で同期させたくなりますよね。
**基本的には、同期されません。**クライアントを超えてCookieやらセッションやらを同期することはできません。じゃぁどうするか。どちらか一方のクライアントからCookieを抽出して、もう一方のクライアントにセットすることで対応しています。

Set-CookieしたCookieがリクエストに適用されているか、見ることはできる?

  • Library/Cookieが見えにくくされているように、保存されたCookieそのものを見るのは難しく手間がかかります。
  • プロキシアプリを使い通信内容をキャプチャすることで、Cookieがセットされているかを確認することができる場合があります(だいたい見える)。
  • もし開発用ビルドのアプリを使って挙動確認をしているのであれば、端末を接続したMacの方で、Safariのインスペクタを開くとCookieをチェックすることができます(方法

ちなみに、WKWebViewにはプライベートブラウズモードもある

あるよ程度に知っておくと良さそうです。DataStoreの挙動が変わったりするので注意です。
プライベートブラウズの実装方法(iOSエンジニア向け)

まとめ

  • Webサイト側から見たときに気になりそうな、iOSのWebViewのCookieについてまとめました
  • 基本的なCookie挙動はブラウザと変わらず、永続化もされます
  • アプリがWebViewとネイティブ両方で通信する場合、双方でCookieが同期されるわけではないということを、ちょっとだけ意識しておくと良さそうです

以上、この1年でわかったWKWebViewのCookieに関する挙動まとめでした。

それでは失礼します。みなさん良きクリスマスをお迎えください:christmas_tree:
明日は、 @youichiro さんです!


付録(iOSアプリエンジニア向け)

テスト用にアプリを組んでいろいろ試しました。挙動をまとめます。

登場人物

  • WKWebsiteDataStore.httpCookieStore(WebViewのCookie保存場所)
  • HTTPCookieStorage(ネイティブ側のCookie保存場所)
処理 結果
HTTPCookieStorageにCookieをセットする WKWebViewには反映されない
websiteDataStore.httpCookieStoreに、Cookieをセットする 条件付きで、HTTPCookieStorageに同期(後述)
WKWebView使用中に、WebサイトからSet-CookieされたCookie HTTPCookieStorageには同期されない
WKUserScriptを使用して、JavaScript(document.cookie)でCookieをセット HTTPCookieStoreには同期されないが、有効期限のあるものはwebsiteDataStore.httpCookieStoreに永続化されている

WKWebView→HTTPCookieStoreにCookieが同期される条件

WKWebViewをインスタンス化するときに渡すWKWebViewConfigurationに、あらかじめCookieをセットしておくと、WKWebViewがインスタンス化されるときに、HTTPCookieStorageにも同期されているようだった。
インスタンス化されたあとは、同期されない(Set-Cookie無念)

lazy var webView: WKWebView! = {
    let persistentCookie = HTTPCookie(properties: [.name   : "cookie_1_min",
                                                   .domain : "example.com",
                                                   .path   : "/",
                                                   .value  : "1",
                                                   .expires: Date(timeIntervalSinceNow: 60),
                                                   .secure: true])!
    // setCookieする
    let config = WKWebViewConfiguration()
    config.websiteDataStore
        .httpCookieStore
        .setCookie(persistentCookie, completionHandler: nil)

    // ここでconfigを渡すことで、Cookieが同期されるっぽい
    let view = WKWebView(frame: view.bounds, configuration: config)
    // レイアウトの処理は端折る

    // 順序が入れ替わると、同期されない!!!
    view.configuration
        .websiteDataStore
        .httpCookieStore
        .setCookie(HTTPCookie(......), completionHandler: nil)

    return view
}()

override func viewDidLoad() {
    super.viewDidLoad()

    view.addSubview(webView)
    let url = URL(string: "https://〜〜")!
    let request = URLRequest(url: url)
    webView.load(request)
}

WKUserScriptでセットしたCookieは、WebViewとしては永続化されている

そもそもドキュメントへのインジェクションなので、最初のリクエストにはセットされないのが残念ではある。
インジェクトしたスクリプトが実行されたあとは、有効期限のあるものについては永続化されている。
HTTPCookieStorageには同期されない。

lazy var webView: WKWebView! = {
    let script = "document.cookie = 'cookie_from_script=1; domain=example.com; path=/; expires=Sun, 13-December-2020 00:00:00 GMT'"

    let userScript = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: false)

    let userContent = WKUserContentController()
    userContent.addUserScript(userScript)

    let config = WKWebViewConfiguration()
    config.userContentController = userContent

    // ここでconfigを渡すけれど、HTTPCookieStorageにはCookie同期されない
    let view = WKWebView(frame: view.bounds, configuration: config)
    // レイアウトの処理は端折る

    return view
}()

override func viewDidLoad() {
    super.viewDidLoad()
    view.addSubview(webView)

    let url = URL(string: "https://〜〜")!
    let request = URLRequest(url: url)
    webView.load(request)
}

そもそもUserScriptって、拡張機能とかの用途だよねきっと。

以上!

  1. WKProcessPoolインスタンスをWKWebViewのインスタンス同士で共有している場合、WKProcessPoolのインスタンスが消えるまではセッションCookieが維持される。WKProcessPoolは、アプリが複数のWebViewを生成し、WebView同士でセッションを引き継ぎたい場合に使用する。

  2. ちなみに、アプリを一旦閉じる(ホームボタンを押してアプリをバックグラウンドに移動する)だけでは、このCookieは消えません。この場合はアプリが一時停止しているだけで、WebViewのインスタンスは残っているためです。アプリをKill(バックグラウンドのアプリ一覧からアプリを削除)した場合は、当然ながらWebViewインスタンスも削除されるので、有効期限が設定されていないCookieは消えます。

  3. (参照) https://qiita.com/ShingoFukuyama/items/eede79a284c3669846e9#cookiecachecredential%E3%81%9D%E3%81%AE%E4%BB%96web%E3%83%87%E3%83%BC%E3%82%BF%E3%81%AA%E3%81%A9%E3%82%92%E6%B6%88%E3%81%97%E3%81%9F%E3%81%84

  4. (参照)Apple Documentation Archive | File System Basics

  5. (参照)https://www.alphansotech.com/ios-application-files-and-folder-structure

61
37
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
61
37

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?