本文主要介紹防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: