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?
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:
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.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).[request addValue:@"TeskCookieKey1=TeskCookieValue1;TeskCookieKey2=TeskCookieValue2;" forHTTPHeaderField:@"Cookie"]
).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 ...).
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 !!! :)
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 ...
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.
WKWebView
ne fonctionne pas bien avec NSHTTPCookieStorage
, donc pour iOS 8, 9, 10 vous devrez utiliser UIWebView.WKWebView
en tant que singleton, mais vous devez utiliser la même instance de WKProcessPool
à chaque fois pour obtenir à nouveau les cookies souhaités.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
.
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];
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)
}
}
}
Où url
est l'URL actuelle:
let url = URL(string: "https://insofttransfer.com")!
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
WKProcessPool
peu importe.
- Extraire les cookies en utilisant
websiteDataStore.httpCookieStore.getAllCookies()
- Enregistrez ces cookies dans UserDefaults (ou de préférence le trousseau).
- ...
- 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.
- Enregistrez le processPool de la configuration de la vue Web dans UserDefaults (ou de préférence le trousseau).
- Extraire les cookies en utilisant
websiteDataStore.httpCookieStore.getAllCookies()
- Enregistrez ces cookies dans UserDefaults (ou de préférence le trousseau).
- ...
- Recréez plus tard le pool de processus à partir du stockage et affectez-le à la configuration de la vue Web
- 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.