iOS开发实战 - Cookie注入
Cookie注入的使用场景:
在开发中,我们常常会遇到这样一种场景:通过在一部分网络请求中注入Cookie信息让后台动态校验当前用户的登录状态以及用户权限。
- 在APP中打开一个需要登录用户才能看的页面,一般客户端会先判断是否登录,如果没有登录去登录。缺点每次都要判断,如果是付费内容,还要引导用户去支付,这些都要去后台发起多个请求,去判断,增加了网络开销,如果逻辑处理的不够严谨,很容易出错。
- Cookie的注入可以解决上面的问题,一次请求将用户信息发送给后台,让后台判断给你什么数据,你只用按约定判断返回的字段,做出相应的处理即可,这样减少了用户请求的次数,减轻了服务器的负载,也省了用户的流量。
第一步:获取登录成功的请求标识:如:PHPSESSID=495njbid8as3o79bjqh1mocl22
注:"PHPSESSID"这个字段名对于不同的服务器,配置不同名称也不同,你可以询问后台,不过我建议还是自己去抓包查看,这里我使用的抓包工具是:Charles,网上有破解版的(你也可以使用postman, 免费的,网页版和客户端都有)。
一般客户端会在登录成功的回调里面获取Set-Cookie,对Set-Cookie处理获取内部的"PHPSESSID=495njbid8as3o79bjqh1mocl22"
我们先通过抓包工具来看看登录成功返回的信息:
登录成功抓包
1. 查看一下Cookie信息(包含Set-Cookie信息)
Cookie信息注意:这里我们可以看到三条Set-Cookie信息,这是服务器端告诉你下次请求的时候需要在请求头中加入这三条信息。只是第二条和第三条是用户的账户名和经过MD5加密的密码,这个我们就是我们上传给服务器的,不用提取,不过需要按照"topmasteruserName=17737199679;"这种服务器要求的格式拼接Cookie字符串。
2. 我们再来看看客户端获取的Set-Cookie信息(由请求响应Cookie中三条Set-Cookie拼接成的),需要注意的是每次获取到的Set-Cookie字符串中它们的排列顺序不同:
获取Set-Cookie3. 得到这个字符串后,就是想办法提取PHPSESSID=495njbid8as3o79bjqh1mocl22了(后面的path=/等不需要)
提取PHPSESSID4. 保存信息到本地
保存用户信息5. 拼接Cookie字符串
拼接Cookie字符串6. 到这里需要向服务器注入的Cookie已经准备好了,只需要在发起请求的时候设置请求头的Cookie即可;
(1)AFNetworking 注入Cookie
[manager.requestSerializer setValue:[FunctionFast getCookieFromUserDefault] forHTTPHeaderField:@"Cookie"];
(2)UIWebView中请求注入Cookie,一般公司限制会员/付费用户才能查看
//首次请求注入cookie
- (void)requestData {
NSURL *url = [NSURL URLWithString:self.url];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
// 注入Cookie
[request setValue:[FunctionFast getCookieFromUserDefault] forHTTPHeaderField:@"Cookie"];
[self.webView loadRequest:request];
}
//该webview后续请求url时(同域)调用
- (void)loadRequestWithUrlString:(NSString *)urlString {
// 在此处获取返回的cookie
NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieStorage 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];
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request addValue:cookieValue forHTTPHeaderField:@"Cookie"];
[self.wkWebView loadRequest:request];
}
(3)WKWebView中请求注入Cookie
参考资料:http://www.jianshu.com/p/541d46671448
WKWebView相比于UIWebView:(iOS8.0+)
①WKWebView 是苹果在 WWDC 2014 上推出的新一代 webView 组件,用以替代 UIKit 中笨重难用、内存泄漏的 UIWebView;WKWebView 拥有60fps滚动刷新率、和 safari 相同的 JavaScript 引擎等优势。
②速度快了一倍,内存却减少为原来的一半
③cookie不再是自动携带,需要手动设置
④交互更加顺畅,比如app底部四个tabBar也都是网页的,在UIWebView下点击,整个H5页都会闪白一下,但是在WKWebView下点击,四个tabBar效果与原生app效果更加类似,不会有闪白现象;
⑤WKWebView 自诩拥有更快的加载速度,更低的内存占用,但实际上 WKWebView 是一个多进程组件,Network Loading 以及 UI Rendering 在其它进程中执行。初次适配 WKWebView 的时候,我们也惊讶于打开 WKWebView 后,App 进程内存消耗反而大幅下降,但是仔细观察会发现,Other Process 的内存占用会增加。在一些用 webGL 渲染的复杂页面,使用 WKWebView 总体的内存占用(App Process Memory + Other Process Memory)不见得比 UIWebView 少很多。
⑥在 UIWebView 上当内存占用太大的时候,App Process 会 crash;而在 WKWebView 上当总体的内存占用比较大的时候,WebContent Process 会 crash,从而出现白屏现象。在 WKWebView 中加载下面的测试链接可以稳定重现白屏现象:
http://people.mozilla.org/~rnewman/fennec/mem.html
这个时候 WKWebView.URL 会变为 nil, 简单的 reload 刷新操作已经失效,对于一些长驻的H5页面影响比较大。
⑦增减了一些代理方法,更方便的进行协议拦截和进度条展示
补充:
业界普遍认为 WKWebView 拥有自己的私有存储,不会将 Cookie 存入到标准的 Cookie 容器NSHTTPCookieStorage中。
实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或 服务器 Set-Cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中,FireFox 工程师曾建议通过 reset WKProcessPool 来触发 Cookie 同步到 NSHTTPCookieStorage 中,实践发现不起作用,并可能会引发当前页面 session cookie 丢失等问题。
WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。
WKProcessPool
苹果开发者文档对 WKProcessPool 的定义是:A WKProcessPool object represents a pool of Web Content process. 通过让所有 WKWebView 共享同一个 WKProcessPool 实例,可以实现多个 WKWebView 之间共享 Cookie(session Cookie and persistent Cookie)数据。不过 WKWebView WKProcessPool 实例在 app 杀进程重启后会被重置,导致 WKProcessPool 中的 Cookie、session Cookie 数据丢失,目前也无法实现 WKProcessPool 实例本地化保存。
由于许多 H5 业务都依赖于 Cookie 作登录动态校验,而 WKWebView 上请求不会自动携带 Cookie, 目前的主要解决方案是:
!!添加之前(登录时)保存好的cookie或者拼接cookie所需的相关字符串
第一种:
和上面的UIWebView一样, 在loadRequest 前,在 request header 中设置 Cookie, 解决首个请求 Cookie 带不上的问题;
- (void)requestData {
NSURL *url = [NSURL URLWithString:self.url];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:10];
// 注入Cookie
[request setValue:[FunctionFast getCookieFromUserDefault] forHTTPHeaderField:@"Cookie"];
[self.wkWebView loadRequest:request];
}
//该webview调用其他url时(同域)调用
- (void)loadRequestWithUrlString:(NSString *)urlString {
// 在此处获取返回的cookie
NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieStorage 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];
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request addValue:cookieValue forHTTPHeaderField:@"Cookie"];
[self.wkWebView loadRequest:request];
}
第二种:
通过 document.cookie 设置 Cookie 解决后续页面(同域)Ajax、iframe 请求的 Cookie 问题(注意:document.cookie() 无法跨域设置 cookie)
补充:
比如,第一个请求是 www.a.com,我们通过在 request header 里带上 Cookie 解决该请求的 Cookie 问题,接着页面跳转到 www.b.com,这个时候 www.b.com 这个请求就可能因为没有携带 cookie 而无法访问。当然,由于每一次页面跳转前都会调用回调函数:
— (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
可以在该回调函数里拦截请求,copy request,在 request header 中带上 cookie 并重新 loadRequest。不过这种方法依然解决不了页面 iframe 跨域请求的 Cookie 问题,毕竟-[WKWebView loadRequest:] 只适合加载 mainFrame 请求。
//注入cookie: 在创建WKWebView之前的时候将cookie信息存放到WKUserScript中
- (void)injectCookie {
//创建webview配置对象
WKWebViewConfiguration *webConfig = [[WKWebViewConfiguration alloc] init];
// 设置偏好设置
webConfig.preferences = [[WKPreferences alloc] init];
// 默认为0
webConfig.preferences.minimumFontSize = 10;
// 默认认为YES
webConfig.preferences.javaScriptEnabled = YES;
// 在iOS上默认为NO,表示不能自动通过窗口打开
webConfig.preferences.javaScriptCanOpenWindowsAutomatically = NO;
// web内容处理池
webConfig.processPool = [[WKProcessPool alloc] init];
// 将所有cookie以document.cookie = 'key=value';形式进行拼接
NSUserDefaults *defaults = [NSUserDefaults standardUserDefaults];
#warning 然而这里的单引号一定要注意是英文的
//格式 @"document.cookie = 'key1=value1';document.cookie = 'key2=value2'";
NSString *cookie = [NSString stringWithFormat: @"document.cookie = '%@';document.cookie = 'topmasteruserName=%@';document.cookie = 'topmasteruserCode=%@;'", [defaults valueForKey:@"PHPSESSID"], [defaults valueForKey:@"loginName"], [defaults valueForKey:@"ticket"]];
// 加cookie给h5识别,表明在iOS端打开该地址
WKUserContentController* userContentController = WKUserContentController.new;
WKUserScript * cookieScript = [[WKUserScript alloc]
initWithSource: cookie
injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
webConfig.userContentController = userContentController;
self.wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height) configuration:webConfig];
[self.view addSubview:_wkWebView];
_wkWebView.UIDelegate = self;
_wkWebView.navigationDelegate = self;
}
//该webview中调用其他url时(同域)调用
- (void)loadRequestWithUrlString:(NSString *)urlString {
// 在此处获取返回的cookie
NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieStorage 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];
}
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:urlString]];
[request addValue:cookieValue forHTTPHeaderField:@"Cookie"];
[self.wkWebView loadRequest:request];
}
到这里已经满足基本需求了,如有错误的地方,欢迎亲们指正。