WKWebView Постоянное хранилище файлов cookie
Я использую WKWebView
в своем родном приложении для iPhone, на веб-сайте, который позволяет входить/регистрироваться и сохраняет информацию о сеансе в файлах cookie. Я пытаюсь выяснить, как постоянно хранить информацию о cookie файлах, поэтому, когда приложение перезапускается, пользователь все еще имеет доступ к своей веб-сессии.
У меня есть 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
, а затем через некоторое время WKWebView
действие во второй WKWebView
, сеанс сохраняется, поэтому файлы cookie были успешно переданы. Однако, когда я перезапускаю приложение, создается новый пул процессов, и информация о сеансе уничтожается. Есть ли способ получить информацию о сеансе для сохранения после перезапуска приложения?
Ответы
Ответ 1
На самом деле это сложный вопрос, потому что: а) какая-то ошибка, которая все еще не устранена Apple (я думаю) и б) зависит от того, какие куки вы хотите, я думаю.
Я не смог проверить это сейчас, но могу дать вам несколько советов:
- Получение файлов cookie от
NSHTTPCookieStorage.sharedHTTPCookieStorage()
. Кажется, что этот NSHTTPCookieStorage
, очевидно, что куки не сразу сохраняются для NSHTTPCookieStorage
чтобы найти их. Люди предлагают инициировать сохранение путем сброса пула процессов, но я не знаю, работает ли это надежно. Вы можете попробовать это для себя, хотя. - На самом деле пул процессов - это не то, что сохраняет куки (хотя он определяет, являются ли они общими, как вы правильно указали). В документации сказано, что
WKWebsiteDataStore
, так что я посмотрю это. По крайней мере, получение куки файлов оттуда с помощью fetchDataRecordsOfTypes:completionHandler:
могло бы быть возможным (хотя я не уверен, как их установить, и я предполагаю, что вы не можете просто сохранить хранилище в пользовательских настройках по умолчанию по той же причине, что и для пула процессов). - Если вам удастся получить нужные вам файлы cookie (или, скорее, их значения), но не сможете восстановить их, как я полагаю, так и будет, посмотрите здесь (в основном, это показывает, как просто подготовить httprequest с ними, соответствующую часть:
[request addValue:@"TeskCookieKey1=TeskCookieValue1;TeskCookieKey2=TeskCookieValue2;" forHTTPHeaderField:@"Cookie"]
). - Если ничего не помогает, проверьте это. Я знаю, что давать только ответы по ссылкам - нехорошо, но я не могу скопировать все это и просто хочу добавить их для полноты картины.
В общем, последнее: я сказал, что ваш успех также может зависеть от типа куки. Это потому, что в этом ответе говорится, что файлы cookie, установленные сервером, недоступны через NSHTTPCookieStorage
. Я не знаю, относится ли это к вам (но я предполагаю, что это так, поскольку вы, вероятно, ищете сеанс, то есть файл cookie, установленный на сервере, правильно?), И я не знаю, означает ли это, что другие методы не работают также.
Если ничего не помогает, вы можете сохранить учетные данные пользователей (например, связку ключей) и использовать их при следующем запуске приложения для автоматической аутентификации. Это может не восстановить все данные сеанса, но, учитывая, что пользователь вышел из приложения, что может быть на самом деле желательно? Также, возможно, определенные значения могут быть перехвачены и сохранены для последующего использования с использованием внедренного скрипта, как упомянуто здесь (очевидно, не для установки их при запуске, но, возможно, получить их в какой-то момент. Вам, конечно, нужно знать, как работает сайт),
Я надеюсь, что это может, по крайней мере, указать вам на некоторые новые направления решения проблемы. Кажется, это не так тривиально, как должно быть (с другой стороны, cookie файлы сеансов являются чем-то вроде безопасности, поэтому, возможно, скрытие их от приложения - это осознанный выбор дизайна Apple...).
Ответ 2
После нескольких дней исследований и экспериментов я нашел решение для управления сеансами в 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;
}
Для поддержания сеанса в веб-просмотре я сделал свой веб-просмотр и синтаксис 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, поэтому на вашем веб-сайте входа в систему сохраните файлы cookie из httpCookieStore в пользовательские значения по умолчанию в режиме 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, хотя я написал backback для более низких версий также, но не тестировал их.
Надеюсь, это решает ваши проблемы!!!:)
Ответ 3
Я немного опаздываю на вечеринку, но люди могут найти это полезным. Есть обходной путь, это немного раздражает, но насколько я могу сказать, это единственное решение, которое работает надежно, по крайней мере, до тех пор, пока яблоко не исправит свои немые API...
Я потратил хорошие 3 дня, пытаясь вытащить кэшированные куки из WKWebView
, не говоря уже о том, что меня нигде не было... в конце концов я выпустил, что могу просто получить файлы cookie непосредственно с сервера.
Первое, что я пытался сделать, это получить все файлы cookie с javascript, которые были запущены в WKWebView
, а затем передать их в WKUserContentController
, где я бы просто сохранил их в UserDefaults
. Это не сработало с моих файлов cookie, где httponly
и, видимо, вы не можете получить те, у кого есть javascript...
Я закончил тем, что исправил это, вставив javascript-вызов на страницу со стороны сервера (Ruby on Rail в моем случае) с куки файлами в качестве параметра, например
sendToDevice("key:value")
Вышеупомянутая функция js просто передает файлы cookie на устройство. Надеюсь, это поможет кому-то оставаться в здравом уме...
Ответ 4
Я немного опоздал с ответом на это, но я хотел бы добавить некоторые идеи к существующим ответам. Ответ, уже упомянутый здесь, уже предоставляет ценную информацию в Cookie Persistence на WKWebView. Есть несколько предостережений, хотя.
-
WKWebView
плохо работает с NSHTTPCookieStorage
, поэтому для iOS 8, 9, 10 вам придется использовать UIWebView. - Вам не нужно обязательно держаться за
WKWebView
как за WKWebView
но вам нужно каждый раз использовать один и тот же экземпляр WKProcessPool
чтобы снова получить нужные файлы cookie. - Желательно сначала установить файлы cookie с
setCookie
метода setCookie
а затем создать экземпляр WKWebView
.
Я также хотел бы выделить решение iOS 11+ в 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)
}
}
Вспомогательные методы:
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
завершения setCookie
не вызывались во второй раз, когда я пытался открыть WKWebView
. Это кажется ошибкой в WebKit. Поэтому мне пришлось сначала создать экземпляр WKWebView
а затем вызвать setCookie
для конфигурации. Убедитесь, что загружаете URL только после того, как все вызовы setCookie
вернулись.
Ответ 5
WKWebView
соответствует NSCoding
, поэтому вы можете использовать NSCoder
для декодирования/кодирования вашего webView и сохранения его в другом месте, например NSUserDefaults
.
//return data to store somewhere
NSData* data = [NSKeyedArchiver archivedDataWithRootObject:self.webView];/
self.webView = [NSKeyedUnarchiver unarchiveObjectWithData:data];
Ответ 6
Наконец, я нашел решение для управления сессиями в 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")!
Ответ 7
После тщательного поиска и ручной отладки я пришел к этим простым выводам (iOS11+).
Вы должны учитывать эти две категории:
Примечание: уже доступно много подробных реализаций, поэтому я оставлю это простым, не добавляя больше деталей реализации.
Ответ 8
Сохраните информацию в NSUserDefaults
. В то же время, если информация о сеансе очень важна, лучше сохранить ее в KeyChain
.