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