Yoshiyasu KO

【過去記事】WebViewでJavaScriptと闘ったお話 〜LocalStorageへ認証情報を保存する〜

2018-09-19

※本記事は、はてなブログに掲載していた記事を移行したものです

引き続きWebViewのお話。
WebViewでフロントエンドアクセスする時に、ログイン情報を引き継いだままアクセスしたいという課題がありまして、簡単にできるかなーと思ってたらかなり長い闘いになったので、残していきたいと思います。

実現したいこと

WebViewでフロントエンドにアクセスした時に、アプリ側でのログイン情報(=認証情報)を引き継ぎたい!

困ったこと

WebViewで認証情報(Auth-Key、Access-Token、Clientなど)を添えてアクセスする時は、

WebView#loadUrl(url: String, Header: HashMap<String, String>)

で、認証情報をヘッダに入れてアクセスするのが基本的ですね。
これで簡単にできるじゃーん、とか思ってました。最初はね。

でも罠があったんですね。

ヘッダで渡した認証情報はloadUrl()で渡したURLでのみ有効・・・

・・・へ?

つまり、loadUrl()で読み込んだURLから別のページに遷移すると、認証情報が失われちゃうんですね・・・これは困った。

解決法

WebViewのLocalStorageに認証情報を保存して、ページを遷移する際に都度LocalStorageの認証情報を取得し必要に応じて更新する

LocalStorageは、Cookieなどと同様にブラウザに対して永続的にデータを保存する仕組みです。

① JavaScriptを有効にする

val webView = findViewById<WebView>(R.id.webview)
webView.settings.javaScriptEnabled = true

② WebStorage(LocalStorage)の利用を許可する

webView.settings.domStorageEnabled = true

③ WebViewClient#onPageFinished() でヘッダで送信した認証情報をLocalStorageから取得する。その際ローカルで保存している認証情報と照合させ、もし一致していなかったならLocalStorageを更新する。

private val webViewClient = object : WebViewClient() {

    override fun onPageFinished(view: WebView?, url: String?) {
        super.onPageFinished(view, url)
        if (view != null) {
            // LocalStorageの認証情報の読み込み
            view.evaluateJavascript("""localStorage.getItem("auth-info")""") { resultString ->
                val authKey = "some-authKey"
                val accessToken = "some-accessToken"
                val client = "some-client"
                try {
                    if (resultString != "null") {
                        val json = JSONObject(resultString.toParsableJsonString())
                        val isSameAuthKey = json.getString(KEY_AUTH_KEY) == authKey
                        val isSameAccessToken = json.getString(KEY_ACCESS_TOKEN) == accessToken
                        val isSameClient = json.getString(KEY_CLIENT) == client
                        // ローカルの認証情報と一致したなら何もしない
                        if (isSameAuthKey && isSameAccessToken && isSameClient) return@evaluateJavascript
                    }
                    // LocalStorageへの認証情報の保存
                    val authInfo
                            = """{ "accessToken": "${accessToken}", "authKey": "${authKey}", "client": "${client}" }"""
                    view.evaluateJavascript("""localStorage.setItem("auth-info", JSON.stringify($authInfo))""", null)
                }
                // パースエラー時
                catch (e: JSONException) {
                    // 何かcatch処理
                }
                // getItemで取得した結果のJSON成型失敗時
                catch (e: IndexOutOfBoundsException) {
                    // 何かcatch処理
                }
            }
        }
    }
}

private fun String.toParsableJsonString(): String
        = this.substring(this.indexOf("{"), this.lastIndexOf("}") + 1).replace("""\"""", """"""")

注意が必要なところ

LocalStorageへ認証情報を保存する↓のところ

val authInfo = """{ "accessToken": "${accessToken}", "authKey": "${authKey}", "client": "${client}" }"""
view.evaluateJavascript("""localStorage.setItem("auth-info", JSON.stringify($authInfo))""", null)

JSONで認証情報を保存するので、authInfoをJSON形式のStringにしているが、これをそのまま

view.evaluateJavascript("""localStorage.setItem("auth-info", $authInfo)""", null)

として保存すると、それ以降でLocalStorageから認証情報をJSON形式で取得できなくなってしまう。
(取得しに行った時のレスポンスが[object Object]となってしまいパースできなくなる)

文字列で渡しているんですが、JavaScriptが余計なお節介で勝手にJSONオブジェクトと認識して格納してしまうのが原因のよう。
ですので、一旦これを文字列に変換させる必要が。

そこで、JSON形式のデータを文字列に変換するメソッドであるJSON.stringify()を噛ませてあげることでしっかり文字列として保存され一件落着になります。
これに半日つまずいてたなんて言えない

view.evaluateJavascript("""localStorage.setItem("auth-info", JSON.stringify($authInfo))""", null)

まとめ

  • LocalStorageを使えば認証情報を引き継げるよ!
  • localStorage.getItem()でLocalStorageから値を取得して、localStorage.setItem()で値をセットするよ!
  • localStorage.setItem()するときは、渡した文字列が勝手にJSON形式に変換されて正しく保存されなくなっちゃうので、JSON.stringify()を渡す文字列に嚙ますんだよ!

おわりに

iOSのWebViewはここの認証情報の保存を勝手によしなにやってくれるみたい。
AndroidのWebViewってめんどくs(ry

じゃ、そゆことで〜

参考

↓の記事のおかげで助かりました、ありがとうございました。
https://www.tam-tam.co.jp/tipsnote/javascript/post5978.html