【问题标题】:WKWebView Persistent Storage of CookiesWKWebView Cookie 的持久存储
【发布时间】:2016-09-29 13:42:25
【问题描述】:

我在我的原生 iPhone 应用程序中使用WKWebView,在一个允许登录/注册的网站上,并将会话信息存储在 cookie 中。我正在尝试弄清楚如何持久存储 cookie 信息,因此当应用重新启动时,用户仍然可以使用他们的网络会话。

我在应用中有 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 登录时,一段时间后将操作传递给第二个WKWebView,会话被保留,因此cookies 被成功共享。但是,当我重新启动应用程序时,会创建一个新的进程池并销毁会话信息。有没有办法让会话信息在应用重启时保持不变?

【问题讨论】:

  • 你不需要做额外的动作来保存WKWebView数据——它应该被自动保存(唯一需要澄清的是你在配置中使用defaultDataStore,因为它只是一个保存到磁盘的)。您确定会话信息在WKWebView 中丢失了吗? cookie 有没有可能有小 ttl 或其他东西?你检查了吗?

标签: ios cookies wkwebview


【解决方案1】:

这实际上是一个艰难的问题,因为 a) 一些 bug 仍然没有被 Apple 解决(我认为)并且 b) 我认为取决于你想要什么 cookie。

我现在无法对此进行测试,但我可以给你一些建议:

  1. NSHTTPCookieStorage.sharedHTTPCookieStorage() 获取cookie。这个似乎有问题,显然 cookie 没有立即保存以供NSHTTPCookieStorage 找到它们。 People 建议通过重置进程池来触发保存,但我不知道这是否可靠。不过,您可能想自己尝试一下。
  2. 进程池并不是真正保存 cookie 的原因(尽管它定义了它们是否按照您正确的说明共享)。文档说那是WKWebsiteDataStore,所以我会查一下。至少使用 fetchDataRecordsOfTypes:completionHandler: 从那里获取 cookie 是可能的(但不确定如何设置它们,我假设您不能只将存储保存在用户默认值中,原因与进程池相同)。
  3. 如果你设法得到你需要的 cookie(或者更确切地说是它们的值),但不能像我猜的那样恢复它们,请看here(基本上它显示了如何简单地用它们准备 httprequest已经,相关部分:[request addValue:@"TeskCookieKey1=TeskCookieValue1;TeskCookieKey2=TeskCookieValue2;" forHTTPHeaderField:@"Cookie"])。
  4. 如果一切都失败了,请检查this。我知道仅提供仅链接的答案并不好,但我无法复制所有内容,只是为了完整起见而添加它。

最后一件事:我说过你的成功也可能取决于 cookie 的类型。这是因为this answer 声明服务器设置的cookie 不能通过NSHTTPCookieStorage 访问。我不知道这是否与您有关(但我想是的,因为您可能正在寻找会话,即服务器设置的 cookie,对吗?)而且我不知道这是否意味着其他方法失败也是。

如果所有其他方法都失败了,您可能会考虑将用户凭据保存在某处(例如钥匙串)并在下一个应用程序启动时重新使用它们以自动进行身份验证。这可能不会恢复所有会话数据,但考虑到用户退出了可能真正需要的应用程序? 也可能某些值可以被捕获并保存以供以后使用注入脚本,如提到的here(显然不是在开始时设置它们,但可能在某个时候检索它们。你需要知道站点是如何工作的,当然)。

我希望这至少可以为您指明解决问题的一些新方向。这似乎并不像应有的那么微不足道(再说一次,会话 cookie 是一种与安全相关的东西,所以也许将它们从应用程序中隐藏起来是 Apple 有意识的设计选择......)。

【讨论】:

  • 看起来我最终能够在 NSHTTPCookiesStorage 中查看一些 cookie,所以你是对的,可能只是没有立即保存。但是,会话所需的 cookie 似乎不存在,因此您关于服务器端 cookie 的最后一点可能是正在发生的事情。
  • 该死,太糟糕了。我想如果应用程序完全重新启动,您可能必须主动重新登录。只要您将用户的凭据安全地存储在某个地方(我建议使用钥匙串,至少对于密码)就可以了。当然不理想,但这样您就可以使用适当的 HTTPRequest 请求新的会话令牌?
【解决方案2】:

我回答这个问题有点晚了,但我想对现有答案添加一些见解。提到的答案here 为 WKWebView 上的 Cookie Persistence 提供了有价值的信息。但是,有一些注意事项。

  1. WKWebView 不适用于 NSHTTPCookieStorage,所以对于 iOS 8、9、10 你将不得不使用 UIWebView。
  2. 您不一定要坚持使用WKWebView 作为 单例,但您确实需要使用相同的实例 WKProcessPool 每次都会重新获得所需的 cookie。
  3. 最好先使用setCookie 设置cookies 方法,然后实例化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 发布setCookie 调用。我遇到了一些问题,其中setCookie 完成处理程序在我第二次尝试打开WKWebView 时没有被调用。这似乎是 WebKit 中的一个错误。因此,我必须先实例化WKWebView,然后在配置上调用setCookie。确保仅在所有 setCookie 调用都返回后加载 URL。

【讨论】:

  • 寻找复杂事物解决方案的人并不容易 :) 如果您有 ios 8 到 11 的解决方案,请分享...
【解决方案3】:

终于找到了在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)
    }
}}

然后像这样为我们的视图控制器创建一个扩展

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")!

【讨论】:

  • 非常感谢您提供此复制/粘贴解决方案!它就像一个魅力!
【解决方案4】:

经过几天的研究和实验,我找到了在 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 中,我检查它是否不是登录页面,并从 User Defaults 将 cookie 加载到 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 上,将 httpCookieStore 中的 cookie 保存到 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 进行了测试,虽然我已经写过 低版本的回退,但没有测试这些。

希望这能解决您的问题!!! :)

【讨论】:

  • 这是我发现解决此问题的第一个答案(我正在使用 iOS 14 进行测试)。非常感谢!
  • @bruestle2 乐于助人:)
  • 巨大的“谢谢!!!”发布此解决方案!如上所述 - 这是我在大量关于该主题的文档记录不佳/过时的文章中找到的唯一 ios14 解决方案
【解决方案5】:

经过大量搜索和手动调试,我得出了这些简单的结论(iOS11+)。

您需要考虑这两个类别:

  • 你正在使用WKWebsiteDataStore.nonPersistentDataStore:

    那么WKProcessPool 没关系

    1. 使用websiteDataStore.httpCookieStore.getAllCookies()提取cookie
    2. 将这些 cookie 保存到 UserDefaults(或者最好是 Keychain)中。
    3. ...
    4. 稍后当您从存储中重新创建这些 cookie 时,请为每个 cookie 调用 websiteDataStore.httpCookieStore.setCookie() 并 你可以走了。
  • 你正在使用WKWebsiteDataStore.defaultDataStore

    然后WKProcessPool与配置相关联DOES 事。它必须与 cookie 一起保存。

    1. 将 webview 配置的 processPool 保存到 UserDefaults(或者最好是 Keychain)中。
    2. 使用websiteDataStore.httpCookieStore.getAllCookies()提取cookie
    3. 将这些 cookie 保存到 UserDefaults(或者最好是 Keychain)中。
    4. ...
    5. 稍后从存储中重新创建进程池并将其分配给 Web 视图的配置
    6. 从存储中重新创建 cookie 并为每个 cookie 调用 websiteDataStore.httpCookieStore.setCookie()

注意:已经有许多详细的实现可用,所以我保持简单,不添加更多实现细节。

【讨论】:

  • 我凭经验刚刚尝试过使用 nonPersistentDataStore,WKProcessPool 很重要……只要您使用相同的 nonPersistentDataStore。基本上,如果你想为类似隐身模式的东西提供一个非持久性存储,但它在不同的 WKWebView 实例之间共享(想想浏览器的选项卡),那么你还想共享同一个 WKProcessPool。
  • @txulu 感谢您告诉我。自从我的回答(一年前)以来,情况很可能发生了变化。我的答案适用于持久化然后恢复,你试过这个用例吗?还是您只是尝试跨多个 WKWebViews 使用?如果我描述的用例发生了变化,我很乐意再试一次并更新我的答案。
  • 我尝试了多个,是的。当您有多个 WKWebViews 时,它们必须共享数据存储,并且不会共享进程池或 cookie。这是有道理的,出于安全原因,不同的进程不应相互通信。
【解决方案6】:

我参加聚会有点晚了,但人们可能会觉得这很有用。有一种解决方法,它有点烦人,但据我所知,这是唯一可靠的解决方案,至少在苹果修复他们的愚蠢 API 之前......

我花了 3 天时间试图从 WKWebView 中取出缓存的 cookie,不用说这让我无处可去……最终我发布了我可以直接从服务器获取 cookie。

我尝试做的第一件事是使用在WKWebView 中运行的javascript 获取所有cookie,然后将它们传递给WKUserContentController,我将它们存储到UserDefaults。这没有用,因为我的 cookie 在 httponly 中,显然你不能用 javascript 获得那些......

我最终通过将 javascript 调用插入服务器端的页面(在我的情况下为 Ruby on Rail)以 cookie 作为参数来修复它,例如

sendToDevice("key:value")

上面的 js 函数只是将 cookie 传递给设备。希望这能帮助人们保持理智......

【讨论】:

    【解决方案7】:

    WKWebView 符合NSCoding,因此您可以使用NSCoder 对您的webView 进行解码/编码,并将其存储在其他位置,例如NSUserDefaults

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

    【讨论】:

    • 谢谢,但是在新应用启动后从nsuserdefaults检索WKWebView时,网站的会话信息没有保留。
    【解决方案8】:

    将信息存储在NSUserDefaults 中。同时如果session信息很关键,最好保存在KeyChain中。

    【讨论】:

    • 在用户默认值中存储什么信息? WKProcessPool 不能存储在默认值中,有关于能够对其进行编码的错误。
    猜你喜欢
    • 2017-07-25
    • 1970-01-01
    • 1970-01-01
    • 2015-03-17
    • 2014-10-07
    • 2015-12-09
    • 2012-12-01
    • 2020-09-22
    • 1970-01-01
    相关资源
    最近更新 更多