Могу ли я установить куки для использования WKWebView?


Я пытаюсь переключить существующее приложение из UIWebView в WKWebView. Текущее приложение управляет логином / сеансом пользователей за пределами веб-представления и устанавливает файлы cookie, необходимые для аутентификации, в хранилище NSHTTPCookieStore. К сожалению, новый WKWebView не использует куки из NSHTTPCookieStorage. Есть ли другой способ добиться этого?

13 77

13 ответов:

редактировать только для iOS 11+

использовать WKHTTCookieStore:

let cookie = HTTPCookie(properties: [
    .domain: "example.com",
    .path: "/",
    .name: "MyCookieName",
    .value: "MyCookieValue",
    .secure: "TRUE",
    .expires: NSDate(timeIntervalSinceNow: 31556926)
])! 

webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)

так как вы тянете их из HTTPCookeStorage, вы можете сделать это:

let cookies = HTTPCookieStorage.shared.cookies ?? []
for (cookie) in cookies {
    webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
}

старый ответ для iOS 10 и ниже

если вы требуете, чтобы ваши куки были установлены на начальном запросе загрузки, вы можете установить их на NSMutableURLRequest. Поскольку cookies - это просто специально отформатированный заголовок запроса, это может быть достигнуто, например Итак:

WKWebView * webView = /*set up your webView*/
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://example.com/index.html"]];
[request addValue:@"TeskCookieKey1=TeskCookieValue1;TeskCookieKey2=TeskCookieValue2;" forHTTPHeaderField:@"Cookie"];
// use stringWithFormat: in the above line to inject your values programmatically
[webView loadRequest:request];

Если вам требуется, чтобы последующие запросы AJAX на странице имели свои куки-файлы, это может быть достигнуто простым использованием WKUserScript для установки значений программно с помощью javascript при запуске документа следующим образом:

WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc] 
    initWithSource: @"document.cookie = 'TeskCookieKey1=TeskCookieValue1';document.cookie = 'TeskCookieKey2=TeskCookieValue2';"
    injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
// again, use stringWithFormat: in the above line to inject your values programmatically
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
webViewConfig.userContentController = userContentController;
WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];

объединение этих двух методов должно дать вам достаточно инструментов для передачи значений cookie из родной земли приложения в веб-вид Земли. Вы можете найти дополнительную информацию о cookie javascript api на странице mozilla, если вам нужны некоторые более продвинутые куки.

Да это отстой, что Apple не поддерживает многие из тонкости UIWebView. Не уверен, что они когда-нибудь поддержат их, но, надеюсь, они скоро это сделают. Надеюсь, это поможет!

после игры с ответ (что было фантастически полезно :) нам пришлось внести несколько изменений:

  • нам нужны веб-представления для работы с несколькими доменами без утечки личной информации cookie между этими доменами
  • нам это нужно, чтобы соблюдать безопасные куки
  • если сервер изменяет значение cookie, мы хотим, чтобы наше приложение знало об этом в NSHTTPCookieStorage
  • если сервер меняет значение cookie, мы не хотим наши скрипты, чтобы сбросить его обратно в исходное значение, когда вы перейдете по ссылке / AJAX и т. д.

поэтому мы изменили наш код это:

создать запрос

NSMutableURLRequest *request = [originalRequest mutableCopy];
NSString *validDomain = request.URL.host;
const BOOL requestIsSecure = [request.URL.scheme isEqualToString:@"https"];

NSMutableArray *array = [NSMutableArray array];
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
    // Don't even bother with values containing a `'`
    if ([cookie.name rangeOfString:@"'"].location != NSNotFound) {
        NSLog(@"Skipping %@ because it contains a '", cookie.properties);
        continue;
    }

    // Is the cookie for current domain?
    if (![cookie.domain hasSuffix:validDomain]) {
        NSLog(@"Skipping %@ (because not %@)", cookie.properties, validDomain);
        continue;
    }

    // Are we secure only?
    if (cookie.secure && !requestIsSecure) {
        NSLog(@"Skipping %@ (because %@ not secure)", cookie.properties, request.URL.absoluteString);
        continue;
    }

    NSString *value = [NSString stringWithFormat:@"%@=%@", cookie.name, cookie.value];
    [array addObject:value];
}

NSString *header = [array componentsJoinedByString:@";"];
[request setValue:header forHTTPHeaderField:@"Cookie"];

// Now perform the request...

это гарантирует, что первый запрос имеет правильный набор файлов cookie, без отправки каких-либо файлов cookie из общего хранилища, которые предназначены для других доменов, и без отправки каких-либо безопасных файлов cookie в небезопасный запрос.

дело с Далее запросы

мы также должны убедиться, что другие запросы имеют набор печенья. Это делается с помощью скрипта, который запускается при загрузке документа, который проверяет, есть ли набор файлов cookie, а если нет, установите его в значение NSHTTPCookieStorage.

// Get the currently set cookie names in javascriptland
[script appendString:@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"];

for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
    // Skip cookies that will break our script
    if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
        continue;
    }

    // Create a line that appends this cookie to the web view's document's cookies
    [script appendFormat:@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n", cookie.name, cookie.wn_javascriptString];
}

WKUserContentController *userContentController = [[WKUserContentController alloc] init];
WKUserScript *cookieInScript = [[WKUserScript alloc] initWithSource:script
                                                      injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                   forMainFrameOnly:NO];
[userContentController addUserScript:cookieInScript];

...

// Create a config out of that userContentController and specify it when we create our web view.
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
config.userContentController = userContentController;

self.webView = [[WKWebView alloc] initWithFrame:webView.bounds configuration:config];

работа с изменениями cookie

нам также нужно иметь дело с сервером, изменяющим значение файла cookie. Это означает добавление другого скрипта для обратного вызова из веб-представления, которое мы создаем обновите наш NSHTTPCookieStorage.

WKUserScript *cookieOutScript = [[WKUserScript alloc] initWithSource:@"window.webkit.messageHandlers.updateCookies.postMessage(document.cookie);"
                                                       injectionTime:WKUserScriptInjectionTimeAtDocumentStart
                                                    forMainFrameOnly:NO];
[userContentController addUserScript:cookieOutScript];

[userContentController addScriptMessageHandler:webView
                                          name:@"updateCookies"];

и реализация метода делегата для обновления любых файлов cookie, которые изменились, убедившись, что мы обновляем только файлы cookie из текущего домена!

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    NSArray<NSString *> *cookies = [message.body componentsSeparatedByString:@"; "];
    for (NSString *cookie in cookies) {
        // Get this cookie's name and value
        NSArray<NSString *> *comps = [cookie componentsSeparatedByString:@"="];
        if (comps.count < 2) {
            continue;
        }

        // Get the cookie in shared storage with that name
        NSHTTPCookie *localCookie = nil;
        for (NSHTTPCookie *c in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:self.wk_webView.URL]) {
            if ([c.name isEqualToString:comps[0]]) {
                localCookie = c;
                break;
            }
        }

        // If there is a cookie with a stale value, update it now.
        if (localCookie) {
            NSMutableDictionary *props = [localCookie.properties mutableCopy];
            props[NSHTTPCookieValue] = comps[1];
            NSHTTPCookie *updatedCookie = [NSHTTPCookie cookieWithProperties:props];
            [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:updatedCookie];
        }
    }
}

это, кажется, исправить наши проблемы cookie без нас иметь дело с каждым местом мы используем WKWebView по-разному. Теперь мы можем просто использовать этот код в качестве помощника для создания наших веб-просмотров, и он прозрачно обновляет NSHTTPCookieStorage для нас.


EDIT: оказывается, я использовал приватную категорию на NSHTTPCookie-вот код:

- (NSString *)wn_javascriptString {
    NSString *string = [NSString stringWithFormat:@"%@=%@;domain=%@;path=%@",
                        self.name,
                        self.value,
                        self.domain,
                        self.path ?: @"/"];

    if (self.secure) {
        string = [string stringByAppendingString:@";secure=true"];
    }

    return string;
}

работай на меня

func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler: (WKNavigationActionPolicy) -> Void) {
    let headerFields = navigationAction.request.allHTTPHeaderFields
    var headerIsPresent = contains(headerFields?.keys.array as! [String], "Cookie")

    if headerIsPresent {
        decisionHandler(WKNavigationActionPolicy.Allow)
    } else {
        let req = NSMutableURLRequest(URL: navigationAction.request.URL!)
        let cookies = yourCookieData
        let values = NSHTTPCookie.requestHeaderFieldsWithCookies(cookies)
        req.allHTTPHeaderFields = values
        webView.loadRequest(req)

        decisionHandler(WKNavigationActionPolicy.Cancel)
    }
}

вот моя версия Mattrs решение в Swift для инъекции всех файлов cookie из HTTPCookieStorage. Это было сделано в основном для введения файла cookie аутентификации для создания сеанса пользователя.

public func setupWebView() {
    let userContentController = WKUserContentController()
    if let cookies = HTTPCookieStorage.shared.cookies {
        let script = getJSCookiesString(for: cookies)
        let cookieScript = WKUserScript(source: script, injectionTime: .atDocumentStart, forMainFrameOnly: false)
        userContentController.addUserScript(cookieScript)
    }
    let webViewConfig = WKWebViewConfiguration()
    webViewConfig.userContentController = userContentController

    self.webView = WKWebView(frame: self.webViewContainer.bounds, configuration: webViewConfig)
}

///Generates script to create given cookies
public func getJSCookiesString(for cookies: [HTTPCookie]) -> String {
    var result = ""
    let dateFormatter = DateFormatter()
    dateFormatter.timeZone = TimeZone(abbreviation: "UTC")
    dateFormatter.dateFormat = "EEE, d MMM yyyy HH:mm:ss zzz"

    for cookie in cookies {
        result += "document.cookie='\(cookie.name)=\(cookie.value); domain=\(cookie.domain); path=\(cookie.path); "
        if let date = cookie.expiresDate {
            result += "expires=\(dateFormatter.stringFromDate(date)); "
        }
        if (cookie.secure) {
            result += "secure; "
        }
        result += "'; "
    }
    return result
}

куки-файлы должны быть установлены в конфигурации перед это. В противном случае, даже с WKHTTPCookieStore ' s setCookie обработчик завершения, куки-файлы не будут надежно синхронизированы с веб-представлением. Это восходит к этой строке из docs on WKWebViewConfiguration

@NSCopying var configuration: WKWebViewConfiguration { get }

это @NSCopying очень глубокая копия. Реализация находится за пределами меня, но конечный результат заключается в том, что если вы не установите куки перед инициализацией webview, вы не можете рассчитывать на печенье там. Это может усложнить архитектуру приложения, поскольку не инициализация представления становится асинхронным процессом. Вы будете в конечном итоге с чем-то вроде этого

extension WKWebViewConfiguration {
    /// Async Factory method to acquire WKWebViewConfigurations packaged with system cookies
    static func cookiesIncluded(completion: @escaping (WKWebViewConfiguration?) -> Void) {
        let config = WKWebViewConfiguration()
        guard let cookies = HTTPCookieStorage.shared.cookies else {
            completion(config)
            return
        }
        // Use nonPersistent() or default() depending on if you want cookies persisted to disk
        // and shared between WKWebViews of the same app (default), or not persisted and not shared
        // across WKWebViews in the same app.
        let dataStore = WKWebsiteDataStore.nonPersistent()
        let waitGroup = DispatchGroup()
        for cookie in cookies {
            waitGroup.enter()
            dataStore.httpCookieStore.setCookie(cookie) { waitGroup.leave() }
        }
        waitGroup.notify(queue: DispatchQueue.main) {
            config.websiteDataStore = dataStore
            completion(config)
        }
    }
}

а потом использовать его что-то вроде

override func loadView() {
    view = UIView()
    WKWebViewConfiguration.cookiesIncluded { [weak self] config in
        let webView = WKWebView(frame: .zero, configuration: webConfiguration)
        webView.load(request)
        self.view = webView
    }
}

приведенный выше пример откладывает создание представления до последнего возможного момента, другим решением было бы создать конфигурацию или webview заблаговременно и обработать асинхронный характер перед созданием представления контроллер.

последнее примечание: после того, как вы создадите этот webview, вы выпустили его в дикую природу, вы не можете добавить больше файлов cookie без использования методов, описанных в ответ. Однако вы можете использовать WKHTTPCookieStoreObserver API, чтобы, по крайней мере, наблюдать изменения, происходящие с печеньем. Поэтому, если файл cookie сеанса обновляется в webview, вы можете вручную обновить систему HTTPCookieStorage С этим новым cookie, если это необходимо.

для получения дополнительной информации, перейдите к 18: 00 на этом 2017 Сеанс WWDC загрузка пользовательского веб-контента. В начале этого сеанса есть обманчивый пример кода, который опускает тот факт, что webview должен быть создан в обработчике завершения.

cookieStore.setCookie(cookie!) {
    webView.load(loggedInURLRequest)
}

живая демонстрация в 18: 00 проясняет это.

Edit по крайней мере, в Mojave Beta 7 и iOS 12 Beta 7 я вижу гораздо более последовательное поведение с файлами cookie. Элемент setCookie(_:) метод даже позволяет устанавливать куки после WKWebView есть был создан. Я действительно нашел это важным, хотя, чтобы не трогать the processPool переменная вообще. Функция настройки cookie работает лучше всего, когда дополнительные пулы не создаются и когда это свойство оставлено в покое. Я думаю, можно с уверенностью сказать, у нас были проблемы из-за ошибки в WebKit.

Swift 3 Обновление:

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Void) {
    if let urlResponse = navigationResponse.response as? HTTPURLResponse,
       let url = urlResponse.url,
       let allHeaderFields = urlResponse.allHeaderFields as? [String : String] {
       let cookies = HTTPCookie.cookies(withResponseHeaderFields: allHeaderFields, for: url)
       HTTPCookieStorage.shared.setCookies(cookies , for: urlResponse.url!, mainDocumentURL: nil)
       decisionHandler(.allow)
    }
}

установить cookie

self.webView.evaluateJavaScript("document.cookie='access_token=your token';domain='your domain';") { (data, error) -> Void in
        self.webView.reload()
}

удалить cookie

self.webView.evaluateJavaScript("document.cookie='access_token=';domain='your domain';") { (data, error) -> Void in
        self.webView.reload()
}

в iOS 11 Теперь вы можете управлять cookie:), см. этот сеанс:https://developer.apple.com/videos/play/wwdc2017/220/

enter image description here

пожалуйста, найдите решение, которое, скорее всего, будет работать для вас из коробки. В основном он изменен и обновлен для Swift 4 @user3589213 ' s ответ.

func webView(_ webView: WKWebView, decidePolicyFor navigationAction: WKNavigationAction, decisionHandler: @escaping (WKNavigationActionPolicy) -> Void) {
    let headerKeys = navigationAction.request.allHTTPHeaderFields?.keys
    let hasCookies = headerKeys?.contains("Cookie") ?? false

    if hasCookies {
        decisionHandler(.allow)
    } else {
        let cookies = HTTPCookie.requestHeaderFields(with: HTTPCookieStorage.shared.cookies ?? [])

        var headers = navigationAction.request.allHTTPHeaderFields ?? [:]
        headers += cookies

        var req = navigationAction.request
        req.allHTTPHeaderFields = headers

        webView.load(req)

        decisionHandler(.cancel)
    }
}

после просмотра различных ответов здесь и не имея никакого успеха, я прочесал документацию WebKit и наткнулся на requestHeaderFields статический метод на HTTPCookie, который преобразует массив в cookie-файлов в формат, подходящий для поля заголовка. Сочетая это с mattr озарения обновления URLRequest перед загрузкой его с заголовками cookie получил меня через финишную черту.

Swift 4.1:

var request = URLRequest(url: URL(string: "https://example.com/")!)
let headers = HTTPCookie.requestHeaderFields(with: cookies)
for (name, value) in headers {
    request.addValue(value, forHTTPHeaderField: name)
}

let webView = WKWebView(frame: self.view.frame)
webView.load(request)

чтобы сделать это еще проще, использовать расширение:

extension WKWebView {
    func load(_ request: URLRequest, with cookies: [HTTPCookie]) {
        var request = request
        let headers = HTTPCookie.requestHeaderFields(with: cookies)
        for (name, value) in headers {
            request.addValue(value, forHTTPHeaderField: name)
        }

        load(request)
    }
}

теперь это будет просто:

let request = URLRequest(url: URL(string: "https://example.com/")!)
let webView = WKWebView(frame: self.view.frame)
webView.load(request, with: cookies)

это расширение также доступно в LionheartExtensions если вы просто хотите падение в решении. Ура!

причина опубликованного ответа заключается в том, что я пробовал много решений, но не работал должным образом большая часть ответа не работает в первый раз, пожалуйста, используйте мое решение, оно работает как для iOS больше 11.0, так и меньше iOS 11 до 8.0

для iOS >= 11.0 -- Swift 4.2

Get http cookies и в wkwebview куки-магазин, как этот путь, это очень сложный момент, чтобы загрузить ваш запрос в wkwebview, необходимо отправить запрос загрузка, когда куки будут установлены полностью, вот функция, которую я написал.

вызов функции с закрытие в завершении вы вызываете load webview. К вашему сведению эта функция обрабатывает только iOS >= 11.0

self.WwebView.syncCookies {
    if let request = self.request {
       self.WwebView.load(request)
    }
}

вот реализация для syncCookies

лучшее исправление для запросов XHR показано здесь

Swift 4 версия:

func webView(_ webView: WKWebView, decidePolicyFor navigationResponse: WKNavigationResponse, decisionHandler: @escaping (WKNavigationResponsePolicy) -> Swift.Void) {
    guard
        let response = navigationResponse.response as? HTTPURLResponse,
        let url = navigationResponse.response.url
    else {
        decisionHandler(.cancel)
        return
    }

    if let headerFields = response.allHeaderFields as? [String: String] {
        let cookies = HTTPCookie.cookies(withResponseHeaderFields: headerFields, for: url)
        cookies.forEach { (cookie) in
            HTTPCookieStorage.shared.setCookie(cookie)
        }
    }

    decisionHandler(.allow)
}

Если кто-то использует Alamofire, то это лучшее решение.

  let cookies = Alamofire.SessionManager.default.session.configuration.httpCookieStorage?.cookies(for: URL(string: BASE_URL)!)
  for (cookie) in cookies ?? [] {
      webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
  }