iOS
Swift

ログイン認証したあとに、WKWebViewでCookieを使ってセッションを保つ方法と失敗例

More than 1 year has passed since last update.

はじめに

ログイン認証が必要なWebViewアプリをWKWebViewを使って作る機会がありました。その際にCookie周りで困ることがあったので、共有のために記事を投稿します。

実現したい仕様

  • ネイティブで作ったログイン画面に認証情報をいれてログインすると、WebページのTOP画面に遷移
  • TOP画面以降は認証状態を保ったまま、WebView内で様々な画面に遷移

Kobito.O5p8sD.png

アプリ側は技術的にはこんな感じでいける??

  • アプリ起動時に、ネイティブで作ったログイン画面を表示
  • 認証情報をリクエストパラメーターとしてログインAPIを叩く
  • ログインAPIでtokenをアプリ内部に保持(tokenは例えばPHPなら、PHPSESSIDに該当)
  • WebViewを扱うViewControllerに遷移して、WKWebViewをinitしてaddSubView
  • init時に、WKWebViewにtokenをCookieとしてsetしておく

※この記事では、サーバーサイドの処理は扱いません

解決方法

ググってみて色々試してみましたが、最終的にはStack Overflowのこの記事に書いてあった内容に落ち着きました。

class WebViewController : UIViewController, WKNavigationDelegate {

    var wkWebView: WKWebView!
    var path: String = "https://yonell.com/top" // WebViewで最初に表示させるページ
    var token: String = "i03b032kos@21lgj" // ログインAPIからの戻り値

    override func viewDidLoad() {
        super.viewDidLoad()

        self.setWkWebView()
        dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), {
            self.wkWebViewLoad(self.path)
        })

    // WKWebViewを初期化してaddSubView
    func setWkWebView() {
        // NSMutableRequestだけでなくて、WKWebView自体にもCookieをセットする
        // ここでsetするおかげで、初回以降もセッションを保つことが出来る
        let userContentController = WKUserContentController()
        let cookieScript = WKUserScript(source: "document.cookie = 'PHPSESSID=\(self.token);path=/';", injectionTime: .AtDocumentStart, forMainFrameOnly: true)
        userContentController.addUserScript(cookieScript)
        let wkWebViewConfig = WKWebViewConfiguration()
        wkWebViewConfig.userContentController = userContentController

        // WKWebViewはStoryboardでSetできないのでソースコードで対応
        // 横幅、高さ、ステータスバーの高さを取得する
        let statusBarHeight: CGFloat! = UIApplication.sharedApplication().statusBarFrame.height
        let width: CGFloat! = self.view.bounds.width
        let height: CGFloat! = self.view.bounds.height

        self.wkWebView = WKWebView(frame: CGRectMake(0, statusBarHeight, width, height - statusBarHeight), configuration: wkWebViewConfig)
        self.wkWebView.navigationDelegate = self
        self.view.addSubview(self.wkWebView)        
    }

    // 初回リクエストにCookieを設定
    // これがないと初回リクエストで認証通らないです
    func wkWebViewLoad(urlString: String) {
        if let url = NSURL(string : urlString) {
            // NSMutableRequestにCookieとしてtokenをset
            let request = NSMutableURLRequest(URL: url, cachePolicy: NSURLRequestCachePolicy.UseProtocolCachePolicy, timeoutInterval: 10.0)
            request.HTTPShouldHandleCookies = false
            request.setValue("PHPSESSID=\(self.token)",forHTTPHeaderField: "Cookie")

             // 初回リクエスト
             // WebViewが表示された後は、基本的にはこのload.Request()は呼び出されない
            self.wkWebView.loadRequest(request)
        }
    }

最終案に落ち着くまで色々試して没になっていった失敗例たち

失敗例1 ページ遷移ごとに二回HTTPリクエストを投げる

一番最初にとりあえず試した案。今思うと、なんというトンデモ(´・ω・`)

どんな案?

最終案同様、NSMutableRequestを作る際に、headerにcookieをセット。
これで初回リクエストは認証が通るが、それ以降は毎回認証が途切れてログイン画面にリダイレクトされてしまい困る。なので、リダイレクト先のログイン画面へのリクエストを送る前にdecidePolicyForNavigationActionでフック。毎回NSMutableRequestでheaderにcookieをセットして同じ最初にリクエストしてたURLを再度リクエストする。

ダメなところ

遷移するたびに二回リクエストを送っている。普通に考えてありえないし、画面遷移する時に想定外のことが起きまくる。

失敗例2 リクエストを送る前にheaderをチェックする

普通に考えて余計なリクエストは送りたくないよね、ということでリクエスト1回で済む方法。

どんな案?

decidePolicyForNavigationActionのなかで、1回目のリクエストのheaderを見る。CookieがついてればそのままdecisionHandler(WKNavigationActionPolicy.Allowさせて、ついてなければ最初のリクエストでやってたようにNSMutableRequstにCookieをつけてあげて、もとのリクエストはdecisionHandler(WKNavigationActionPolicy.Cancel)させる。絶対に認証情報がついたリクエストしか飛ばないのでいけると思いました。思いました。。。

元ネタ

Can I set the cookies to be used by a WKWebView?のuser3589213さんのanswer

func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
    let headerFields = navigationAction.request.allHTTPHeaderFields
    var headerIsPresent = contains(headerFields?.keys.array as! [String], "Cookie")

    if headerIsPresent {
        decisionHandler(WKNavigationActionPolicy.Allow)
    } else {
        let req = NSMutableURLRequest(URL: navigationAction.request.URL!)
        let cookies = yourCookieData
        let values = NSHTTPCookie.requestHeaderFieldsWithCookies(cookies)
        req.allHTTPHeaderFields = values
        webView.loadRequest(req)

        decisionHandler(WKNavigationActionPolicy.Cancel)
    }
}

ダメなところ

これだと確かに動くのですが、methodが毎回GETになってしまいます。この方法でテストしてたら、POSTできない!!というバグからこのことが発覚。なので、下のように、methodとbodyを一回目のリクエストからコピって来るようにしました。。。。が、、、originalRequest.HTTPBodyがなぜかnilになっていて、POSTのときにパラメーターが送れない。

    let originalRequest = navigationAction.request
    req.HTTPbody = originalRequest.HTTPBody
    req.HTTPMethod = originalRequest.HTTPMethod

少し調べてみると、、、
WKWebView ignores NSURLRequest body

Apple Developer Forumsで同じようなことを言っている人がいる。

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler  
{  
    NSURLRequest *request = navigationAction.request;  

    // request.HTTPBody is nil here

request.HTTPBody is nil here...
同じです。

んで 結局どうしたか

NSMutableRequestに毎回つめるんじゃなくて、WKWebViewそのものにcookieをsetすればいいんじゃないかと思いながら、よーくこの記事Can I set the cookies to be used by a WKWebView?を読んでみると、、、mattrさんが回答してる案がよさそういうことに。みんなからも指示されてるし。ネイティブからjsを使ってCookieをsetしてますね。参考に試してみたら無事に動きました。月並みですが、ドキュメントはよく読もうと改めて思い知りました。英語だからって適当読みはよくない。。。

最後に

調べてみるともっと複雑なことを実現されてる方がいるなか、こんなシンプルなやつではまってしまってました。同じようにWKWebViewを使ってる方、もっといい案などあればぜひぜひ教えて下さい。

参考にしたstackoverflow達

下記の質問でそこそこ議論されていて、こちらを参考にしました。日本語ではいい記事がなかく、英語だからめんどくさいと思って自分で模索してたら時間がかかりましたが、もっと早くきちんと読んでいれば早く解決したのにな、、、と少し反省しています。

Can I set the cookies to be used by a WKWebView?

Losing cookies in WKWebView