ios - ¿Puedo configurar las cookies para que las use WKWebView?
uiwebview ios8 (11)
Actualización de 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)
}
}
Estoy intentando cambiar una aplicación existente de UIWebView a WKWebView. La aplicación actual administra el inicio de sesión / sesión de los usuarios fuera de la vista web y establece las cookies necesarias para la autenticación en el NSHTTPCookieStore. Lamentablemente, WKWebView nuevo no utiliza las cookies de NSHTTPCookieStorage. ¿Hay alguna otra manera de lograr esto?
Aquí está mi versión de la solución de Mattrs en Swift para inyectar todas las cookies de HTTPCookieStorage. Esto se hizo principalmente para inyectar una cookie de autenticación para crear una sesión de usuario.
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
}
Después de jugar con esta respuesta (que fue fantásticamente útil :) tuvimos que hacer algunos cambios:
- Necesitamos las vistas web para tratar con múltiples dominios sin filtrar la información de cookies privada entre esos dominios
- Lo necesitamos para honrar las cookies seguras
- Si el servidor cambia el valor de una cookie, queremos que nuestra aplicación lo sepa en
NSHTTPCookieStorage
- Si el servidor cambia el valor de una cookie, no queremos que nuestras secuencias de comandos la restablezcan a su valor original cuando sigas un enlace / AJAX, etc.
Entonces modificamos nuestro código para que sea esto;
Creando una solicitud
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...
Esto garantiza que la primera solicitud tenga las cookies correctas establecidas, sin enviar cookies del almacenamiento compartido que sean para otros dominios, y sin enviar cookies seguras a una solicitud insegura.
Tratar con más solicitudes
También debemos asegurarnos de que otras solicitudes tengan las cookies establecidas. Esto se hace usando una secuencia de comandos que se ejecuta en la carga de documentos que verifica si hay un conjunto de cookies y, en caso NSHTTPCookieStorage
en el valor en 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];
Hacer frente a los cambios de cookies
También tenemos que tratar con el servidor cambiando el valor de una cookie. Esto significa agregar otra secuencia de comandos para devolver la llamada de la vista web que estamos creando para actualizar nuestro 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"];
e implementando el método delegado para actualizar cualquier cookie que haya cambiado, ¡asegurándonos de que solo estamos actualizando las cookies del dominio actual!
- (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];
}
}
}
Esto parece solucionar nuestros problemas de cookies sin que tengamos que ocuparnos de cada lugar en el que utilizamos WKWebView de manera diferente. Ahora podemos usar este código como ayudante para crear nuestras vistas web y actualiza de manera transparente NSHTTPCookieStorage
para nosotros.
EDITAR: Resulta que utilicé una categoría privada en NSHTTPCookie - aquí está el código:
- (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;
}
Después de revisar varias respuestas aquí y no tener éxito, requestHeaderFields
la documentación de WebKit y tropecé con el método static HTTPCookie
en HTTPCookie
, que convierte una matriz de cookies en un formato adecuado para un campo de encabezado. Combinar esto con la idea de mattr de actualizar URLRequest
antes de cargarlo con los encabezados de las cookies me permitió cruzar la línea de meta.
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)
Para hacer esto aún más simple, usa una extensión:
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)
}
}
Ahora solo se convierte en:
let request = URLRequest(url: URL(string: "https://example.com/")!)
let webView = WKWebView(frame: self.view.frame)
webView.load(request, with: cookies)
Esta extensión también está disponible en LionheartExtensions si solo desea una solución de acceso LionheartExtensions . ¡Aclamaciones!
En iOS 11, puede administrar cookies ahora :), consulte esta sesión: https://developer.apple.com/videos/play/wwdc2017/220/
Encuentre la solución que muy probablemente funcionará para usted de la caja. Básicamente está modificado y actualizado para la answer de Swift 4 @ user3589213 .
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)
}
}
La mejor solución para solicitudes XHR se muestra here
Swift 4 versión:
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)
}
Las cookies deben configurarse en la configuración antes de que se WKWebView
. De lo contrario, incluso con el controlador de finalización de setCookie
, las cookies no se sincronizarán de manera confiable con la vista web. Esto vuelve a esta línea desde los docs en WKWebViewConfiguration
@NSCopying var configuration: WKWebViewConfiguration { get }
Ese @NSCopying
es algo así como una copia profunda. La implementación está más allá de mí, pero el resultado final es que a menos que establezca las cookies antes de inicializar la vista web, no puede contar con que las cookies estén allí. Esto puede complicar la arquitectura de la aplicación porque no inicializar una vista se convierte en un proceso asincrónico. Terminarás con algo como esto
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)
}
}
}
y luego usarlo algo así como
override func loadView() {
view = UIView()
WKWebViewConfiguration.cookiesIncluded { [weak self] config in
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.load(request)
self.view = webView
}
}
El ejemplo anterior aplaza la creación de la vista hasta el último momento posible, otra solución sería crear la configuración o la vista web con anticipación y manejar la naturaleza asíncrona antes de la creación de un controlador de vista.
Una nota final: una vez que creas esta vista web, la has dejado libre, no puedes agregar más cookies sin usar los métodos que se describen en esta respuesta . Sin embargo, puede usar la WKHTTPCookieStoreObserver
para, al menos, observar los cambios que suceden a las cookies. Por lo tanto, si una cookie de sesión se actualiza en la vista web, puede actualizar manualmente HTTPCookieStorage
del sistema con esta nueva cookie, si lo desea.
Para obtener más información al respecto, salte a las 18:00 en esta carga de contenido web personalizado de la sesión WWDC de 2017 . Al comienzo de esta sesión, hay una muestra engañosa de código que omite el hecho de que la vista web debe crearse en el controlador de finalización.
cookieStore.setCookie(cookie!) {
webView.load(loggedInURLRequest)
}
La demostración en vivo a las 18:00 aclara esto.
establecer la cookie
self.webView.evaluateJavaScript("document.cookie=''access_token=your token'';domain=''your domain'';") { (data, error) -> Void in
self.webView.reload()
}
eliminar cookie
self.webView.evaluateJavaScript("document.cookie=''access_token='';domain=''your domain'';") { (data, error) -> Void in
self.webView.reload()
}
trabaja para mi
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)
}
}
Editar solo para iOS 11+
Use 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)
Como los está sacando de HTTPCookeStorage, puede hacer esto:
let cookies = HTTPCookieStorage.shared.cookies ?? []
for (cookie) in cookies {
webView.configuration.websiteDataStore.httpCookieStore.setCookie(cookie)
}
Respuesta anterior para iOS 10 y debajo
Si requiere que sus cookies se configuren en la solicitud de carga inicial, puede configurarlas en NSMutableURLRequest. Como las cookies son solo un encabezado de solicitud especialmente formateado, esto se puede lograr así:
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];
Si necesita que las solicitudes posteriores de AJAX en la página tengan sus cookies establecidas, esto se puede lograr simplemente usando WKUserScript para establecer los valores mediante programación mediante javascript al inicio del documento de la siguiente manera:
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];
La combinación de estas dos técnicas debería brindarle las herramientas suficientes para transferir los valores de las cookies de Native App Land a Web View Land. Puede encontrar más información en la aplicación cookie javascript en la página de mozilla , si necesita algunas cookies más avanzadas.
Sí, apesta que Apple no respalde muchas de las sutilezas de UIWebView . No estoy seguro si alguna vez los apoyarán, pero espero que lo hagan pronto. ¡Espero que esto ayude!