はじめに
こんにちは、@rs_tukkiです。
最近外に出られない日々が続いているので、自宅で完結できる趣味が増えた気がします。
さて、今はAndroidのWebViewがアツいみたいなので、それに関連してるようでしてない気がする話を少し。
実装したかった仕様
今回私が開発していたAndroidアプリは全体的にWebViewに依存しており、
まずAPIで認証処理を行ってから、取得したアクセストークンを使ってWebサイトにログインするという流れを取っています。
アプリ(API)「君のサイトにログインしたいんだけど。まずIDとPASS投げるから認証して」
サーバ「オッケー、認証は問題ないね。君用のアクセストークンあげるからこれくっつけてアクセスしてね」
APIでの通信を行う場合は、OkHttp3のCookiejarクラスを実装した独自のCookieストアを使用し、
レスポンスで受け取ったCookieを明示的にCookieManagerに保存する処理を行っていました。
class SharedCookieStore() : CookieJar { // CookieManagerはアプリ全体で共通のインスタンスとして定義する private val cookieManager = CookieManager.getInstance() // レスポンスの内容からCookieを永続化するメソッド override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) { val urlString = url.toString() cookies.map(Cookie::toString) .forEach { cookieManager.setCookie(urlString, it) } sync() } private fun sync() { cookieManager.flush() } }
val apiModule = module { single { SharedCookieStore() } bind CookieJar::class // SharedCookieStoreクラスをインスタンス化 factory { buildHttpClient(get()) } // 上記のSharedCookieStoreクラスを注入 private fun buildHttpClient(cookieJar: CookieJar): OkHttpClient { val dispatcher = Dispatcher() val builder = OkHttpClient.Builder() .dispatcher(dispatcher) .cookieJar(cookieJar) // SharedCookieStoreをOkHttpClientのcookieJarに指定(以下省略)
これでCookieManagerに必要なCookieが保存されましたので、あとはこれを使ってWebViewを開いてあげましょう。
アプリ(WebView)「今から君のサイトにアクセスするね。アクセストークンはこれで っCookie」
サーバ「はいはい、トークン問題ないからアクセスしていいよー」
後は、このアクセストークンの有効期限が切れるまでサイトへのアクセスが可能という仕様にするつもりでした。
実際の仕様
さて、このアクセスしたサイトですが、実は
アクセストークンの有効期限とKeyを保持したまま、定期的にValueだけ更新されるという仕様が組み込まれています。
サーバ「ごめん、今君が使ってるCookie、値が変わったんだわ。次のアクセスからこっち使ってね」
アプリ(WebView)「えっ」
そして、この後再度アクセスしようとすると
アプリ(WebView)「次このページにアクセスするね…えーっとアクセストークンはこれかな? っCookie」
サーバ「んー、そのCookie知らない値だわ。申し訳ないけどログインからやり直して」
アプリ(WebView)「えっ」
というわけで、なぜかWebViewは新しいCookieを使うことなく認証に失敗してしまいました。
原因
(サーバ側で既にあるCookieのValueを更新しただけなら、CookieManagerに自動的に反映されてそうなもんだけどな…)
と思いながらWebViewの実装やらCookieManagerの仕様やらを見返していたのですが、ここに勘違いがありました。
曰く、
Android アプリケーションで cookie を管理する際、デフォルトの動作はアプリとは非同期で揮発性のメモリに展開された値を最大数分(?)程度の遅れで不揮発な領域に書き込みに行くようです。
(中略)
実際には cookie に {user_id: 1} のような値を書き込むわけですが、それが不揮発性メモリと同期される前にアプリケーションを即時に終了させると、次回実行時に(cookie の expire が実行時より後に設定されていようと)不揮発性メモリに残った値を参照してアプリの状態を復元するため、今行ったログイン処理がなかったことにされてしまう可能性が存在する、ということです。
つまり、今貰った新しいValueを裏で永続化する前に次のアクセスを行ってしまったことで、
キャッシュとして残っていた古いValueを使ってしまい不整合が発生した…ということのようです。
どう修正したか
Cookieを永続化できていなかったのが原因なわけですから、通信ごとに永続化してやればいいわけです。
WebViewのFragmentに内部クラスとしてWebViewClientを継承したクラスを作成し、
onPageFinishedのメソッドをオーバーライドして現在のアクセストークンを明示的に永続化します。
class WebViewFragment() { private inner class MyWebViewClient(val isOtherWindow: Boolean) : WebViewClient() { override fun onPageFinished(view: WebView?, url: String?) { // Webviewでの遷移時に現在のアクセストークンを永続化する CookieManager.getInstance().flush() super.onPageFinished(view, url) } }
最初に書いた通り、API通信の時にはきっちり永続化しているんですけどね…
WebViewでも同じ対応が必要という認識がすっぽり抜け落ちていました。反省。
class SharedCookieStore() : CookieJar { // CookieManagerはアプリ全体で共通のインスタンスとして定義する private val cookieManager = CookieManager.getInstance() // レスポンスの内容からCookieを永続化するメソッド override fun saveFromResponse(url: HttpUrl, cookies: MutableList<Cookie>) { val urlString = url.toString() cookies.map(Cookie::toString) .forEach { cookieManager.setCookie(urlString, it) } sync() } // Cookieを永続化する private fun sync() { cookieManager.flush() } }
まとめ
さて、今回はAndroidにおけるCookieの仕様についてお話ししました。
CookieManager.flush()、忘れるなよ!絶対だぞ!
参考
Android - AndroidのWebViewにクッキーを設定したい|teratail
Android の cookie 管理と CookieManager と flush() - happy lie, happy life