iOS 防 DNS 污染方案调研 --- Cookie 业务场景

本文涉及的产品
全局流量管理 GTM,标准版 1个月
云解析 DNS,旗舰版 1个月
公共DNS(含HTTPDNS解析),每月1000万次HTTP解析
简介: 本文将讨论下类似这样的问题: WKWebView 对于 Cookie 的管理一直是它的短板,那么 iOS11 是否有改进,如果有,如何利用这样的改进? 采用 IP 直连方案后,服务端返回的 Cookie 里的 Domain 字段也会使用 IP 。

概述

本文将讨论下类似这样的问题:

  • 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:@"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];
                           }];
               }];
           }
       }];
   }];
}

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);
}

相关的文章:

相关文章
|
3月前
|
并行计算 数据挖掘 大数据
[go 面试] 并行与并发的区别及应用场景解析
[go 面试] 并行与并发的区别及应用场景解析
|
24天前
|
JavaScript API 开发工具
<大厂实战场景> ~ Flutter&鸿蒙next 解析后端返回的 HTML 数据详解
本文介绍了如何在 Flutter 中解析后端返回的 HTML 数据。首先解释了 HTML 解析的概念,然后详细介绍了使用 `http` 和 `html` 库的步骤,包括添加依赖、获取 HTML 数据、解析 HTML 内容和在 Flutter UI 中显示解析结果。通过具体的代码示例,展示了如何从 URL 获取 HTML 并提取特定信息,如链接列表。希望本文能帮助你在 Flutter 应用中更好地处理 HTML 数据。
103 1
|
3月前
|
机器学习/深度学习 存储 人工智能
提升深度学习性能的利器—全面解析PAI-TorchAcc的优化技术与应用场景
在当今深度学习的快速发展中,模型训练和推理的效率变得尤为重要。为了应对计算需求不断增长的挑战,AI加速引擎应运而生。其中,PAI-TorchAcc作为一个新兴的加速引擎,旨在提升PyTorch框架下的计算性能。本文将详细介绍PAI-TorchAcc的基本概念、主要特性,并通过代码实例展示其性能优势。
18160 166
|
4月前
|
设计模式 Java
Java中的设计模式及其应用场景解析
Java中的设计模式及其应用场景解析
|
1月前
|
存储
让星星⭐月亮告诉你,HashMap的put方法源码解析及其中两种会触发扩容的场景(足够详尽,有问题欢迎指正~)
`HashMap`的`put`方法通过调用`putVal`实现,主要涉及两个场景下的扩容操作:1. 初始化时,链表数组的初始容量设为16,阈值设为12;2. 当存储的元素个数超过阈值时,链表数组的容量和阈值均翻倍。`putVal`方法处理键值对的插入,包括链表和红黑树的转换,确保高效的数据存取。
56 5
|
3月前
|
测试技术 Linux 虚拟化
iOS自动化测试方案(五):保姆级VMware虚拟机安装MacOS
详细的VMware虚拟机安装macOS Big Sur的保姆级教程,包括下载VMware和macOS镜像、图解安装步骤和遇到问题时的解决方案,旨在帮助读者顺利搭建macOS虚拟机环境。
155 3
iOS自动化测试方案(五):保姆级VMware虚拟机安装MacOS
|
3月前
|
测试技术 开发工具 iOS开发
iOS自动化测试方案(三):WDA+iOS自动化测试解决方案
这篇文章是iOS自动化测试方案的第三部分,介绍了在没有MacOS系统条件下,如何使用WDA(WebDriverAgent)结合Python客户端库facebook-wda和tidevice工具,在Windows系统上实现iOS应用的自动化测试,包括环境准备、问题解决和扩展应用的详细步骤。
297 1
iOS自动化测试方案(三):WDA+iOS自动化测试解决方案
|
3月前
|
运维 监控 数据可视化
Elasticsearch全观测技术解析问题之面对客户不同的场景化如何解决
Elasticsearch全观测技术解析问题之面对客户不同的场景化如何解决
|
3月前
|
存储 数据挖掘 大数据
深度解析Hologres计算资源配置:如何根据业务场景选择合适的计算类型?
【8月更文挑战第22天】Hologres是一款由阿里云提供的分布式分析型数据库,支持高效的大数据处理与分析。本文通过电商优化商品推荐策略的案例,介绍了Hologres中的计算组型与通用型配置。计算组型提供弹性扩展资源,适合大规模数据及高并发查询;通用型则适用于多数数据分析场景,具备良好计算性能。通过实例创建、数据加载、计算任务建立及结果查询的步骤展示,读者可理解两种配置的差异并根据业务需求灵活选择。
56 2
|
3月前
|
测试技术 数据安全/隐私保护 iOS开发
iOS自动化测试方案(四):保姆级搭建iOS自动化开发环境
iOS自动化测试方案的第四部分,涵盖了基础环境准备、iPhone虚拟机设置、MacOS虚拟机与iPhone真机的连接,以及扩展问题和代码示例,确保读者能够顺利完成环境搭建并进行iOS自动化测试。
308 0
iOS自动化测试方案(四):保姆级搭建iOS自动化开发环境

相关产品

  • 云解析DNS
  • 下一篇
    无影云桌面