web-dev-qa-db-ja.com

CookieのWKWebView永続ストレージ

ログイン/登録を許可し、Cookieにセッション情報を保存するWebサイトで、ネイティブのiPhoneアプリケーションでWKWebViewを使用しています。 Cookie情報を永続的に保存する方法を見つけようとしているため、アプリを再起動しても、ユーザーは引き続きWebセッションを利用できます。

アプリには2つのWKWebViewsがあり、それらはWKProcessPoolを共有しています。共有プロセスプールから始めます。

WKProcessPool *processPool = [[WKProcessPool alloc] init];

次に、WKWebViewごとに:

WKWebViewConfiguration *theConfiguration = [[WKWebViewConfiguration alloc] init]; 
theConfiguration.processPool = processPool; 
self.webView = [[WKWebView alloc] initWithFrame:frame configuration:theConfiguration];

最初のWKWebViewを使用してログインし、しばらくしてから2番目のWKWebViewにアクションを渡すと、セッションが保持されるため、Cookieが正常に共有されました。ただし、アプリを再起動すると、新しいプロセスプールが作成され、セッション情報が破棄されます。アプリを再起動してもセッション情報を保持する方法はありますか?

30
haplo1384

これは実際には難しいものです。a)いくつかの バグ がまだ解決されていませんApple(私が思う)、b)必要なcookieに依存する、と思う。

これをテストすることはできませんでしたが、いくつかのヒントを示します。

  1. NSHTTPCookieStorage.sharedHTTPCookieStorage()からCookieを取得します。これはバグがあるようです。明らかに、クッキーはNSHTTPCookieStorageがそれらを見つけるためにすぐには保存されません。 People プロセスプールをリセットして保存をトリガーすることを提案しますが、それが確実に機能するかどうかはわかりません。ただし、自分で試してみることをお勧めします。
  2. プロセスプールは、実際にはCookieを保存するものではありません(ただし、正しく指定されたとおりに共有されるかどうかは定義されます)。ドキュメントには、それがWKWebsiteDataStoreであると書かれているので、調べてみます。少なくともfetchDataRecordsOfTypes:completionHandler:を使用してそこからCookieを取得することは可能かもしれません(ただし、それらの設定方法がわからないため、プロセスプールと同じ理由でストアをユーザーのデフォルトに保存することはできないと思います) 。
  3. 必要なCookie(またはその値)を取得できたとしても、私が推測するように復元できない場合は、 here (基本的にはhttprequestを単純に準備する方法を示します)すでに、関連部分:[request addValue:@"TeskCookieKey1=TeskCookieValue1;TeskCookieKey2=TeskCookieValue2;" forHTTPHeaderField:@"Cookie"])。
  4. 他のすべてが失敗した場合は、 this を確認してください。リンクのみの回答を提供するのは良くないことは知っていますが、すべてをコピーすることはできず、完全を期すために追加したいだけです。

最後に、Cookieの種類によっても成功する可能性があると言いました。これは、 この回答 が、サーバーによって設定されたCookieがNSHTTPCookieStorageを介してアクセスできないことを示しているためです。それがあなたに関連しているかどうかはわかりません(しかし、おそらくあなたがセッションを探しているので、サーバーセットCookieが正しいのでしょうか?)、これが他のメソッドが失敗することを意味するかどうかわかりません同じように。

他のすべてが失敗した場合、ユーザー資格情報をどこかに保存することを検討し(キーチェーンなど)、次回のアプリ起動時にそれらを自動的に再認証します。これは、すべてのセッションデータを復元するわけではありませんが、ユーザーがアプリを終了することを考えると、実際には望ましいでしょうか? here のように、注入されたスクリプトを使用して後で使用するために特定の値をキャッチして保存することもできます(明らかに、開始時に設定するためではなく、ある時点で取得する場合があります。もちろん、サイトは動作します)。

少なくとも、この問題を解決するいくつかの新しい方向に向けることができれば幸いです。それはあるべきほど些細なことではないようです(それから、セッションクッキーはセキュリティ関連のものの一種であるため、アプリからそれらを隠すことはAppleによる意識的な設計選択かもしれません...)。

16
Gero

数日間の調査と実験の後、WKWebViewでセッションを管理するソリューションを見つけました。これを回避する方法は他にありませんでした。

最初に、ユーザーのデフォルトでデータを設定および取得するメソッドを作成する必要があります。データと言う場合はNSDataを意味します。ここにメソッドがあります。

+(void)saveDataInNSDefault:(id)object key:(NSString *)key{
    NSData *encodedObject = [NSKeyedArchiver archivedDataWithRootObject:object];
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:encodedObject forKey:key];
    [defaults synchronize];
}

+ (id)getDataFromNSDefaultWithKey:(NSString *)key{
    NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
    NSData *encodedObject = [defaults objectForKey:key];
    id object = [NSKeyedUnarchiver unarchiveObjectWithData:encodedObject];
    return object;
}

WebViewでセッションを維持するために、WebViewとWKProcessPoolシングルトンを作成しました。

- (WKWebView *)sharedWebView {
    static WKWebView *singleton;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        WKWebViewConfiguration *webViewConfig = [[WKWebViewConfiguration alloc] init];
        WKUserContentController *controller = [[WKUserContentController alloc] init];

        [controller addScriptMessageHandler:self name:@"callNativeAction"];
        [controller addScriptMessageHandler:self name:@"callNativeActionWithArgs"];
        webViewConfig.userContentController = controller;
        webViewConfig.processPool = [self sharedWebViewPool];

        singleton = [[WKWebView alloc] initWithFrame:self.vwContentView.frame configuration:webViewConfig];

    });
    return singleton;
}

- (WKProcessPool *)sharedWebViewPool {
    static WKProcessPool *pool;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{

        pool = [Helper getDataFromNSDefaultWithKey:@"pool"];

        if (!pool) {
            pool = [[WKProcessPool alloc] init];
        }

    });
    return pool;
}

ViewDidLoadで、ログインページではないかどうかを確認し、ユーザーのデフォルトからCookieCookieをHttpCookieStoreにロードして、認証を渡すか、それらのCookieを使用してセッションを維持します。

if (!isLoginPage) {
            [request setValue:accessToken forHTTPHeaderField:@"Authorization"];

            NSMutableSet *setOfCookies = [Helper getDataFromNSDefaultWithKey:@"cookies"];
            for (NSHTTPCookie *cookie in setOfCookies) {
                if (@available(iOS 11.0, *)) {

                    [webView.configuration.websiteDataStore.httpCookieStore setCookie:cookie completionHandler:^{}];
                } else {
                    // Fallback on earlier versions
                    [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
                }
            }
        }

そして、リクエストをロードします。

これで、Cookieを使用してWebViewセッションを維持するため、ログインページwebviewで、cookieをhttpCookieStoreからviewDidDisappearメソッドのユーザーデフォルトに保存します。

- (void)viewDidDisappear:(BOOL)animated {

    if (isLoginPage) { //checking if it’s login page.
        NSMutableSet *setOfCookies = [Helper getDataFromNSDefaultWithKey:@"cookies"]?[Helper getDataFromNSDefaultWithKey:@"cookies"]:[NSMutableArray array];
        //Delete cookies if >50
        if (setOfCookies.count>50) {
            [setOfCookies removeAllObjects];
        }
        if (@available(iOS 11.0, *)) {
            [webView.configuration.websiteDataStore.httpCookieStore getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull arrCookies) {

                for (NSHTTPCookie *cookie in arrCookies) {
                    NSLog(@"Cookie: \n%@ \n\n", cookie);
                    [setOfCookies addObject:cookie];
                }
                [Helper saveDataInNSDefault:setOfCookies key:@"cookies"];
            }];
        } else {
            // Fallback on earlier versions
            NSArray *cookieStore = NSHTTPCookieStorage.sharedHTTPCookieStorage.cookies;
            for (NSHTTPCookie *cookie in cookieStore) {
                NSLog(@"Cookie: \n%@ \n\n", cookie);
                [setOfCookies addObject:cookie];
            }
            [Helper saveDataInNSDefault:setOfCookies key:@"cookies"];
        }
    }

    [Helper saveDataInNSDefault:[self sharedWebViewPool] key:@"pool"];
}

注:上記の方法はiOS 11でのみテストされていますが、以前のバージョンでもフォールバックを記述しましたが、それらはテストしませんでした。

これがあなたの問題を解決することを願っています!!! :)

4
Harish Pathak

私はパーティーに少し遅れていますが、人々はこれが便利だと思うかもしれません。回避策はありますが、少し面倒ですが、少なくともAppleダムAPIを修正するまでは、確実に機能する唯一のソリューションです...

言うまでもなく、キャッシュされたCookieをWKWebViewから取得しようとして3日間を費やしました。

私が最初にやろうとしたことは、WKWebView内で実行されていたjavascriptを使用してすべてのCookieを取得し、WKUserContentControllerに渡し、そこでUserDefaultsに保存することです。私のcookieがhttponlyであり、明らかにJavaScriptでそれらを取得できないため、これは機能しませんでした...

サーバー側のページ(私の場合はRuby on Rail)にJavaScript呼び出しを挿入して、Cookieをパラメーターとして挿入することで修正しました。

sendToDevice("key:value")

上記のjs関数は、単にCookieをデバイスに渡すだけです。これが誰かの正気を保つのに役立つことを願っています...

3
Dovydas Rupšys

私はこれに答えるのに少し遅れていますが、既存の答えにいくつかの洞察を加えたいと思います。すでに述べた答え here は、WKWebViewのCookie Persistenceに貴重な情報を既に提供しています。ただし、いくつかの注意事項があります。

  1. WKWebViewNSHTTPCookieStorageではうまく機能しないため、iOS 8、9、10ではUIWebViewを使用する必要があります。
  2. 必ずしもWKWebViewをシングルトンとして保持する必要はありませんが、目的のCookieを再度取得するために毎回WKProcessPoolの同じインスタンスを使用する必要があります。
  3. 最初にsetCookieメソッドを使用してCookieを設定してから、WKWebViewをインスタンス化することをお勧めします。

また、SwiftのiOS 11+ソリューションを強調したいと思います。

let urlString = "http://127.0.0.1:8080"
var webView: WKWebView!
let group = DispatchGroup()

override func viewDidLoad() {
    super.viewDidLoad()
    self.setupWebView { [weak self] in
        self?.loadURL()
    }
}

override func viewDidDisappear(_ animated: Bool) {
    super.viewDidDisappear(animated)
    if #available(iOS 11.0, *) {
        self.webView.configuration.websiteDataStore.httpCookieStore.getAllCookies { cookies in
            self.setData(cookies, key: "cookies")
        }
    } else {
        // Fallback on earlier versions
    }
}

private func loadURL() {
    let urlRequest = URLRequest(url: URL(string: urlString)!)
    self.webView.load(urlRequest)
}

private func setupWebView(_ completion: @escaping () -> Void) {

    func setup(config: WKWebViewConfiguration) {
        self.webView = WKWebView(frame: CGRect.zero, configuration: config)
        self.webView.navigationDelegate = self
        self.webView.uiDelegate = self
        self.webView.translatesAutoresizingMaskIntoConstraints = false
        self.view.addSubview(self.webView)

        NSLayoutConstraint.activate([
            self.webView.leadingAnchor.constraint(equalTo: self.view.leadingAnchor),
            self.webView.trailingAnchor.constraint(equalTo: self.view.trailingAnchor),
            self.webView.topAnchor.constraint(equalTo: self.view.topAnchor),
            self.webView.bottomAnchor.constraint(equalTo: self.view.bottomAnchor)])
    }

    self.configurationForWebView { config in
        setup(config: config)
        completion()
    }

}

private func configurationForWebView(_ completion: @escaping (WKWebViewConfiguration) -> Void) {

    let configuration = WKWebViewConfiguration()

    //Need to reuse the same process pool to achieve cookie persistence
    let processPool: WKProcessPool

    if let pool: WKProcessPool = self.getData(key: "pool")  {
        processPool = pool
    }
    else {
        processPool = WKProcessPool()
        self.setData(processPool, key: "pool")
    }

    configuration.processPool = processPool

    if let cookies: [HTTPCookie] = self.getData(key: "cookies") {

        for cookie in cookies {

            if #available(iOS 11.0, *) {
                group.enter()
                configuration.websiteDataStore.httpCookieStore.setCookie(cookie) {
                    print("Set cookie = \(cookie) with name = \(cookie.name)")
                    self.group.leave()
                }
            } else {
                // Fallback on earlier versions
            }
        }

    }

    group.notify(queue: DispatchQueue.main) {
        completion(configuration)
    }
}

ヘルパーメソッド:

func setData(_ value: Any, key: String) {
    let ud = UserDefaults.standard
    let archivedPool = NSKeyedArchiver.archivedData(withRootObject: value)
    ud.set(archivedPool, forKey: key)
}

func getData<T>(key: String) -> T? {
    let ud = UserDefaults.standard
    if let val = ud.value(forKey: key) as? Data,
        let obj = NSKeyedUnarchiver.unarchiveObject(with: val) as? T {
        return obj
    }

    return nil
}

編集:WKWebView post setCookie呼び出しをインスタンス化することが望ましいと述べました。 setCookieを2回開いたときに、WKWebView完了ハンドラーが呼び出されないという問題に遭遇しました。これはWebKitのバグのようです。したがって、最初にWKWebViewをインスタンス化してから、構成でsetCookieを呼び出す必要がありました。すべてのsetCookie呼び出しが返された後にのみURLをロードしてください。

2
jarora

WKWebViewNSCodingに準拠しているため、NSCoderを使用してwebViewをデコード/エンコードし、NSUserDefaultsなどの別の場所に保存できます。

//return data to store somewhere
NSData* data = [NSKeyedArchiver archivedDataWithRootObject:self.webView];/

self.webView = [NSKeyedUnarchiver unarchiveObjectWithData:data];
0
wj2061

最後に、WKWebViewでセッションを管理するためのソリューションを見つけました。Swift= 4で動作しますが、ソリューションはSwift 3またはobject-C:

class ViewController: UIViewController {

let url = URL(string: "https://insofttransfer.com")!


@IBOutlet weak var webview: WKWebView!

override func viewDidLoad() {

    super.viewDidLoad()
    webview.load(URLRequest(url: self.url))
    webview.uiDelegate = self
    webview.navigationDelegate = self
}}

WKWebviewの拡張機能を作成...

extension WKWebView {

enum PrefKey {
    static let cookie = "cookies"
}

func writeDiskCookies(for domain: String, completion: @escaping () -> ()) {
    fetchInMemoryCookies(for: domain) { data in
        print("write data", data)
        UserDefaults.standard.setValue(data, forKey: PrefKey.cookie + domain)
        completion();
    }
}


 func loadDiskCookies(for domain: String, completion: @escaping () -> ()) {
    if let diskCookie = UserDefaults.standard.dictionary(forKey: (PrefKey.cookie + domain)){
        fetchInMemoryCookies(for: domain) { freshCookie in

            let mergedCookie = diskCookie.merging(freshCookie) { (_, new) in new }

            for (cookieName, cookieConfig) in mergedCookie {
                let cookie = cookieConfig as! Dictionary<String, Any>

                var expire : Any? = nil

                if let expireTime = cookie["Expires"] as? Double{
                    expire = Date(timeIntervalSinceNow: expireTime)
                }

                let newCookie = HTTPCookie(properties: [
                    .domain: cookie["Domain"] as Any,
                    .path: cookie["Path"] as Any,
                    .name: cookie["Name"] as Any,
                    .value: cookie["Value"] as Any,
                    .secure: cookie["Secure"] as Any,
                    .expires: expire as Any
                ])

                self.configuration.websiteDataStore.httpCookieStore.setCookie(newCookie!)
            }

            completion()
        }

    }
    else{
        completion()
    }
}

func fetchInMemoryCookies(for domain: String, completion: @escaping ([String: Any]) -> ()) {
    var cookieDict = [String: AnyObject]()
    WKWebsiteDataStore.default().httpCookieStore.getAllCookies { (cookies) in
        for cookie in cookies {
            if cookie.domain.contains(domain) {
                cookieDict[cookie.name] = cookie.properties as AnyObject?
            }
        }
        completion(cookieDict)
    }
}}

その後、このようなView Controllerの拡張機能を作成します

extension ViewController: WKUIDelegate, WKNavigationDelegate {
func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
   //load cookie of current domain
    webView.loadDiskCookies(for: url.Host!){
        decisionHandler(.allow)
    }
}

public func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
   //write cookie for current domain
    webView.writeDiskCookies(for: url.Host!){
        decisionHandler(.allow)
    }
}
}

urlは現在のURLです。

    let url = URL(string: "https://insofttransfer.com")!
0
Moussa Ndour

広範囲にわたる検索と手動デバッグの後、これらの簡単な結論に達しました(iOS11 +)。

これらの2つのカテゴリを考慮する必要があります:

  • _WKWebsiteDataStore.nonPersistentDataStore_を使用しています:

    次に、WKProcessPool重要ではありません

    1. websiteDataStore.httpCookieStore.getAllCookies()を使用してCookieを抽出します
    2. これらのCookieをUserDefaults(またはできればキーチェーン)に保存します。
    3. ...
    4. 後でストレージからこれらのCookieを再作成するときは、各Cookieに対してwebsiteDataStore.httpCookieStore.setCookie()を呼び出してください。
  • _WKWebsiteDataStore.defaultDataStore_を使用しています:

    次に、構成に関連付けられたWKProcessPoolDOESマター。クッキーとともに保存する必要があります。

    1. WebView構成のprocessPoolをUserDefaults(またはできればキーチェーン)に保存します。
    2. websiteDataStore.httpCookieStore.getAllCookies()を使用してCookieを抽出します
    3. これらのCookieをUserDefaults(またはできればキーチェーン)に保存します。
    4. ...
    5. 後でストレージからプロセスプールを再作成し、Webビューの構成に割り当てます
    6. ストレージからCookieを再作成し、各Cookieに対してwebsiteDataStore.httpCookieStore.setCookie()を呼び出します

注:多くの詳細な実装がすでに利用可能であるため、実装の詳細を追加しないことでシンプルにします。

0
Tumata