WKWebView 持久化存储 Cookies

WKWebView Persistent Storage of Cookies

我在本机 iPhone 应用程序中使用 WKWebView,在允许 login/registration 的网站上,并将会话信息存储在 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,会话被保留,因此 cookie 被成功共享。但是,当我重新启动该应用程序时,会创建一个新的进程池并销毁会话信息。有什么方法可以让会话信息在应用程序重启后保持不变?

将信息存储在NSUserDefaults中。同时如果session信息非常关键,最好存放在KeyChain.

WKWebView 符合 NSCoding ,所以你可以使用 NSCoder 到 decode/encode 你的 webView,并将它存储在其他地方,比如 NSUserDefaults

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

self.webView = [NSKeyedUnarchiver unarchiveObjectWithData:data];

这实际上是一个棘手的问题,因为 a) 一些 bug Apple 仍未解决(我认为)和 b) 我认为取决于您想要什么 cookie。

我现在无法测试这个,但我可以给你一些建议:

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

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

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

我希望至少可以为您指出一些解决问题的新方向。它似乎并不像它应该的那样微不足道(话又说回来,会话 cookie 是一种与安全相关的东西,所以也许将它们隐藏在远离应用程序的地方是 Apple 的一个有意识的设计选择......)。

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

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

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

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

sendToDevice("key:value")

上面的js函数只是简单的向设备传递cookies。希望这会帮助人们保持理智...

经过几天的研究和实验,我找到了一个在 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 中,我检查它是否不是登录页面,并从用户默认值将 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"];
}

Note: Above method is tested for iOS 11 only, although I have written fallback for lower versions also but didn’t test those.

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

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

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

终于找到了在WKWebView中管理session的解决方案,工作在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")!

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

你需要考虑这两类:

  • 您正在使用 WKWebsiteDataStore.nonPersistentDataStore:

    Then the WKProcessPool does not matter.

    1. Extract cookies using websiteDataStore.httpCookieStore.getAllCookies()
    2. Save these cookies into UserDefaults (or preferably the Keychain).
    3. ...
    4. Later when you re-create these cookies from storage, call websiteDataStore.httpCookieStore.setCookie() for each cookie and you're good to go.
  • 您正在使用 WKWebsiteDataStore.defaultDataStore:

    Then the WKProcessPool associated with configuration DOES matter. It has to be saved along with the cookies.

    1. Save the webview configuration's processPool into UserDefaults (or preferably the Keychain).
    2. Extract cookies using websiteDataStore.httpCookieStore.getAllCookies()
    3. Save these cookies into UserDefaults (or preferably the Keychain).
    4. ...
    5. Later re-create the process pool from storage and assign it to the web view's configuration
    6. Re-create the cookies from storage and call websiteDataStore.httpCookieStore.setCookie() for each cookie

注意:已经有许多详细的实现可用,因此我通过不添加更多实现细节来保持简单。