web-dev-qa-db-fra.com

Stockage persistant de cookies WKWebView

J'utilise un WKWebView dans mon application iPhone native, sur un site Web qui permet la connexion/l'enregistrement et stocke les informations de session dans des cookies. J'essaie de comprendre comment stocker de manière persistante les informations sur les cookies, donc lorsque l'application redémarre, l'utilisateur a toujours sa session Web disponible.

J'ai 2 WKWebViews dans l'application et ils partagent un WKProcessPool. Je commence par un pool de processus partagé:

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

Ensuite, pour chaque WKWebView:

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

Lorsque je me connecte en utilisant le premier WKWebView, puis que je passe quelque temps plus tard l'action au 2e WKWebView, la session est conservée, les cookies ont donc été partagés avec succès. Cependant, lorsque je relance l'application, un nouveau pool de processus est créé et les informations de session sont détruites. Existe-t-il un moyen de conserver les informations de session lors du redémarrage de l'application?

30
haplo1384

C'est en fait difficile car il y a) certains bug qui ne sont toujours pas résolus par Apple (je pense) et b) dépend des cookies que vous voulez, je pense .

Je n'ai pas pu tester cela maintenant, mais je peux vous donner quelques conseils:

  1. Obtention des cookies de NSHTTPCookieStorage.sharedHTTPCookieStorage(). Celui-ci semble bogué, apparemment les cookies ne sont pas immédiatement enregistrés pour que NSHTTPCookieStorage les trouve. People suggère de déclencher une sauvegarde en réinitialisant le pool de processus, mais je ne sais pas si cela fonctionne de manière fiable. Vous voudrez peut-être essayer par vous-même, cependant.
  2. Le pool de processus n'est pas vraiment ce qui enregistre les cookies (bien qu'il définisse s'ils sont partagés comme vous l'avez correctement déclaré). La documentation dit que c'est WKWebsiteDataStore, donc je chercherais ça. Récupérer au moins les cookies à partir de là en utilisant fetchDataRecordsOfTypes:completionHandler: pourrait être possible (je ne sais pas comment les définir, cependant, et je suppose que vous ne pouvez pas simplement enregistrer le magasin dans les valeurs par défaut de l'utilisateur pour la même raison que pour le pool de processus).
  3. Si vous parvenez à obtenir les cookies dont vous avez besoin (ou plutôt leurs valeurs), mais que vous ne pouvez pas les restaurer comme je suppose que ce sera le cas, regardez ici (en gros, cela montre comment préparer simplement la requête http avec eux déjà, partie pertinente: [request addValue:@"TeskCookieKey1=TeskCookieValue1;TeskCookieKey2=TeskCookieValue2;" forHTTPHeaderField:@"Cookie"]).
  4. Si tout le reste échoue, cochez this . Je sais que fournir uniquement des réponses de lien uniquement n'est pas bon, mais je ne peux pas copier tout cela et je veux simplement l'ajouter par souci d'exhaustivité.

Une dernière chose en général: j'ai dit que votre succès pouvait aussi dépendre du type de cookie. C'est parce que cette réponse indique que les cookies définis par le serveur ne sont pas accessibles via NSHTTPCookieStorage. Je ne sais pas si cela vous concerne (mais je suppose que oui, puisque vous recherchez probablement une session, c'est-à-dire un cookie défini par le serveur, correct?) Et je ne sais pas si cela signifie que les autres méthodes échouent ainsi que.

Si tout le reste échoue, vous pourriez envisager d'enregistrer les informations d'identification des utilisateurs quelque part (trousseau, par exemple) et de les réutiliser au prochain démarrage de l'application pour l'authentification automatique. Cela pourrait ne pas restaurer toutes les données de session, mais étant donné que l'utilisateur quitte l'application, ce qui est peut-être réellement souhaitable? Certaines valeurs peuvent également être capturées et enregistrées pour une utilisation ultérieure à l'aide d'un script injecté, comme mentionné ici (évidemment pas pour les définir au début, mais peut-être les récupérer à un moment donné. Vous devez savoir comment le le site fonctionne alors, bien sûr).

J'espère que cela pourrait au moins vous orienter vers de nouvelles directions pour résoudre le problème. Ce n'est pas aussi trivial qu'il devrait l'être, semble-t-il (là encore, les cookies de session sont une sorte de chose importante pour la sécurité, alors peut-être que les cacher loin de l'application est un choix de conception délibéré d'Apple ...).

16
Gero

Après des jours de recherches et d'expériences, j'ai trouvé une solution pour gérer les sessions dans WKWebView, ceci est un travail à faire car je n'ai pas trouvé d'autre moyen d'y parvenir, voici les étapes:

Vous devez d'abord créer des méthodes pour définir et obtenir des données par défaut, quand je dis données, cela signifie NSData, voici les méthodes.

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

Pour maintenir la session sur la vue Web, j'ai créé ma vue Web et 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;
}

Dans ViewDidLoad, je vérifie s'il ne s'agit pas de la page de connexion et charge les cookies dans HttpCookieStore à partir des valeurs par défaut de l'utilisateur afin qu'il passe l'authentification ou utilise ces cookies pour maintenir la 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];
                }
            }
        }

Et, chargez la demande.

Maintenant, nous allons maintenir des sessions de visualisation Web en utilisant des cookies, donc sur votre page Web de connexion, enregistrez les cookies de httpCookieStore dans les valeurs par défaut de l'utilisateur dans la méthode 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"];
}

Remarque: la méthode ci-dessus est testée pour iOS 11 uniquement, bien que j'aie également écrit des solutions de remplacement pour les versions inférieures, mais que je n'ai pas testé celles-ci.

J'espère que cela résout vos problèmes !!! :)

4
Harish Pathak

Je suis un peu en retard à la fête mais les gens pourraient trouver cela utile. Il existe une solution de contournement, c'est un peu ennuyeux, mais pour autant que je puisse dire, c'est la seule solution qui fonctionne de manière fiable, au moins jusqu'à Apple corriger leurs API stupides ...

J'ai passé 3 bons jours à essayer de retirer les cookies mis en cache du WKWebView inutile de dire que cela ne m'a mené nulle part ... j'ai finalement annoncé que je pouvais simplement obtenir les cookies directement du serveur.

La première chose que j'ai essayé de faire est d'obtenir tous les cookies avec javascript qui s'exécutaient dans le WKWebView puis de les transmettre au WKUserContentController où je les stocke simplement dans UserDefaults . Cela n'a pas fonctionné depuis mes cookies où httponly et apparemment, vous ne pouvez pas obtenir ceux avec javascript ...

J'ai fini par le réparer en insérant un appel javascript dans la page côté serveur (Ruby on Rail dans mon cas) avec les cookies comme paramètre, par ex.

sendToDevice("key:value")

La fonction js ci-dessus transmet simplement les cookies à l'appareil. J'espère que cela aidera quelqu'un à rester sain d'esprit ...

3
Dovydas Rupšys

Je suis un peu en retard pour répondre à cette question, mais j'aimerais ajouter quelques éclaircissements aux réponses existantes. La réponse déjà mentionnée ici fournit déjà des informations précieuses sur la persistance des cookies sur WKWebView. Il y a cependant quelques mises en garde.

  1. WKWebView ne fonctionne pas bien avec NSHTTPCookieStorage, donc pour iOS 8, 9, 10 vous devrez utiliser UIWebView.
  2. Vous n'avez pas besoin de conserver le WKWebView en tant que singleton, mais vous devez utiliser la même instance de WKProcessPool à chaque fois pour obtenir à nouveau les cookies souhaités.
  3. Il est préférable de définir d'abord les cookies à l'aide de la méthode setCookie puis d'instancier la WKWebView.

Je voudrais également souligner la solution iOS 11+ dans Swift.

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

Méthodes d'assistance:

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
}

Edit: j'avais mentionné qu'il était préférable d'instancier WKWebView post setCookie appels. J'ai rencontré des problèmes dans lesquels les gestionnaires de complétion setCookie n'étaient pas appelés la deuxième fois que j'ai essayé d'ouvrir le WKWebView. Cela semble être un bogue dans le WebKit. Par conséquent, j'ai dû instancier WKWebView d'abord, puis appeler setCookie sur la configuration. Assurez-vous de ne charger l'URL qu'après le retour de tous les appels setCookie.

2
jarora

WKWebView est conforme à NSCoding, vous pouvez donc utiliser NSCoder pour décoder/encoder votre webView et la stocker ailleurs, comme NSUserDefaults.

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

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

Enfin, j'ai trouvé une solution pour gérer les sessions dans WKWebView, travailler sous Swift 4, mais la solution peut être portée à Swift 3 ou 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
}}

Créer une extension pour 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)
    }
}}

Ensuite, créez une extension pour notre contrôleur de vue comme ceci

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 est l'URL actuelle:

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

Après une recherche approfondie et un débogage manuel, j'ai atteint ces conclusions simples (iOS11 +).

Vous devez considérer ces deux catégories:

  • Vous utilisez WKWebsiteDataStore.nonPersistentDataStore:

    Alors le WKProcessPoolpeu importe.

    1. Extraire les cookies en utilisant websiteDataStore.httpCookieStore.getAllCookies()
    2. Enregistrez ces cookies dans UserDefaults (ou de préférence le trousseau).
    3. ...
    4. Plus tard, lorsque vous recréez ces cookies à partir du stockage, appelez websiteDataStore.httpCookieStore.setCookie() pour chaque cookie et vous êtes prêt à partir.
  • Vous utilisez WKWebsiteDataStore.defaultDataStore:

    Ensuite, le WKProcessPool associé à la configuration importe. Il doit être enregistré avec les cookies.

    1. Enregistrez le processPool de la configuration de la vue Web dans UserDefaults (ou de préférence le trousseau).
    2. Extraire les cookies en utilisant websiteDataStore.httpCookieStore.getAllCookies()
    3. Enregistrez ces cookies dans UserDefaults (ou de préférence le trousseau).
    4. ...
    5. Recréez plus tard le pool de processus à partir du stockage et affectez-le à la configuration de la vue Web
    6. Recréez les cookies à partir du stockage et appelez websiteDataStore.httpCookieStore.setCookie() pour chaque cookie

Remarque: il existe de nombreuses implémentations détaillées déjà disponibles, donc je reste simple en n'ajoutant pas plus de détails d'implémentation.

0
Tumata