I'm trying to switch an existing app from UIWebView to WKWebView. The current app manages the users login / session outside of the web view and sets the COOKIEs required for authentication into the the NSHTTPCOOKIEStore. Unfortunately new WKWebView doesn't use the COOKIEs from the NSHTTPCOOKIEStorage. Is there another way to achieve this?
我想把一个现有的应用从UIWebView切换到WKWebView。当前应用程序管理web视图之外的用户登录/会话,并将验证所需的COOKIE设置为NSHTTPCOOKIEStore。不幸的是,新的WKWebView不使用来自NSHTTPCOOKIEStorage的COOKIE。还有其他方法可以实现这一点吗?
86
Edit for iOS 11+ only
只对ios11 +进行编辑
Use WKHTTCOOKIEStore:
使用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)
Since you are pulling them over from HTTPCookeStorage, you can do this:
因为您正在从HTTPCookeStorage将它们拉过来,所以您可以这样做:
let COOKIEs = HTTPCOOKIEStorage.shared.COOKIEs ?? []
for (COOKIE) in COOKIEs {
webView.configuration.websiteDataStore.httpCOOKIEStore.setCOOKIE(COOKIE)
}
Old answer for iOS 10 and below
iOS 10和以下的旧答案
If you require your COOKIEs to be set on the initial load request, you can set them on NSMutableURLRequest. Because COOKIEs are just a specially formatted request header this can be achieved like so:
如果您要求在初始加载请求上设置COOKIE,您可以在NSMutableURLRequest上设置它们。因为COOKIE只是一个特殊格式的请求头,所以可以这样实现:
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];
If you require subsequent AJAX requests on the page to have their COOKIEs set, this can be achieved by simply using WKUserScript to set the values programmatically via Javascript at document start like so:
如果需要对页面上的后续AJAX请求设置COOKIE,可以通过简单地使用WKUserScript在document start时通过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];
Combining these two techniques should give you enough tools to transfer COOKIE values from Native App Land to Web View Land. You can find more info on the COOKIE Javascript api on mozilla's page, if you require some more advanced COOKIEs.
结合这两种技术,您将有足够的工具将COOKIE值从本地应用程序域传输到Web视图域。如果您需要一些更高级的COOKIE,您可以在mozilla页面上找到关于COOKIE Javascript api的更多信息。
Yeah it sucks that Apple is not supporting many of the niceties of UIWebView. Not sure if they will ever support them, but hopefully they will get on this soon. Hope this helps!
是的,苹果没有支持UIWebView的很多优点,这太糟糕了。不确定他们是否会支持他们,但希望他们很快就能做到。希望这可以帮助!
53
After playing with this answer (which was fantastically helpful :) we've had to make a few changes:
在尝试了这个答案(非常有用)之后,我们必须做一些改变:
NSHTTPCOOKIEStorage
So we modified our code to be this;
我们把代码修改成这样;
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...
This makes sure that the first request has the correct COOKIEs set, without sending any COOKIEs from the shared storage that are for other domains, and without sending any secure COOKIEs into an insecure request.
这样可以确保第一个请求设置了正确的COOKIE,而不会从共享存储中发送任何其他域的COOKIE,也不会向不安全请求发送任何安全COOKIE。
We also need to make sure that other requests have the COOKIEs set. This is done using a script that runs on document load which checks to see if there is a COOKIE set and if not, set it to the value in NSHTTPCOOKIEStorage
.
我们还需要确保其他请求具有COOKIE集。
// 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];
We also need to deal with the server changing a COOKIE's value. This means adding another script to call back out of the web view we are creating to update our NSHTTPCOOKIEStorage
.
我们还需要处理服务器更改COOKIE值的问题。这意味着添加另一个脚本,以从我们正在创建的web view中调用,以更新我们的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"];
and implementing the delegate method to update any COOKIEs that have changed, making sure that we are only updating COOKIEs from the current domain!
并实现委托方法来更新任何已更改的COOKIE,确保我们只更新来自当前域的COOKIE !
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSArray *COOKIEs = [message.body componentsSeparatedByString:@"; "];
for (NSString *COOKIE in COOKIEs) {
// Get this COOKIE's name and value
NSArray *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];
}
}
}
This seems to fix our COOKIE problems without us having to deal with each place we use WKWebView differently. We can now just use this code as a helper to create our webviews and it transparently updates NSHTTPCOOKIEStorage
for us.
这似乎可以解决我们的COOKIE问题,而不必处理我们使用WKWebView的每个地方。我们现在可以使用这个代码作为一个助手来创建我们的webview,它会透明地为我们更新NSHTTPCOOKIEStorage。
EDIT: Turns out I used a private category on NSHTTPCOOKIE - here's the code:
编辑:原来我在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;
}
19
work for me
为我工作
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)
}
}
13
Here is my version of Mattrs solution in Swift for injecting all COOKIEs from HTTPCOOKIEStorage. This was done mainly to inject an authentication COOKIE to create a user session.
这是我在Swift中的Mattrs解决方案版本,用于从HTTPCOOKIEStorage注入所有COOKIE。这主要是为了注入一个身份验证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
}
8
Swift 3 update :
斯威夫特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)
}
}
7
set COOKIE
集饼干
self.webView.evaluateJavascript("document.COOKIE='access_token=your token';domain='your domain';") { (data, error) -> Void in
self.webView.reload()
}
delete COOKIE
删除饼干
self.webView.evaluateJavascript("document.COOKIE='access_token=';domain='your domain';") { (data, error) -> Void in
self.webView.reload()
}
6
In iOS 11, you can manage COOKIE now :), see this session: https://developer.apple.com/videos/play/wwdc2017/220/
在ios11中,您现在可以管理COOKIE:),请参见这个会话:https://developer.apple.com/videos/play/wwdc2017/220/
6
The COOKIEs must be set on the configuration before the WKWebView
is created. Otherwise, even with WKHTTPCOOKIEStore
's setCOOKIE
completion handler, the COOKIEs won't reliably be synced to the web view. This goes back to this line from the docs on WKWebViewConfiguration
在创建WKWebView之前,必须在配置上设置COOKIE。否则,即使使用WKHTTPCOOKIEStore的setCOOKIE完成处理程序,COOKIE也不能可靠地同步到web视图。这可以追溯到WKWebViewConfiguration上的文档
@NSCopying var configuration: WKWebViewConfiguration { get }
That @NSCopying
is somewhat of a deep copy. The implementation is beyond me, but the end result is that unless you set COOKIEs before initializing the webview, you can't count on the COOKIEs being there. This can complicate app architecture because not initializing a view becomes an asynchronous process. You'll end up with something like this
@NSCopying有点像一个深层拷贝。实现超出了我的范围,但是最终的结果是,除非在初始化webview之前设置COOKIE,否则不能指望COOKIE在那里。这可能会使应用程序架构复杂化,因为不初始化视图就会变成一个异步过程。你会得到这样的结果
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)
}
}
}
and then to use it something like
然后使用它
override func loadView() {
view = UIView()
WKWebViewConfiguration.COOKIEsIncluded { [weak self] config in
let webView = WKWebView(frame: .zero, configuration: webConfiguration)
webView.load(request)
self.view = webView
}
}
The above example defers view creation until the last possible moment, another solution would be to create the config or webview well in advance and handle the asynchronous nature before creation of a view controller.
上面的示例将视图创建延迟到可能的最后一刻,另一种解决方案是提前创建配置或webview,并在创建视图控制器之前处理异步特性。
A final note: once you create this webview, you have set it loose into the wild, you can't add more COOKIEs without using methods described in this answer. You can however use the WKHTTPCOOKIEStoreObserver
api to at least observe changes happening to COOKIEs. So if a session COOKIE gets updated in the webview, you can manually update the system's HTTPCOOKIEStorage
with this new COOKIE if desired.
最后一个注意事项:一旦你创建了这个webview,你将它释放到野外,你不能添加更多的COOKIE而不使用这个答案中描述的方法。但是,您可以使用WKHTTPCOOKIEStoreObserver api至少观察到COOKIE发生的变化。因此,如果一个会话COOKIE在webview中被更新,你可以用这个新COOKIE手动更新系统的HTTPCOOKIEStorage。
For more on this, skip to 18:00 at this 2017 WWDC Session Custom Web Content Loading. At the beginning of this session, there is a deceptive code sample which omits the fact that the webview should be created in the completion handler.
关于这方面的更多信息,请跳转到2017年WWDC自定义Web内容加载的18:00。在这个会话的开始,有一个欺骗性的代码示例,它忽略了应该在完成处理程序中创建webview的事实。
COOKIEStore.setCOOKIE(COOKIE!) {
webView.load(loggedInURLRequest)
}
The live demo at 18:00 clarifies this.
18:00现场演示澄清了这一点。
2
Please find the solution which most likely will work for you out of the box. Basically it's modified and updated for Swift 4 @user3589213's answer.
请找出最适合你的解决方案。基本上,它对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)
}
}
1
The better fix for XHR requests is shown here
这里显示了对XHR请求的更好修复
Swift 4 version:
斯威夫特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)
}
0
After looking through various answers here and not having any success, I combed through the WebKit documentation and stumbled upon the requestHeaderFields
static method on HTTPCOOKIE
, which converts an array of COOKIEs into a format suitable for a header field. Combining this with mattr's insight of updating the URLRequest
before loading it with the COOKIE headers got me through the finish line.
在查看了各种各样的答案并没有取得任何成功之后,我梳理了WebKit文档,并在HTTPCOOKIE上发现了requestHeaderFields静态方法,该方法将一组COOKIE转换为适合头字段的格式。结合mattr在使用COOKIE头部加载URLRequest之前更新URLRequest的见解,我完成了这一任务。
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)
To make this even simpler, use an extension:
为了使这更简单,使用扩展:
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)
}
}
Now it just becomes:
现在就变成了:
let request = URLRequest(url: URL(string: "https://example.com/")!)
let webView = WKWebView(frame: self.view.frame)
webView.load(request, with: COOKIEs)
This extension is also available in LionheartExtensions if you just want a drop-in solution. Cheers!
如果您只是想要一个drop-in解决方案,那么在lionheartexties中也可以使用这个扩展。干杯!