本文主要介绍防DNS污染方案在WebView场景下所遇到的一些问题及解决方案。
当前最佳实践文档只针对结合使用时,如何使用HTTPDNS解析出的IP,关于HTTPDNS本身的解析服务,请先查看iOS SDK 开发手册。
WKWebView无法使用NSURLProtocol拦截请求
针对该问题方案如下:
换用UIWebView
换用UIWebView方案不做赘述,说明下使用私有API进行注册拦截的方法 :
// 注册自己的 protocol [NSURLProtocol registerClass:[CustomProtocol class]]; // 创建 WKWebview WKWebViewConfiguration * config = [[WKWebViewConfiguration alloc] init]; WKWebView * wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, [UIScreen mainScreen].bounds.size.width, [UIScreen mainScreen].bounds.size.height) configuration:config]; [wkWebView loadRequest:webViewReq]; [self.view addSubview:wkWebView]; //注册 scheme Class cls = NSClassFromString(@"WKBrowsingContextController"); SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:"); if ([cls respondsToSelector:sel]) { // 通过 http 和 https 的请求,同理可通过其他的 Scheme 但是要满足 ULR Loading System [cls performSelector:sel withObject:@"http"]; [cls performSelector:sel withObject:@"https"]; }
使用私有API的另一风险是兼容性问题,比如上面的
browsingContextController
就只能在iOS 8.4以后才能用,反注册scheme的方法unregisterSchemeForCustomProtocol :
也是在iOS 8.4 以后才被添加进来的,要支持iOS 8.0 ~ 8.3机型的话,只能通过动态生成字符串的方式拿到WKBrowsingContextController
,而且还不能反注册,不过这些问题都不大。至于向后兼容,这个也不用太担心,因为iOS发布新版本之前都会有开发者预览版的,那个时候再测一下也不迟。对于本文的例子来说,如果将来哪个iOS版本移除了这个API,那很可能是因为官方提供了完整的解决方案,到那时候自然也不需要本文介绍的方法了 。注意避免执行太晚,如果在
- (void)viewDidLoad
中注册,可能会因为注册太晚,引发问题。建议在+load
方法中执行 。然后同样会遇到iOS端HTTPS(含SNI)业务场景:IP直连方案说明里提到的各种NSURLProtocol相关的问题,可以参照里面的方法解决。
WebView中的Cookie处理业务场景“IP直连”方案说明
本章节将讨论类似这样的问题:
WKWebView对于Cookie的管理一直是它的短板,那么iOS11是否有改进,如果有,如何利用这样的改进?
采用IP直连方案后,服务端返回的Cookie里的Domain字段也会使用IP。如果IP是动态的,就有可能导致一些问题:由于许多H5业务都依赖于Cookie作登录态校验 ,而WKWebView上请求不会自动携带 Cookie。
WKWebView使用NSURLProtocol拦截请求无法获取Cookie信息
iOS 11推出了新的APIWKHTTPCookieStore
可以用来拦截WKWebView的Cookie信息。
用法示例如下:
WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
// get cookies
[cookieStroe getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
NSLog(@"All cookies %@",cookies);
}];
// set cookie
NSMutableDictionary *dict = [NSMutableDictionary dictionary];
dict[NSHTTPCookieName] = @"userid";
dict[NSHTTPCookieValue] = @"123";
dict[NSHTTPCookieDomain] = @"xxxx.com";
dict[NSHTTPCookiePath] = @"/";
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:dict];
[cookieStroe setCookie:cookie completionHandler:^{
NSLog(@"set cookie");
}];
// delete cookie
[cookieStroe deleteCookie:cookie completionHandler:^{
NSLog(@"delete cookie");
}];
利用iOS 11 API WKHTTPCookieStore解决WKWebView首次请求不携带Cookie的问题
问题说明:由于许多H5业务都依赖于Cookie作登录态校验,而WKWebView上请求不会自动携带 Cookie。比如,如果你在Native层面做了登录操作,获取了Cookie信息,也使用NSHTTPCookieStorage存到了本地,但是使用WKWebView打开对应网页时,网页依然处于未登录状态。如果是登录也在WebView里做的,就不会有这个问题。
iOS 11的API可以解决该问题,只要是存在WKHTTPCookieStore里的cookie,WKWebView每次请求都会携带,存在NSHTTPCookieStorage的cookie,并不会每次都携带。于是会发生首次WKWebView 请求不携带Cookie的问题。
解决方法:
在执行
-[WKWebView loadReques:]
前将NSHTTPCookieStorage
中的内容复制到WKHTTPCookieStore
中,以此来达到WKWebView Cookie 注入的目的。示例代码如下:[self copyNSHTTPCookieStorageToWKHTTPCookieStoreWithCompletionHandler:^{ NSURL *url = [NSURL URLWithString:@"https://www.v2ex.com"]; NSURLRequest *request = [NSURLRequest requestWithURL:url]; [_webView loadRequest:request]; }];
- (void)copyNSHTTPCookieStorageToWKHTTPCookieStoreWithCompletionHandler:(nullable void (^)())theCompletionHandler; { NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]; WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore; if (cookies.count == 0) { !theCompletionHandler ?: theCompletionHandler(); return; } for (NSHTTPCookie *cookie in cookies) { [cookieStroe setCookie:cookie completionHandler:^{ if ([[cookies lastObject] isEqual:cookie]) { !theCompletionHandler ?: theCompletionHandler(); return; } }]; } }
这个是iOS 11的API ,针对iOS 11之前的系统 ,需要另外处理。
利用 iOS 11之前的API解决WKWebView首次请求不携带Cookie的问题
通过让所有WKWebView共享同一个WKProcessPool实例,可以实现多个WKWebView之间共享Cookie(session Cookie and persistent Cookie)数据。不过WKWebView WKProcessPool实例在app杀进程重启后会被重置,导致WKProcessPool中的Cookie、session Cookie数据丢失,目前也无法实现 WKProcessPool实例本地化保存。可以采取cookie放入Header的方法来做。
WKWebView * webView = [WKWebView new];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://xxx.com/login"]];
[request addValue:@"skey=skeyValue" forHTTPHeaderField:@"Cookie"];
[webView loadRequest:request];
其中对于skey=skeyValue
这个cookie值的获取,也可以统一通过domain获取,获取的方法,可以参照下面的工具类:
HTTPDNSCookieManager.h
#ifndef HTTPDNSCookieManager_h
#define HTTPDNSCookieManager_h
// URL匹配Cookie规则
typedef BOOL (^HTTPDNSCookieFilter)(NSHTTPCookie *, NSURL *);
@interface HTTPDNSCookieManager : NSObject
+ (instancetype)sharedInstance;
/**
指定URL匹配Cookie策略
@param filter 匹配器
*/
- (void)setCookieFilter:(HTTPDNSCookieFilter)filter;
/**
处理HTTP Reponse携带的Cookie并存储
@param headerFields HTTP Header Fields
@param URL 根据匹配策略获取查找URL关联的Cookie
@return 返回添加到存储的Cookie
*/
- (NSArray<NSHTTPCookie *> *)handleHeaderFields:(NSDictionary *)headerFields forURL:(NSURL *)URL;
/**
匹配本地Cookie存储,获取对应URL的request cookie字符串
@param URL 根据匹配策略指定查找URL关联的Cookie
@return 返回对应URL的request Cookie字符串
*/
- (NSString *)getRequestCookieHeaderForURL:(NSURL *)URL;
/**
删除存储cookie
@param URL 根据匹配策略查找URL关联的cookie
@return 返回成功删除cookie数
*/
- (NSInteger)deleteCookieForURL:(NSURL *)URL;
@end
#endif /* HTTPDNSCookieManager_h */
HTTPDNSCookieManager.m
#import <Foundation/Foundation.h>
#import "HTTPDNSCookieManager.h"
@implementation HTTPDNSCookieManager
{
HTTPDNSCookieFilter cookieFilter;
}
- (instancetype)init {
if (self = [super init]) {
/**
此处设置的Cookie和URL匹配策略比较简单,检查URL.host是否包含Cookie的domain字段
通过调用setCookieFilter接口设定Cookie匹配策略,
比如可以设定Cookie的domain字段和URL.host的后缀匹配 | URL是否符合Cookie的path设定
细节匹配规则可参考RFC 2965 3.3节
*/
cookieFilter = ^BOOL(NSHTTPCookie *cookie, NSURL *URL) {
if ([URL.host containsString:cookie.domain]) {
return YES;
}
return NO;
};
}
return self;
}
+ (instancetype)sharedInstance {
static id singletonInstance = nil;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
if (!singletonInstance) {
singletonInstance = [[super allocWithZone:NULL] init];
}
});
return singletonInstance;
}
+ (id)allocWithZone:(struct _NSZone *)zone {
return [self sharedInstance];
}
- (id)copyWithZone:(struct _NSZone *)zone {
return self;
}
- (void)setCookieFilter:(HTTPDNSCookieFilter)filter {
if (filter != nil) {
cookieFilter = filter;
}
}
- (NSArray<NSHTTPCookie *> *)handleHeaderFields:(NSDictionary *)headerFields forURL:(NSURL *)URL {
NSArray *cookieArray = [NSHTTPCookie cookiesWithResponseHeaderFields:headerFields forURL:URL];
if (cookieArray != nil) {
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in cookieArray) {
if (cookieFilter(cookie, URL)) {
NSLog(@"Add a cookie: %@", cookie);
[cookieStorage setCookie:cookie];
}
}
}
return cookieArray;
}
- (NSString *)getRequestCookieHeaderForURL:(NSURL *)URL {
NSArray *cookieArray = [self searchAppropriateCookies:URL];
if (cookieArray != nil && cookieArray.count > 0) {
NSDictionary *cookieDic = [NSHTTPCookie requestHeaderFieldsWithCookies:cookieArray];
if ([cookieDic objectForKey:@"Cookie"]) {
return cookieDic[@"Cookie"];
}
}
return nil;
}
- (NSArray *)searchAppropriateCookies:(NSURL *)URL {
NSMutableArray *cookieArray = [NSMutableArray array];
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
if (cookieFilter(cookie, URL)) {
NSLog(@"Search an appropriate cookie: %@", cookie);
[cookieArray addObject:cookie];
}
}
return cookieArray;
}
- (NSInteger)deleteCookieForURL:(NSURL *)URL {
int delCount = 0;
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
for (NSHTTPCookie *cookie in [cookieStorage cookies]) {
if (cookieFilter(cookie, URL)) {
NSLog(@"Delete a cookie: %@", cookie);
[cookieStorage deleteCookie:cookie];
delCount++;
}
}
return delCount;
}
@end
发送请求使用方法示例:
WKWebView * webView = [WKWebView new];
NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://xxx.com/login"]];
NSString *value = [[HTTPDNSCookieManager sharedInstance] getRequestCookieHeaderForURL:url];
[request setValue:value forHTTPHeaderField:@"Cookie"];
[webView loadRequest:request];
接收处理请求:
NSURLSessionTask *task = [session dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
if (!error) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
// 解析 HTTP Response Header,存储cookie
[[HTTPDNSCookieManager sharedInstance] handleHeaderFields:[httpResponse allHeaderFields] forURL:url];
}
}];
[task resume];
通过document.cookie
设置Cookie解决后续页面(同域)Ajax、iframe请求的Cookie问题。
WKUserContentController* userContentController = [WKUserContentController new];
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie = 'skey=skeyValue';" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[userContentController addUserScript:cookieScript];
Cookie包含动态IP导致登录失效问题
关于Cookie失效的问题,假如客户端登录session存在Cookie,此时这个域名配置了多个IP,使用域名访问会读对应域名的Cookie,使用IP访问则去读对应IP的Cookie,假如前后两次使用同一个域名配置的不同IP访问,会导致Cookie的登录session失效。
如果App里面的WebView页面需要用到系统Cookie存的登录session,之前App所有本地网络请求使用域名访问,是可以共用Cookie的登录session的,但现在本地网络请求使用HTTPDNS后改用IP访问,导致还使用域名访问的WebView读不到系统Cookie存的登录session了(系统Cookie对应IP了)。IP直连后,服务端返回Cookie包含动态IP导致登录失效。
使用IP访问后,服务端返回的cookie也是IP。导致可能使用对应的域名访问,无法使用本地Cookie,或者使用隶属于同一个域名的不同IP去访问,Cookie也对不上,导致登录失效。
思路是这样的:
应该得干预Cookie的存储,基于域名。
根源上,API域名返回单IP。
第二种思路将失去DNS调度特性,故不考虑。第一种思路更为可行。
基于iOS11 API WKHTTPCookieStore来解决WKWebView的Cookie管理问题
当每次服务端返回Cookie后,在存储前都进行下改造,使用域名替换下IP。之后虽然每次网络请求都是使用IP访问,但是host我们都手动改为了域名,这样本地存储的Cookie也就能对得上了。
代码演示:
在网络请求成功后,或者加载网页成功后,主动将本地的domain字段为IP的Cookie替换IP为host域名地址。
- (void)updateWKHTTPCookieStoreDomainFromIP:(NSString *)IP toHost:(NSString *)host {
WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
[cookieStroe getAllCookies:^(NSArray<NSHTTPCookie *> * _Nonnull cookies) {
[[cookies copy] enumerateObjectsUsingBlock:^(NSHTTPCookie * _Nonnull cookie, NSUInteger idx, BOOL * _Nonnull stop) {
if ([cookie.domain isEqualToString:IP]) {
NSMutableDictionary<NSHTTPCookiePropertyKey, id> *dict = [NSMutableDictionary dictionaryWithDictionary:cookie.properties];
dict[NSHTTPCookieDomain] = host;
NSHTTPCookie *newCookie = [NSHTTPCookie cookieWithProperties:[dict copy]];
[cookieStroe setCookie:newCookie completionHandler:^{
[self logCookies];
[cookieStroe deleteCookie:cookie
completionHandler:^{
[self logCookies];
}];
}];
}
}];
}];
}
iOS 11中也提供了对应的API供我们来处理替换Cookie的时机,那就是下面的API:
@protocol WKHTTPCookieStoreObserver <NSObject>
@optional
- (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore;
@end
//WKHTTPCookieStore
/*! @abstract Adds a WKHTTPCookieStoreObserver object with the cookie store.
@param observer The observer object to add.
@discussion The observer is not retained by the receiver. It is your responsibility
to unregister the observer before it becomes invalid.
*/
- (void)addObserver:(id<WKHTTPCookieStoreObserver>)observer;
/*! @abstract Removes a WKHTTPCookieStoreObserver object from the cookie store.
@param observer The observer to remove.
*/
- (void)removeObserver:(id<WKHTTPCookieStoreObserver>)observer;
用法如下:
@interface WebViewController ()<WKHTTPCookieStoreObserver>
- (void)viewDidLoad {
[super viewDidLoad];
[NSURLProtocol registerClass:[WebViewURLProtocol class]];
NSHTTPCookieStorage *cookieStorage = [NSHTTPCookieStorage sharedHTTPCookieStorage];
[cookieStorage setCookieAcceptPolicy:NSHTTPCookieAcceptPolicyAlways];
WKHTTPCookieStore *cookieStroe = self.webView.configuration.websiteDataStore.httpCookieStore;
[cookieStroe addObserver:self];
[self.view addSubview:self.webView];
//... ...
}
#pragma mark -
#pragma mark - WKHTTPCookieStoreObserver Delegate Method
- (void)cookiesDidChangeInCookieStore:(WKHTTPCookieStore *)cookieStore {
[self updateWKHTTPCookieStoreDomainFromIP:CYLIP toHost:CYLHOST];
}
-updateWKHTTPCookieStoreDomainFromIP
方法的实现,在上文已经给出。
这个方案需要客户端维护一个IP —> HOST的映射关系,需要能从IP反向查找到HOST,这个维护成本还是挺高的。下面介绍下,更通用的方法,也是iOS11之前的处理方法:
iOS 11之前的处理方法:NSURLProtocal拦截后,手动管理Cookie的存储:
步骤:做IP替换时将原始URL保存到Header中。
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReq = [request mutableCopy];
NSString *originalUrl = mutableReq.URL.absoluteString;
NSURL *url = [NSURL URLWithString:originalUrl];
// 异步接口获取IP地址
NSString *ip = [[HttpDnsService sharedInstance] getIpByHostAsync:url.host];
if (ip) {
NSRange hostFirstRange = [originalUrl rangeOfString:url.host];
if (NSNotFound != hostFirstRange.location) {
NSString *newUrl = [originalUrl stringByReplacingCharactersInRange:hostFirstRange withString:ip];
mutableReq.URL = [NSURL URLWithString:newUrl];
[mutableReq setValue:url.host forHTTPHeaderField:@"host"];
// 添加originalUrl保存原始URL
[mutableReq addValue:originalUrl forHTTPHeaderField:@"originalUrl"];
}
}
NSURLRequest *postRequestIncludeBody = [mutableReq cyl_getPostRequestIncludeBody];
return postRequestIncludeBody;
}
然后获取到数据后,手动管理Cookie :
- (void)handleCookiesFromResponse:(NSURLResponse *)response {
NSString *originalURLString = [self.request valueForHTTPHeaderField:@"originalUrl"];
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSDictionary<NSString *, NSString *> *allHeaderFields = httpResponse.allHeaderFields;
if (originalURLString && originalURLString.length > 0) {
NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:allHeaderFields forURL: [[NSURL alloc] initWithString:originalURLString]];
if (cookies && cookies.count > 0) {
NSURL *originalURL = [NSURL URLWithString:originalURLString];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookies forURL:originalURL mainDocumentURL:nil];
}
}
}
}
- (void)URLSession:(NSURLSession *)session task:(NSURLSessionTask *)task willPerformHTTPRedirection:(NSHTTPURLResponse *)response newRequest:(NSURLRequest *)newRequest completionHandler:(void (^)(NSURLRequest *))completionHandler {
NSString *location = response.allHeaderFields[@"Location"];
NSURL *url = [[NSURL alloc] initWithString:location];
NSMutableURLRequest *mRequest = [newRequest mutableCopy];
mRequest.URL = url;
if (location && location.length > 0) {
if ([[newRequest.HTTPMethod lowercaseString] isEqualToString:@"post"]) {
// POST重定向为GET
mRequest.HTTPMethod = @"GET";
mRequest.HTTPBody = nil;
}
[mRequest setValue:nil forHTTPHeaderField:@"host"];
// 在这里为 request 添加 cookie 信息。
[self handleCookiesFromResponse:response];
[XXXURLProtocol removePropertyForKey:XXXURLProtocolHandledKey inRequest:mRequest];
completionHandler(mRequest);
} else{
completionHandler(mRequest);
}
}
发送请求前,向请求中添加Cookie信息:
+ (void)handleCookieWithRequest:(NSMutableURLRequest *)request {
NSString* originalURLString = [request valueForHTTPHeaderField:@"originalUrl"];
if (!originalURLString || originalURLString.length == 0) {
return;
}
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies];
if (cookies && cookies.count >0) {
NSDictionary *cookieHeaders = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
NSString *cookieString = [cookieHeaders objectForKey:@"Cookie"];
[request addValue:cookieString forHTTPHeaderField:@"Cookie"];
}
}
+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
NSMutableURLRequest *mutableReq = [request mutableCopy];
//...
[self handleCookieWithRequest:mutableReq];
return [mutableReq copy];
}
相关的文章:
302重定向问题
上面提到的Cookie方案无法解决302请求的Cookie问题,比如,第一个请求是 http://www.a.com,我们通过在request header里带上Cookie解决该请求的Cookie问题,接着页面302跳转到 http://www.b.com,这个时候http://www.b.com这个请求就可能因为没有携带Cookie而无法访问。当然,由于每一次页面跳转前都会调用回调函数:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
可以在该回调函数里拦截302请求,copy request,在request header中带上Cookie并重新loadRequest。不过这种方法依然解决不了页面iframe跨域请求的Cookie问题,毕竟-[WKWebView loadRequest:]
只适合加载mainFrame请求。
相关参考
相关的库:
可以参考的Demo: