お世話になります、 @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コンテンツを表示するだけの、ペラペラとしたスクリーンみたいなものです。
アプリケーションにはいろんな画面要素(UI)がありますが、WebViewは、もう本当にここだけ。
念のため、地味に重要なことなので書いておきますが、このように画面上にWebViewが存在すれば、それはWebViewのインスタンスがあるということです。画面から消えると、当然ながらインスタンスは消えます。
(あー……画面に見えていなくても、裏に隠れているとかであれば、インスタンスは残りますが……)
ちなみに、WKWebView
のWK
というプレフィックスは、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データが削除されるタイミングは不明です。プログラムから明示的に削除することも可能です。
Cookieは、アプリ内のLibrary/Cookies
に永続化されている(らしい)
このあたりは…さほど詳しくないのですが、永続化されたCookieは、アプリのサンドボックス内にあるLibrary/Cookies
に永続化されるそうです3。サンドボックスというのは、アプリ固有のディレクトリで、他のアプリからは見ることのできない専用の領域のことです4。
ここで、iOSのファイル構成を、ちょっと覗いてみたいと思います5。
参考にしたサイトの情報をまとめると、こんな感じのファイル構成になっているようです。
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がついているだろう」と思ってはいけないということです。
もしこれを読んでいる人で、**いや〜うちのアプリは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に関する挙動まとめでした。
それでは失礼します。みなさん良きクリスマスをお迎えください
明日は、 @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って、拡張機能とかの用途だよねきっと。
以上!
-
WKProcessPool
インスタンスをWKWebViewのインスタンス同士で共有している場合、WKProcessPool
のインスタンスが消えるまではセッションCookieが維持される。WKProcessPool
は、アプリが複数のWebViewを生成し、WebView同士でセッションを引き継ぎたい場合に使用する。 ↩ -
(参照) 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 ↩
-
(参照)https://www.alphansotech.com/ios-application-files-and-folder-structure ↩