全部產品
Search
文件中心

HTTPDNS:iOS端WebView " IP直連 " 如何處理 Cookie

更新時間:Jul 13, 2024

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