Cookie
关于 cookie
cookie 是服务端为了识别终端身份,保存在终端本地的用户凭证信息。cookie 中的字段与意义由服务端进行定义。例如,当用户在进行了登录操作后,服务端会将cookie 信息返回给终端,终端会将这些信息进行保存,在下一次再次访问服务端时,终端会将保存的cookie 信息一并发送到服务端,服务端根据cookie 信息是否有效来判断此用户是否可以进行一些行为。在 iOS 中如何管理cookie
iOS 中Cookie管理主要有两个类 NSHTTPCookie和NSHTTPCookieStorage,当你访问一个网站时,NSURLRequest都会帮你主动记录下来你访问的站点设置的Cookie,因为NSHTTPCookieStorage的默认策略是:NSHTTPCookieAcceptPolicyAlways,所以如果 Cookie 存在的话,会把这些信息放在 NSHTTPCookieStorage 容器中共享,当你下次再访问这个站点时,NSURLRequest会拿着上次保存下来了的Cookie继续去请求。
NSHTTPCookieStorage介绍
NSHTTPCookieStorage,管理着所有HTTP请求的Cookie信息.
继承自:NSObject
遵守协议:NSObject
导入声明:@import Foundation;
适用范围:iOS 2.0 及以后
官方解释:NSHTTPCookieStorage 是一个用来管理 cookie 存储的单例。一个 NSHTTPCookie 单例代表一个 cookie。通常来讲,cookie 可以在应用间共享,并且在进程之间保持同步。 对于单进程,Session cookies (这里的 cookie 对象的 isSessionOnly 方法返回 YES
)是局部的并且不能被共享。
在 iOS 中,应用间是不能共享 cookie 的。
在 OS X 10.9 及以后和 iOS 7 及以后,NSHTTPCookieStorage 是线程安全的。
// 获取单例对象
+ (NSHTTPCookieStorage *)sharedHTTPCookieStorage;
// 所有Cookie数据数组 其中存放NSHTTPCookie对象
@property (nullable , readonly, copy) NSArray<NSHTTPCookie *> *cookies;
// 手动设置一条Cookie数据
- (void)setCookie:(NSHTTPCookie *)cookie;
// 删除某条Cookie信息
- (void)deleteCookie:(NSHTTPCookie *)cookie;
// 删除某个时间后的所有Cookie信息 iOS8后可用
- (void)removeCookiesSinceDate:(NSDate *)date NS_AVAILABLE(10_10, 8_0);
// 获取某个特定URL的所有Cookie数据
- (nullable NSArray<NSHTTPCookie *> *)cookiesForURL:(NSURL *)URL;
// 为某个特定的URL设置Cookie
- (void)setCookies:(NSArray<NSHTTPCookie *> *)cookies forURL:(nullable NSURL *)URL mainDocumentURL:(nullable NSURL *)mainDocumentURL;
// Cookie数据的接收协议
枚举如下:
typedef NS_ENUM(NSUInteger, NSHTTPCookieAcceptPolicy) {
NSHTTPCookieAcceptPolicyAlways,//接收所有Cookie信息
NSHTTPCookieAcceptPolicyNever,//不接收所有Cookie信息
NSHTTPCookieAcceptPolicyOnlyFromMainDocumentDomain//只接收主文档域的Cookie信息
};
@property NSHTTPCookieAcceptPolicy cookieAcceptPolicy;
系统下面的两个通知与Cookie管理有关:
据说,在Mac OS是cookie可以共享的(Session cookies 不能共享),在Mac OS app中更改cookie的接收策略会影响到其他正在运行的在使用cookie storage的app.这时NSHTTPCookieStorage会发出两个通知:
// Cookie数据的接收协议改变时发送的通知
FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerAcceptPolicyChangedNotification;
// 管理的Cookie数据发生变化时发送的通知
FOUNDATION_EXPORT NSString * const NSHTTPCookieManagerCookiesChangedNotification;
NSHTTPCookie介绍
NSHTTPCookie是具体的HTTP请求Cookie数据对象.
// 下面两个方法用于对象的创建和初始化 都是通过字典进行键值设置
- (nullable instancetype)initWithProperties:(NSDictionary<NSString *, id> *)properties;
+ (nullable NSHTTPCookie *)cookieWithProperties:(NSDictionary<NSString *, id> *)properties;
// 返回Cookie数据中可用于添加HTTP头字段的字典
+ (NSDictionary<NSString *, NSString *> *)requestHeaderFieldsWithCookies:(NSArray<NSHTTPCookie *> *)cookies;
// 从指定的响应头和URL地址中解析出Cookie数据
+ (NSArray<NSHTTPCookie *> *)cookiesWithResponseHeaderFields:(NSDictionary<NSString *, NSString *> *)headerFields forURL:(NSURL *)URL;
// Cookie数据中的属性字典
@property (nullable, readonly, copy) NSDictionary<NSString *, id> *properties;
// 请求响应的版本
@property (readonly) NSUInteger version;
// 请求相应的名称
@property (readonly, copy) NSString *name;
// 请求相应的值
@property (readonly, copy) NSString *value;
// 过期时间
@property (nullable, readonly, copy) NSDate *expiresDate;
// 请求的域名
@property (readonly, copy) NSString *domain;
//请求的路径
@property (readonly, copy) NSString *path;
// 是否是安全传输
@property (readonly, getter=isSecure) BOOL secure;
// 是否只发送HTTP的服务
@property (readonly, getter=isHTTPOnly) BOOL HTTPOnly;
// 响应的文档
@property (nullable, readonly, copy) NSString *comment;
// 相应的文档URL
@property (nullable, readonly, copy) NSURL *commentURL;
// 服务端口列表
@property (nullable, readonly, copy) NSArray<NSNumber *> *portList;
HTTP cookie的属性键
属性 | 解读 |
---|---|
NSHTTPCookieName | Cookie的名字 |
NSHTTPCookieValue | Cookie的值 |
NSHTTPCookieOriginURL | 和域名一样,NSHTTPCookieDomain或NSHTTPCookieOriginURL必须指定一个值 |
NSHTTPCookieVersion | 接收器的版本 |
NSHTTPCookieDomain | 域名 |
NSHTTPCookiePath | Cookie 存放路径 |
NSHTTPCookieSecure | Cookie是否只应通过安全通道发送,设置Cookie的secure属性为true。只会在HTTPS和SSL等安全协议中传输此类Cookie。默认为false |
NSHTTPCookieComment | 包含Cookie的评论,只有有效的版本1的cookies或更高版本。 这头字段是可选的 |
NSHTTPCookieCommentURL | 接收器的评论URL |
NSHTTPCookieDiscard | Cookie是否应在会议结束时丢弃NSString,字符串值必须是“true”或“假”。 这个字段是可选的。 默认为“假”,除非这是Cookie是第1版或以上,NSHTTPCookieMaximumAge未指定,在这种情况下,它被假定为“TRUE” |
NSHTTPCookieMaximumAge | NSString对象,包含一个整数,在Cookie内保持最多几秒 。仅适用于第1版和更高版本的有效。 默认为“0”。 此字段是可选的 |
NSHTTPCookiePort | 接收机的端口 |
清除Cookie步骤
// 清除所有的cookie 方法:
NSURL *url = [NSURL URLWithString:@"http://www.baidu.com"];
if (url) {
NSArray *cookies = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:url];
for (int i = 0; i < [cookies count]; i++) {
NSHTTPCookie *cookie = (NSHTTPCookie *)[cookies objectAtIndex:i];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie];
}
}
// 清除某一个特定的cookie方法:
NSArray * cookArray = [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookiesForURL:[NSURL URLWithString:self.loadURL]];
NSString * successCode = @"";
for (NSHTTPCookie*cookie in cookArray) {
if ([cookie.name isEqualToString:@"cookiename"]) {
[[NSHTTPCookieStorage sharedHTTPCookieStorage] deleteCookie:cookie];
}
}
// 清除某一个url缓存的方法:
[[NSURLCache sharedURLCache] removeCachedResponseForRequest:[NSURLRequest requestWithURL:url]];
// 清除所有缓存方法:
[[NSURLCache sharedURLCache] removeAllCachedResponses];
- 在iOS中如何使用cookie
(1) 可以在 App 启动时或者在webview的viewDidLoad中判断用户是否登录,本地是否记录了登录后的cookie,并且cookie是否失效,如果是登录状态,本地无cookie,可以手动自定义cookie,并存入NSHTTPCookieStorage中,如果本地有cookie记录,判断cookie是否失效,设置有效的cookie数据。
(2) 在UIWebView或者WKWebView的加载前的代理方法中判断是否已经登录,如果没有登录,则直接跳转到app的登录页面。在登录成功之后发送通知,在webview中进行页面刷新。
(3) 在登录完成时存 cookie 。
(4) 在退出时清除 cookie 。
// 可以在登录完成时,或者已登录但本地没有记录cookie时设置
// 如果服务器没有定义cookie,则可以本地自定义
// 自定义cookie:手动设置的Cookie不会自动持久化到沙盒,需要利用NSUserDefaults进行存储
NSMutableDictionary*cookieProperties = [NSMutableDictionary dictionary];
[cookieProperties setObject:@"token" forKey:NSHTTPCookieName]; //给cookie取名
NSString * token =[[NSUserDefaults standardUserDefaults] objectForKey:@"token"];
[cookieProperties setObject:token forKey:NSHTTPCookieValue];
// 存放目录
[cookieProperties setObject:@"/" forKey:NSHTTPCookiePath];
NSString * domain = [NSURL URLWithString:@"http://www.baidu.com/"].host;
[cookieProperties setObject:domain forKey:NSHTTPCookieDomain]; //设置域名
// 将可变字典转化为cookie
NSHTTPCookie * httpCookie = [NSHTTPCookie cookieWithProperties:cookieProperties];
// 将cookie存入仓库
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:httpCookie];
NSData *cookiesData = [NSKeyedArchiver archivedDataWithRootObject: httpCookie];
[[NSUserDefaults standardUserDefaults] setObject:cookiesData forKey:@"cookie"];
[[NSUserDefaults standardUserDefaults] synchronize];
//生成的cookie:version: 0 name:token value : token值 expiresDate:cookie有效截止日期 created :创建日期 sessionOnly: 返回接收器是否应该在会话结束时被丢弃 domain :域名 partition: 端口 path:路径 isSecure:返回他的cookie不只应通过安全通道发送
<NSHTTPCookie version:0 name:"token" value:"b2a8f8523ab41f8b4b9b2a79ff47c3f1" expiresDate:(null) created:2016-11-09 03:02:41 +0000 sessionOnly:TRUE domain:"api.test.com" partition:"none" path:"/" isSecure:FALSE>
-
cookie的持久化存储
对于获取服务器直接返回的cookie,或者手动定义的cookie
如果设置了Cookie失效时间expiresDate ,sessionOnly:FALSE,会被持久化到文件中,kill掉后系统自动保存,下次启动app会自动加载沙盒的 /Library/Cookies中的Cookies
如果没有设置Cookie失效时间expiresDate:(null),sessionOnly:TURE,kill掉后系统不会自动保存Cookie,如果想持久化Cookie 模仿浏览器存住Cookie,可以使用NSUserDefaults进行本地存储
UIWebView的cookie机制
UIWebView会将NSHttpRequest的所有请求产生的cookie自动保存到NSHTTPCookieStorage容器中,并且在同一个app内多个UIWebView之间共享,不需要我们做任何操作,在后续访问中会将 cookie 自动带到 request 请求当中。
UIWebView 在浏览网页后会将网页中的 cookie 自动存入 NSHTTPCookieStorage 标准容器中。在后续访问中会将 cookie 自动带到 request 请求当中。比如,NSHTTPCookieStorage 中存储了一个Cookie,name=a;value=b;domain=y.qq.com;expires=Sat,02 May 2017 23:20:25 GMT; 则通 过 UIWebView 发起请求 http://y.qq.com,则请求头会自动带上cookie,而通过 WKWebView 发起请求,请求头不会自动带上该cookie。
WKWebView的cookie机制
业界普遍认为 WKWebView 拥有自己的私有存储,不会将 Cookie 存入到标准的 Cookie 容器 NSHTTPCookieStorage 中。
实践发现 WKWebView 实例其实也会将 Cookie 存储于 NSHTTPCookieStorage 中,但存储时机有延迟,在iOS 8上,当页面跳转的时候,当前页面的 Cookie 会写入 NSHTTPCookieStorage 中,而在 iOS 10 上,JS 执行 document.cookie 或服务器 set-cookie 注入的 Cookie 会很快同步到 NSHTTPCookieStorage 中,FireFox 工程师曾建议通过 reset WKProcessPool 来触发 Cookie 同步到 NSHTTPCookieStorage 中,实践发现不起作用,并可能会引发当前页面 session cookie 丢失等问题。
WKWebView Cookie 问题在于 WKWebView 发起的请求不会自动带上存储于 NSHTTPCookieStorage 容器中的 Cookie。
比如,NSHTTPCookieStorage 中存储了一个 Cookie:
name=Nicholas;value=test;domain=y.qq.com;expires=Sat, 02 May 2019 23:38:25 GMT;
通过 UIWebView 发起请求http://y.qq.com/, 则请求头会自动带上 cookie: Nicholas=test;
而通过 WKWebView发起请求http://y.qq.com/, 请求头不会自动带上 cookie: Nicholas=test。
WKWebView中注入Cookie
如果你在Native层面做了登陆操作,获取了Cookie信息,也使用 NSHTTPCookieStorage 存到了本地,但是使用 WKWebView 打开对应网页时,网页依然处于未登陆状态。如果是登陆也在 WebView 里做的,就不会有这个问题。
iOS11
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之前
通过让所有 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"]];
NSString *cookieStr = [NSString stringWithFormat:@"%@=%@",cookie.name,cookie.value];
[request addValue:cookieStr forHTTPHeaderField:@"Cookie"];
[webView loadRequest:request];
cookieStr的获取可以从本地存储的自定义cookie获取,也可以通过以下方法获取:
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];
文中有错误的地方,还望大家包涵并指正,谢谢.
参考链接
iOS 关于token、cookie的那些事
详解iOS App开发中Cookie的管理方法
iOS WKWebView 与 UIWebView Cookie机制的同步
iOS WebView 中的 Cookie 处理业务场景“IP直连”方案说明