iOS 防 DNS 汙染方案調研 --- Cookie 業務場景
概述
本文將討論下類似這樣的問題:
- WKWebView 對於 Cookie 的管理一直是它的短板,那麼 iOS11 是否有改進,如果有,如何利用這樣的改進?
- 采用 IP 直連方案後,服務端返回的 Cookie 裏的 Domain 字段也會使用 IP 。如果 IP 是動態的,就有可能導致一些問題:由於許多 H5 業務都依賴於 Cookie 作登錄態校驗,而 WKWebView 上請求不會自動攜帶 Cookie。
WKWebView 使用 NSURLProtocol 攔截請求無法獲取 Cookie 信息
iOS11推出了新的 API WKHTTPCookieStore
可以用來攔截 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");
}];
利用 iOS11 API WKHTTPCookieStore 解決 WKWebView 首次請求不攜帶 Cookie 的問題
問題說明:由於許多 H5 業務都依賴於 Cookie 作登錄態校驗,而 WKWebView 上請求不會自動攜帶 Cookie。比如,如果你在Native層麵做了登陸操作,獲取了Cookie信息,也使用 NSHTTPCookieStorage 存到了本地,但是使用 WKWebView 打開對應網頁時,網頁依然處於未登陸狀態。如果是登陸也在 WebView 裏做的,就不會有這個問題。
iOS11 的 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;
}
}];
}
}
這個是 iOS11 的API,針對iOS11之前的係統,需要另外處理。
利用 iOS11 之前的 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:@"https://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:@"https://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];
}];
}];
}
}];
}];
}
iOS11中也提供了對應的 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:MY_IP toHost:MY_HOST];
}
-updateWKHTTPCookieStoreDomainFromIP
方法的實現,在上文已經給出。
這個方案需要客戶端維護一個IP --> HOST的映射關係,需要能從 IP 反向查找到 HOST,這個維護成本還時挺高的。下麵介紹下,更通用的方法,也是iOS11 之前的處理方法:
iOS11 之前的處理方法: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 httpdns_getPostRequestIncludeBody];
return postRequestIncludeBody;
}
然後獲取到數據後,手動管理 Cookie:
#pragma NSURLSessionDataDelegate
- (void)URLSession:(NSURLSession *)session dataTask:(NSURLSessionDataTask *)dataTask didReceiveResponse:(NSURLResponse *)response completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler {
NSString* originalUrl = [dataTask.currentRequest valueForHTTPHeaderField:@"originalUrl"];
if (!originalUrl) {
originalUrl = response.URL.absoluteString;
}
if ([response isKindOfClass:[NSHTTPURLResponse class]]) {
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *) response;
NSDictionary<NSString *, NSString *> *allHeaderFields = httpResponse.allHeaderFields;
NSArray *cookies = [NSHTTPCookie cookiesWithResponseHeaderFields:[httpResponse allHeaderFields] forURL: [[NSURL alloc] initWithString:originalUrl]];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookies:cookies forURL:[response URL] mainDocumentURL:nil];
NSURLResponse *retResponse = [[NSHTTPURLResponse alloc] initWithURL:[NSURL URLWithString:originalUrl] statusCode:httpResponse.statusCode HTTPVersion:(__bridge NSString *)kCFHTTPVersion1_1 headerFields:httpResponse.allHeaderFields];
[self.client URLProtocol:self didReceiveResponse:retResponse cacheStoragePolicy:NSURLCacheStorageNotAllowed];
} else {
NSURLResponse *retResponse = [[NSURLResponse alloc] initWithURL:[NSURL URLWithString:originalUrl] MIMEType:response.MIMEType expectedContentLength:response.expectedContentLength textEncodingName:response.textEncodingName];
[self.client URLProtocol:self didReceiveResponse:retResponse cacheStoragePolicy:NSURLCacheStorageNotAllowed];
}
completionHandler(NSURLSessionResponseAllow);
}
相關的文章:
最後更新:2017-09-13 14:32:58