WKWebView Persistent Storage of Cookies

后端 未结 8 1177
时光说笑
时光说笑 2020-11-29 22:22

I am using a WKWebView in my native iPhone application, on a website that allows login/registration, and stores the session information in cookies. I am tryin

相关标签:
8条回答
  • 2020-11-29 22:47

    After extensive search and manual debug I reached these simple conclusions (iOS11+).

    You need to considerate these two categories:

    • You are using 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.
    • You are using 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

    Note: there are many detailed implementations already available so I keep it simple by not adding more implementation details.

    0 讨论(0)
  • 2020-11-29 22:47

    Finally, I have found a solution to manage sessions in WKWebView, work under swift 4, but the solution can be carried to swift 3 or 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
    }}
    

    Create an extension for 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)
        }
    }}
    

    Then Create an extension for our View Controller Like this

    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)
        }
    }
    }
    

    Where url is current URL:

        let url = URL(string: "https://insofttransfer.com")!
    
    0 讨论(0)
  • 2020-11-29 22:48

    WKWebView conforms to NSCoding ,so you can use NSCoder to decode/encode your webView ,and store it somewhere else ,like NSUserDefaults.

    //return data to store somewhere
    NSData* data = [NSKeyedArchiver archivedDataWithRootObject:self.webView];/
    
    self.webView = [NSKeyedUnarchiver unarchiveObjectWithData:data];
    
    0 讨论(0)
  • 2020-11-29 22:49

    After days of research and experiments, I have found a solution to manage sessions in WKWebView, This is a work around because I didn’t find any other way to achieve this, below are the steps:

    First you need to create methods to set and get data in user defaults, when I say data it means NSData, here are the methods.

    +(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;
    }
    

    For maintaining session on webview I made my webview and WKProcessPool singleton.

    - (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;
    }
    

    In ViewDidLoad, I check if it’s not the login page and load cookies into HttpCookieStore from User Defaults so It will by pass authentication or use those cookies to maintain session.

    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];
                    }
                }
            }
    

    And, Load the request.

    Now, we will maintain webview sessions using cookies, so on your login page webview, save cookies from httpCookieStore into user defaults in viewDidDisappear method.

    - (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.

    Hope this solves your problems !!! :)

    0 讨论(0)
  • 2020-11-29 22:59

    Store the information in NSUserDefaults. At the same time if the session information is very critical, it is better to store it in KeyChain.

    0 讨论(0)
  • 2020-11-29 23:02

    I am a bit late to the party but people might find this useful. There is a workaround, it's a bit annoying but as far as I can say it is the only solution that works reliably, at least until apple fix their dumb APIs...

    I've spend a good 3 days trying to get the cached cookies out of the WKWebView needless to say that got me nowhere... eventually I've released that I could just get the cookies directly from the server.

    The first thing I tried to do is get all the cookies with javascript that was running within the WKWebView and then pass them to the WKUserContentController where I would just store them to UserDefaults. This didn't work since my cookies where httponly and apparently you can't get those with javascript...

    I've ended up fixing it by inserting a javascript call into the page on the server side (Ruby on Rail in my case) with the cookies as the parameter, e.g.

    sendToDevice("key:value")

    The above js function is simply passing cookies to the device. Hope this will help someone stay sane...

    0 讨论(0)
提交回复
热议问题