Reputation: 1236
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 trying to figure out how to persistently store the cookie information, so when the app restarts, the user still has their web session available.
I have 2 WKWebViews
in the app, and they share a WKProcessPool
. I start with a shared process pool:
WKProcessPool *processPool = [[WKProcessPool alloc] init];
Then for each WKWebView:
WKWebViewConfiguration *theConfiguration = [[WKWebViewConfiguration alloc] init];
theConfiguration.processPool = processPool;
self.webView = [[WKWebView alloc] initWithFrame:frame configuration:theConfiguration];
When I log in using the first WKWebView
, and then some time later pass the action to the 2nd WKWebView
, the session is retained, so the cookies were successfully shared. However, when I relaunch the app, a new process pool is created and the session information is destroyed. Is there any way to get the session information to persist through an app restart?
Upvotes: 59
Views: 65420
Reputation: 5762
I'm a little late in answering this but I would like to add some insights to the existing answers. The answer mentioned here provides valuable information into Cookie Persistence on WKWebView. However, there are a few caveats to it.
WKWebView
doesn't work well with NSHTTPCookieStorage
, so for iOS
8, 9, 10 you will have to use UIWebView.WKWebView
as a
singleton but you do need to use the same instance of
WKProcessPool
every time to get the desired cookies again.setCookie
method and then instantiate the WKWebView
.I would also like to highlight the iOS 11+ Solution in 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)
}
}
Helper methods:
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:
I had mentioned that it's preferable to instantiate WKWebView
post setCookie
calls. I ran into some issues wherein the setCookie
completion handlers were not getting called the second time I tried to open the WKWebView
. This seems to a be a bug in the WebKit. Therefore, I had to instantiate WKWebView
first and then call setCookie
on the configuration. Make sure to load the URL only after all the setCookie
calls have returned.
Upvotes: 21
Reputation: 1600
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.
- Extract cookies using
websiteDataStore.httpCookieStore.getAllCookies()
- Save these cookies into UserDefaults (or preferably the Keychain).
- ...
- 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.
- Save the webview configuration's processPool into UserDefaults (or preferably the Keychain).
- Extract cookies using
websiteDataStore.httpCookieStore.getAllCookies()
- Save these cookies into UserDefaults (or preferably the Keychain).
- ...
- Later re-create the process pool from storage and assign it to the web view's configuration
- 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.
Upvotes: 9
Reputation: 4424
This is actually a tough one because there's a) some bug that's still not solved by Apple (I think) and b) depends on what cookies you want, I think.
I wasn't able to test this now, but I can give you some pointers:
NSHTTPCookieStorage.sharedHTTPCookieStorage()
. This one seems buggy, apparently the cookies aren't immediately saved for NSHTTPCookieStorage
to find them. People suggest to trigger a save by resetting the process pool, but I don't know whether that reliably works. You might want to try that out for yourself, though.WKWebsiteDataStore
, so I'd look that up. At least getting the cookies from there using fetchDataRecordsOfTypes:completionHandler:
might be possible (not sure how to set them, though, and I assume you can't just save the store in user defaults for the same reason as for the process pool).[request addValue:@"TeskCookieKey1=TeskCookieValue1;TeskCookieKey2=TeskCookieValue2;" forHTTPHeaderField:@"Cookie"]
).One last thing in general: I said that your success might also depend on the type of cookie. That's because this answer states that cookies set by the server are not accessible via NSHTTPCookieStorage
. I don't know whether that's relevant to you (but I guess it is, since you're probably looking for a session, i.e. server-set cookie, correct?) and I don't know whether this means that the other methods fail as well.
If all else fails, you might consider saving the users credentials somewhere (keychain, for example) and reuse them on the next app start to auth automatically. This might not restore all session data, but considering the user quit the app that's maybe actually desirable? Also perhaps certain values can be caught and saved for later use using an injected script, like mentioned here (obviously not for setting them at start, but maybe retrieve them at some point. You need to know how the site works then, of course).
I hope that could at least point you towards some new directions solving the issue. It's not as trivial as it should be, it seems (then again, session cookies are kind of a security relevant thing, so maybe hiding them away from the App is a conscious design choice by Apple...).
Upvotes: 29
Reputation: 191
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")!
Upvotes: 19
Reputation: 1607
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 !!! :)
Upvotes: 13
Reputation: 75
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...
Upvotes: 3
Reputation: 6885
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];
Upvotes: -1
Reputation: 1767
Store the information in NSUserDefaults
. At the same time if the session information is very critical, it is better to store it in KeyChain
.
Upvotes: -3