WKWebView持久存储Cookie

时间:2016-09-29 13:42:25

标签: ios cookies wkwebview

我在我的原生iPhone应用程序中使用WKWebView,在允许登录/注册的网站上,并将会话信息存储在cookie中。我试图找出如何持久存储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登录,然后一段时间后将操作传递给第二个WKWebView时,会话保留,因此cookie已成功共享。但是,当我重新启动应用程序时,会创建一个新的进程池并销毁会话信息。有没有办法让会话信息在应用程序重启时保持不变?

8 个答案:

答案 0 :(得分:16)

这实际上是一个艰难的,因为有一些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。我知道提供只是链接只有答案是不好的,但我不能复制所有这些,只是想为了完整性添加它。
  5. 最后一件事:我说你的成功也可能取决于cookie的类型。这是因为this answer表示服务器设置的cookie无法通过NSHTTPCookieStorage访问。我不知道这是否与你相关(但我想是的,因为你可能正在寻找一个会话,即服务器设置的cookie,对吗?)我不知道这是否意味着其他方法失败了同样。

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

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

答案 1 :(得分: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中,我检查它是否不是登录页面并从用户默认值中将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进行了测试,尽管我已经写过了   对于较低版本的回退也没有测试那些。

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

答案 2 :(得分:3)

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

我花了3天的时间试图从compile 'com.google.firebase:firebase-database:10.0.0' compile 'com.google.firebase:firebase-auth:10.0.0' compile 'com.firebaseui:firebase-ui-auth:1.0.0' 中取出缓存的饼干,不用说让我无处可去......最终我发布了我可以直接从中获取饼干服务器

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

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

httponly

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

答案 3 :(得分:2)

我回答这个问题有点晚了,但是我想对现有的答案加一些见解。 here中已经提到的答案已经为WKWebView的Cookie持久性提供了有价值的信息。虽然有一些警告。

  1. WKWebViewNSHTTPCookieStorage上无法正常运行,因此对于iOS 8、9、10您将必须使用UIWebView。
  2. 您不必一定要坚持WKWebView 单例,但您确实需要使用相同的实例 每次WKProcessPool再次获得所需的Cookie。
  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)
    }
}

Helper方法:

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。

答案 4 :(得分:0)

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

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

self.webView = [NSKeyedUnarchiver unarchiveObjectWithData:data];

答案 5 :(得分:0)

最后,我找到了一种在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")!

答案 6 :(得分:0)

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

您需要考虑以下两个类别:

  • 您正在使用WKWebsiteDataStore.nonPersistentDataStore

      

    然后WKProcessPool 没关系

         
        
    1. 使用websiteDataStore.httpCookieStore.getAllCookies()
    2. 提取Cookie   
    3. 将这些cookie保存到UserDefaults(最好是钥匙串)中。
    4.   
    5. ...
    6.   
    7. 稍后,当您从存储中重新创建这些cookie时,请为每个cookie调用websiteDataStore.httpCookieStore.setCookie()   你很好。
    8.   
  • 您正在使用WKWebsiteDataStore.defaultDataStore

      

    然后将与配置关联的WKProcessPool   问题。它必须与cookie一起保存。

         
        
    1. 将网络视图配置的processPool保存到UserDefaults(或最好是钥匙串)中。
    2.   
    3. 使用websiteDataStore.httpCookieStore.getAllCookies()
    4. 提取Cookie   
    5. 将这些cookie保存到UserDefaults(或最好是钥匙串)中。
    6.   
    7. ...
    8.   
    9. 稍后从存储中重新创建过程池,并将其分配给Web视图的配置
    10.   
    11. 从存储中重新创建cookie,并为每个cookie调用websiteDataStore.httpCookieStore.setCookie()
    12.   

注意:已经有许多详细的实现,因此我通过不添加更多实现细节来简化它。

答案 7 :(得分:-1)

将信息存储在NSUserDefaults中。同时,如果会话信息非常关键,最好将其存储在KeyChain