WKWebView设置保持session免登陆
WKWebView概述
• WKWebView是苹果在WWDC 2014 上推出的新一代WebView组件,相比iOS8及以前的UIWebView拥有更明显的优势
1. 更多的支持HTML5的特性
2. 高达60fps的滚动刷新率以及内置手势
3. 将UIWebViewDelegate与UIWebView拆分成了14类和3个协议
4. Safari相同的JS引擎
5. 占用更少的内存
首先简单熟悉一下WKWebView的属性方法
// webview 配置
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
//配置初始化方法
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
//WKPreferences偏好设置
config.preferences = [[WKPreferences alloc] init];
// 默认为0
config.preferences.minimumFontSize = 10;
// 默认认为YES
config.preferences.javaScriptEnabled = YES;
// 在iOS上默认为NO,表示不能自动通过窗口打开
config.preferences.javaScriptCanOpenWindowsAutomatically = NO;
// 导航代理
@property (nullable, nonatomic, weak) id <WKNavigationDelegate>navigationDelegate;
// 用户交互代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;
// 与UIWebView一样的加载请求API
(nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
// 直接加载HTML
(nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
// 直接加载data
(nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString*)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0);
// 停止加载数据
(void)stopLoading;
// 执行JS代码
(void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;
• JS和WebView内容交互
// 只读属性,所有添加的WKUserScript都在这里可以获取到
@property (nonatomic, readonly, copy) NSArray<WKUserScript *> *userScripts;
// 注入JS
(void)addUserScript:(WKUserScript *)userScript;
// 移除所有注入的JS
(void)removeAllUserScripts;
// 添加scriptMessageHandler到所有的frames中,则都可以通过
// window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
// 发送消息
// JS要调用原生的方法的方式
(void)addScriptMessageHandler:(id<WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
// 根据name移除所注入的scriptMessageHandler
(void)removeScriptMessageHandlerForName:(NSString *)name;
• WKUserScript
在WKUserContentController中,所有使用到WKUserScript。WKUserContentController是用于与JS交互的类,而所注入的JS是WKUserScript对象。它的所有属性和方法如下:
// JS源代码
@property (nonatomic, readonly, copy) NSString *source;
// JS注入时间
@property (nonatomic, readonly) WKUserScriptInjectionTime injectionTime;
// 只读属性,表示JS是否应该注入到所有的frames中还是只有main frame.
@property (nonatomic, readonly, getter=isForMainFrameOnly) BOOLforMainFrameOnly;
// 初始化方法,用于创建WKUserScript对象
// source:JS源代码
// injectionTime:JS注入的时间
// forMainFrameOnly:是否只注入main frame
(instancetype)initWithSource:(NSString *)source injectionTime:(WKUserScriptInjectionTime)injectionTime forMainFrameOnly:(BOOL)forMainFrameOnly;
• WKNavigationDelegate
@protocol WKNavigationDelegate <NSObject>
@optional
// 决定导航的动作,通常用于处理跨域的链接能否导航。WebKit对跨域进行了安全检查限制,不允许跨域,因此我们要对不能跨域的链接
// 单独处理。但是,对于Safari是允许跨域的,不用这么处理。
// 这个是决定是否Request
(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
// 决定是否接收响应
// 这个是决定是否接收response
// 要获取response,通过WKNavigationResponse对象获取
(void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
// 当main frame的导航开始请求时,会调用此方法
(void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
// 当main frame接收到服务重定向时,会回调此方法
(void)webView:(WKWebView *)webViewdidReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation*)navigation;
// 当main frame开始加载数据失败时,会回调
(void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
// 当main frame的web内容开始到达时,会回调
(void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;
// 当main frame导航完成时,会回调
(void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
// 当main frame最后下载数据失败时,会回调
(void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
// 这与用于授权验证的API,与AFN、UIWebView的授权验证API是一样的
(void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler;
// 当web content处理完成时,会回调
(void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);
@end
typedef NS_ENUM(NSInteger, WKNavigationActionPolicy) {
WKNavigationActionPolicyCancel,
WKNavigationActionPolicyAllow,
} NS_ENUM_AVAILABLE(10_10, 8_0);
初步解决方案
其实主要要做的只有两步,1、获取Cookie,2、注入Cookie
1、获取Cookie
• 由于 WKWebView 的 Cookie 存储容器 WKWebsiteDataStore 是私有存储,所以无法从这里获取到Cookie,目前的方法是(1)从网站返回的 response headerfields 中获取。(2)通过调用js的方法获取 cookie。
• (1)从网站返回的 response headerfields 中获取
• 因为cookie都存在http respone的headerfields,找到能获得respone的WKWebView回调,打印
(void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
NSHTTPURLResponse *response = (NSHTTPURLResponse *)navigationResponse.response;
NSArray *cookies =[NSHTTPCookie cookiesWithResponseHeaderFields:[response allHeaderFields] forURL:response.URL];
//读取wkwebview中的cookie 方法1
for (NSHTTPCookie *cookie in cookies) {
// [[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
NSLog(@"wkwebview中的cookie:%@", cookie);
}
//读取wkwebview中的cookie 方法2 读取Set-Cookie字段
NSString *cookieString = [[response allHeaderFields] valueForKey:@"Set-Cookie"];
NSLog(@"wkwebview中的cookie:%@", cookieString);
//看看存入到了NSHTTPCookieStorage了没有
NSHTTPCookieStorage *cookieJar2 = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in cookieJar2.cookies) {
NSLog(@"NSHTTPCookieStorage中的cookie%@", cookie);
}
decisionHandler(WKNavigationResponsePolicyAllow);
}
• (2)通过调用js的方法获取 cookie。
(void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation
{
[webView evaluateJavaScript:[NSString stringWithFormat:@"document.cookie"] completionHandler:^(id _Nullable response, NSError * _Nullable error) {
if (response != 0) {
NSLog(@"\n\n\n\n\n\n document.cookie%@,%@",response,error);
}
}];
}
第2种方法获取的cookie并不全,所以我在项目中并不是通过这个获取的。
只能通过第1种方式去获取cookie,其实主要还是JSESSIONID,只要把JSESSIONID获取并保存了,下次打开app时就能用JSESSIONID来重新连接服务器的会话。
2、注入Cookie
• 注入 Cookie 就是从之前保存 cookie 的 NSHTTPCookieStorage 中取出相关 Cookie,然后在再次请求访问的时候在 request 中注入 Cookie。注入Cookie同样有多种方式。
• (1)JS注入1
//取出 storage 中的cookie并将其拼接成正确的形式
NSArray<NSHTTPCookie *> *tmp = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
NSMutableString *jscode_Cookie = [@"" mutableCopy];
[tmp enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
NSLog(@"%@ = %@", obj.name, obj.value);
[jscode_Cookie appendString:[NSString stringWithFormat:@"document.cookie = '%@=%@';", obj.name, obj.value]];
}];
NSMutableURLRequest *requestObj = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: jscode_Cookie injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
webViewConfig.userContentController = userContentController;
WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];
[requestObj setHTTPShouldHandleCookies:NO];
[_webView loadRequest:requestObj];
• (2)JS注入2
(void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
[webView evaluateJavaScript:@"document.cookie ='TeskCookieKey1=TeskCookieValue1';" completionHandler:^(id result, NSError *error) {
//...
}];
}
• (3) NSMutableURLRequest 注入Cookie
NSURL *url = request.URL;
NSMutableString *cookies = [NSMutableString string];
NSMutableURLRequest *requestObj = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
NSArray *tmp = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
NSDictionary *dicCookies = [NSHTTPCookie requestHeaderFieldsWithCookies:tmp];
NSString *cookie = [self readCurrentCookie];
[requestObj setValue:cookie forHTTPHeaderField:@"Cookie"];
[_webView loadRequest:requestObj];
-(NSString *)readCurrentCookie{
NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieJar cookies]) {
[cookieDic setObject:cookie.value forKey:cookie.name];
}
// cookie重复,先放到字典进行去重,再进行拼接
for (NSString *key in cookieDic) {
NSString *appendString = [NSString stringWithFormat:@"%@=%@;", key, [cookieDic valueForKey:key]];
[cookieValue appendString:appendString];
}
return cookieValue;
}
js注入这三种方式,亲测,只有第一种方式是成功的,而且requestObj setHTTPShouldHandleCookies:NO;这句话非常重要,意思是设置request的cookie是否跟随request请求发送和存储给默认的cookie管理。因为是在请求之前要将sessionId注入给浏览器然后发送给服务器,那么就要设置为NO,设置为NO的话,那浏览器默认打开时生成的cookie(sessionid)将不会跟随web一起发送给服务器,而通过NSMutableURLRequest带注入的cookie发送给服务器,同时将cookie设置到cookie管理器。
AFNetWork获取cookie
由于项目中用到了AFNetWork调取服务器接口,而且首次调用就会生成session,所以我想让AFNetWork生成的cookie与wkwebview共享,则只要从AFNetWork获取sessionid,然后再通过wkwebview调用服务器页面时,将session注入进去然后带给服务器就可以了。
通过如下代码获取cookie:
1,从cookie管理器中获取cookie(AFNetwork已经将cookie存入了标准的cookie管理器中,而wkwebview是不会存在标准的Http cookie管理中)
-(void)saveSession:(NSURL *)url{
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL: url];
NSLog(@"request cookies:%@",cookies);
for (NSHTTPCookie *cookie in cookies) {
if ([K_SESSIONID isEqualToString:cookie.name] ) {
[[NSUserDefaults standardUserDefaults] setValue:cookie.value forKey:K_SESSIONID];
[[NSUserDefaults standardUserDefaults] synchronize];
break;
}
}
}
2, 注入进去
通过上面第一种方式就行了。
Cordova集成保持cookie免登陆
WFWKWebViewEngine.h:
import "CDVWKWebViewEngine.h" import "CDVWebViewEngineProtocol.h" @interface WFWKWebViewEngine : CDVWKWebViewEngine @end
WFWKWebViewEngine.m:
// // WFWKWebViewEngine.m // wkwvtest // // Created by sam.hu on 2017/6/3. // // import "WFWKWebViewEngine.h" import "CDVWKProcessPoolFactory.h" import "WFSessionManager.h" @implementation WFWKWebViewEngine -(void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation{ [super webView:webView didStartProvisionalNavigation:navigation]; } -(id)loadRequest:(NSURLRequest *)request{ NSMutableURLRequest *req = [request mutableCopy]; [req setHTTPShouldHandleCookies:NO]; return [super loadRequest:req]; } -(void)pluginInitialize{ [super pluginInitialize]; NSString *cookieValue= [[WFSessionManager instanceManager] readCurrentCookie]; if (cookieValue) { //添加在js中操作的对象名称,通过该对象来向web view发送消息 WKWebView *webView = (WKWebView *)self.engineWebView; WKUserScript * cookieScript = [[WKUserScript alloc]initWithSource:cookieValue injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; \[webView.configuration.userContentController addUserScript:cookieScript]; webView.configuration.processPool = [[CDVWKProcessPoolFactory sharedFactory] sharedProcessPool]; } } -(void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{ decisionHandler(WKNavigationResponsePolicyAllow); } (void)webView:(WKWebView *)webView didFinishNavigation:(null\_unspecified WKNavigation *)navigation{ [super webView:webView didFinishNavigation:navigation]; [self.commandDelegate evalJs:@"getAppType('isApp','ios')"]; } @end
session管理工具:
import <Foundation/Foundation.h>
@interface WFSessionManager : NSObject
+(instancetype)instanceManager;
-(NSString *)readCurrentCookie;
-(void)saveSession:(NSURL *)url;
-(void)reset;
@end
WFSessionManager.m :
import "WFSessionManager.h"
define K_SESSIONID @"JSESSIONID"
@implementation WFSessionManager
+(instancetype)instanceManager{
static dispatch_once_t onceToken;
static WFSessionManager * manager = nil;
dispatch_once(&onceToken, ^{
manager = [[[self class] alloc] init];
});
return manager;
}
-(NSString *)readCurrentCookie{
NSString* sessionId = [[NSUserDefaults standardUserDefaults] valueForKey:K_SESSIONID];
NSLog(@"cookie dictionary found is %@",sessionId);
if (sessionId) {
NSString *jssessionCookie = [NSString stringWithFormat:@"document.cookie = '%@=%@';", K_SESSIONID, sessionId];
return jssessionCookie;
}
return nil;
}
-(void)saveSession:(NSURL *)url{
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL: url];
NSLog(@"request cookies:%@",cookies);
for (NSHTTPCookie *cookie in cookies) {
if ([K_SESSIONID isEqualToString:cookie.name] ) {
[[NSUserDefaults standardUserDefaults] setValue:cookie.value forKey:K_SESSIONID];
[[NSUserDefaults standardUserDefaults] synchronize];
break;
}
}
}
-(void)reset {
[[NSUserDefaults standardUserDefaults] removeObjectForKey:K_SESSIONID];
[[NSUserDefaults standardUserDefaults] synchronize];
}
@end
wk做离线缓存
wkwebview本身不支持缓存,也不支持对资源文件的拦截。
网络辟谣
在做wkwebview离线缓存时,在网上看到有人说wkwebview直接支持cache,说什么只要设置:request setCachePolicy:NSURLRequestReturnCacheDataDontLoad;这样的缓存策略就行了,但我告诉你,这是瞎扯,说这种话的人都不懂。
webview怎么拦截
- webview是通过注册自定义的NSURLProtocol来做资源文件拦截
- 有人做测试发现wkwebview其实也是留了这个口子,只是没有实现类而已
- 使用了WKBrowsingContextController和registerSchemeForCustomProtocol。 通过反射的方式拿到了私有的 class/selector。通过kvc取到browsingContextController。通过把注册把 http 和 https 请求交给 NSURLProtocol 处理。
具体参考我的github:[demo][3]
ajax请求post的body丢失问题
- 如果post的大部分body内部不是特别多,可以用request的head的方式解决
- 但对于大的body数据,单纯用head的方式是无法解决的
对于第一种情况我现在不讨论,只说第二种情况,让wkwebview完美的支持post所有请求:
- 将body在ajax之前通过与原生通信,将body传给app
- app拿到body后先存到内存
- ajax请求后,在截取的request中,将body拿出设置进去
html5端执行如下代码发送body的json数据给app:
window.webkit.messageHandlers.ajaxToWKWeb.postMessage({body: ‘hello world!’,reqkey:’请求随机数’});
其中ajaxToWKWeb是ios端wkwebview注册的方法。
注册代码如下:
config.userContentController = [[WKUserContentController alloc] init];
// 注入JS对象名称ajaxToWKWeb,当JS通过ajaxToWKWeb来调用时,我们可以在WKScriptMessageHandler代理中接收到
[config.userContentController addScriptMessageHandler:self name:@"ajaxToWKWeb"];
pragma mark - WKScriptMessageHandler
(void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"ajaxToWKWeb"]) {
// 打印所传过来的参数,只支持NSNumber, NSString, NSDate, NSArray,
// NSDictionary, and NSNull类型
//do something
NSLog(@"%@", message.body);
}
}
这种方式传输的数据内容大小限制为2M左右,如果有超过的将会被截掉,如果非要一次性传输这么大的数据量,可以分段传输。
[3]: https://github.com/huguiqi/NSURLProtocol-WebKitSupport “demo”