最近公司項目安全自查, 就對現(xiàn)有項目詳細的檢查了一下, 其中就包括網(wǎng)絡(luò)請求緩存安全這一塊, 所以就查資料詳細的學(xué)習(xí)了一下這一方面的知識, 這里做個記錄也和想要了解這一塊知識的同學(xué)做個分享
初識緩存
緩存的目的的以空間換時間
緩存有不同的分類方法:
按照功能分: 1. 優(yōu)化型緩存 2. 存儲型緩存
按照形式劃分: 1. 內(nèi)存型緩存 2. 磁盤型緩存(iOS 5以后支持)
GET網(wǎng)絡(luò)請求緩存
POST請求不能被緩存叛赚,只有 GET 請求能被緩存饵逐。因為從數(shù)學(xué)的角度來講闻蛀,GET 的結(jié)果是 冪等 的厕妖,就好像字典里的 key 與 value 就是冪等的溉旋,而 POST 不 冪等 航唆。緩存的思路就是將查詢的參數(shù)組成的值作為 key 坠七,對應(yīng)結(jié)果作為value誊稚。從這個意義上說,一個文件的資源鏈接倒脓,也叫 GET 請求撑螺,下文也會這樣看待。
緩存只需要二步
第一個步驟:請使用 GET 請求崎弃。
第二個步驟:
如果你已經(jīng)使用 了 GET 請求甘晤,iOS 系統(tǒng) SDK 已經(jīng)幫你做好了緩存。你需要的僅僅是設(shè)置下內(nèi)存緩存大小饲做、磁盤緩存大小线婚、以及緩存路徑。甚至這兩行代碼不設(shè)置也是可以的盆均,會有一個默認值塞弊。代碼如下:
NSURLCache *urlCache = [[NSURLCache alloc] initWithMemoryCapacity:4 * 1024 * 1024 diskCapacity:20 * 1024 * 1024 diskPath:nil];
[NSURLCache setSharedURLCache:urlCache];
如何控制緩存的有效性
借助 ETag 或 Last-Modified 判斷緩存是否有效。
Last-Modified
Last-Modified 顧名思義,是資源最后修改的時間戳游沿,往往與緩存時間進行對比來判斷緩存是否過期饰抒。
在瀏覽器第一次請求某一個URL時,服務(wù)器端的返回狀態(tài)會是200诀黍,內(nèi)容是你請求的資源袋坑,同時有一個Last-Modified的屬性標記此文件在服務(wù)期端最后被修改的時間,格式類似這樣:
Last-Modified: Fri, 12 May 2006 18:53:33 GMT
客戶端第二次請求此URL時眯勾,根據(jù) HTTP 協(xié)議的規(guī)定咒彤,瀏覽器會向服務(wù)器傳送 If-Modified-Since 報頭,詢問該時間之后文件是否有被修改過:
If-Modified-Since: Fri, 12 May 2006 18:53:33 GMT
如果服務(wù)器端的資源沒有變化咒精,則自動返回 HTTP 304 (Not Changed.)狀態(tài)碼镶柱,內(nèi)容為空,這樣就節(jié)省了傳輸數(shù)據(jù)量模叙。當服務(wù)器端代碼發(fā)生改變或者重啟服務(wù)器時歇拆,則重新發(fā)出資源,返回和第一次請求時類似范咨。從而保證不向客戶端重復(fù)發(fā)出資源故觅,也保證當服務(wù)器有變化時,客戶端能夠得到最新的資源渠啊。
ETag
HTTP 協(xié)議規(guī)格說明定義ETag為“被請求變量的實體值” (參見 —— 章節(jié) 14.19)输吏。 另一種說法是,ETag是一個可以與Web資源關(guān)聯(lián)的記號(token)替蛉。它是一個 hash 值贯溅,用作 Request 緩存請求頭,每一個資源文件都對應(yīng)一個唯一的 ETag 值躲查,
服務(wù)器單獨負責(zé)判斷記號是什么及其含義它浅,并在HTTP響應(yīng)頭中將其傳送到客戶端,以下是服務(wù)器端返回的格式:
ETag: "50b1c1d4f775c61:df3"
客戶端的查詢更新格式是這樣的:
If-None-Match: W/"50b1c1d4f775c61:df3"
如果ETag沒改變镣煮,則返回狀態(tài)304然后不返回姐霍,這也和Last-Modified一樣。
ETag 是的功能與 Last-Modified 類似:服務(wù)端不會每次都會返回文件資源典唇∧髡郏客戶端每次向服務(wù)端發(fā)送上次服務(wù)器返回的 ETag 值,服務(wù)器會根據(jù)客戶端與服務(wù)端的 ETag 值是否相等介衔,來決定是否返回 data恨胚,同時總是返回對應(yīng)的 HTTP 狀態(tài)碼∫鼓担客戶端通過 HTTP 狀態(tài)碼來決定是否使用緩存与纽。比如:服務(wù)端與客戶端的 ETag 值相等,則 HTTP 狀態(tài)碼為 304塘装,不返回 data急迂。服務(wù)端文件一旦修改,服務(wù)端與客戶端的 ETag 值不等蹦肴,并且狀態(tài)值會變?yōu)?00僚碎,同時返回 data。
因為修改資源文件后該值會立即變更阴幌。這也決定了 ETag 在斷點下載時非常有用勺阐。
比如 AFNetworking 在進行斷點下載時,就是借助它來檢驗數(shù)據(jù)的
示例代碼:
/*!
@brief 如果本地緩存資源為最新矛双,則使用使用本地緩存渊抽。如果服務(wù)器已經(jīng)更新或本地?zé)o緩存則從服務(wù)器請求資源。
@details
步驟:
1. 請求是可變的议忽,緩存策略要每次都從服務(wù)器加載
2. 每次得到響應(yīng)后懒闷,需要記錄住 etag
3. 下次發(fā)送請求的同時,將etag一起發(fā)送給服務(wù)器(由服務(wù)器比較內(nèi)容是否發(fā)生變化)
@return 圖片資源
*/
- (void)getData:(GetDataCompletion)completion {
NSURL *url = [NSURL URLWithString:kETagImageURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
// 發(fā)送 etag
if (self.etag.length > 0) {
[request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
// NSLog(@"%@ %tu", response, data.length);dd
// 類型轉(zhuǎn)換(如果將父類設(shè)置給子類栈幸,需要強制轉(zhuǎn)換)
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"statusCode == %@", @(httpResponse.statusCode));
// 判斷響應(yīng)的狀態(tài)碼是否是 304 Not Modified (更多狀態(tài)碼含義解釋: https://github.com/ChenYilong/iOSDevelopmentTips)
if (httpResponse.statusCode == 304) {
NSLog(@"加載本地緩存圖片");
// 如果是愤估,使用本地緩存
// 根據(jù)請求獲取到`被緩存的響應(yīng)`!
NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
// 拿到緩存的數(shù)據(jù)
data = cacheResponse.data;
}
// 獲取并且紀錄 etag速址,區(qū)分大小寫
self.etag = httpResponse.allHeaderFields[@"Etag"];
NSLog(@"etag值%@", self.etag);
!completion ?: completion(data);
}];
}
相應(yīng)的 NSURLSession 搭配 ETag 代碼
/*!
@brief 如果本地緩存資源為最新玩焰,則使用使用本地緩存。如果服務(wù)器已經(jīng)更新或本地?zé)o緩存則從服務(wù)器請求資源芍锚。
@details
步驟:
1. 請求是可變的昔园,緩存策略要每次都從服務(wù)器加載
2. 每次得到響應(yīng)后,需要記錄住 etag
3. 下次發(fā)送請求的同時并炮,將etag一起發(fā)送給服務(wù)器(由服務(wù)器比較內(nèi)容是否發(fā)生變化)
@return 圖片資源
*/
- (void)getData:(GetDataCompletion)completion {
NSURL *url = [NSURL URLWithString:kETagImageURL];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url cachePolicy:NSURLRequestReloadIgnoringCacheData timeoutInterval:15.0];
// 發(fā)送 etag
if (self.etag.length > 0) {
[request setValue:self.etag forHTTPHeaderField:@"If-None-Match"];
}
[[[NSURLSession sharedSession] dataTaskWithRequest:request completionHandler:^(NSData *data, NSURLResponse *response, NSError *error) {
// NSLog(@"%@ %tu", response, data.length);
// 類型轉(zhuǎn)換(如果將父類設(shè)置給子類蒿赢,需要強制轉(zhuǎn)換)
NSHTTPURLResponse *httpResponse = (NSHTTPURLResponse *)response;
NSLog(@"statusCode == %@", @(httpResponse.statusCode));
// 判斷響應(yīng)的狀態(tài)碼是否是 304 Not Modified (更多狀態(tài)碼含義解釋: https://github.com/ChenYilong/iOSDevelopmentTips)
if (httpResponse.statusCode == 304) {
NSLog(@"加載本地緩存圖片");
// 如果是,使用本地緩存
// 根據(jù)請求獲取到`被緩存的響應(yīng)`渣触!
NSCachedURLResponse *cacheResponse = [[NSURLCache sharedURLCache] cachedResponseForRequest:request];
// 拿到緩存的數(shù)據(jù)
data = cacheResponse.data;
}
// 獲取并且紀錄 etag羡棵,區(qū)分大小寫
self.etag = httpResponse.allHeaderFields[@"Etag"];
NSLog(@"%@", self.etag);
dispatch_async(dispatch_get_main_queue(), ^{
!completion ?: completion(data);
});
}] resume];
}
iOSNSURLRequest提供了7種緩存策略
NSURLRequestUseProtocolCachePolicy // 默認的緩存策略(取決于協(xié)議)
NSURLRequestReloadIgnoringLocalCacheData // 忽略緩存,重新請求
NSURLRequestReloadIgnoringLocalAndRemoteCacheData // 未實現(xiàn)
NSURLRequestReloadIgnoringCacheData = NSURLRequestReloadIgnoringLocalCacheData // 忽略緩存嗅钻,
重新請求
NSURLRequestReturnCacheDataElseLoad// 有緩存就用緩存皂冰,沒有緩存就重新請求
NSURLRequestReturnCacheDataDontLoad// 有緩存就用緩存,沒有緩存就不發(fā)請求养篓,當做請求出錯處理(用于離線模式)
NSURLRequestReloadRevalidatingCacheData // 未實現(xiàn)
使用緩存策略的簡單示例
- (void)touchesBegan:(NSSet *)touches withEvent:(UIEvent *)event
{
// 1.創(chuàng)建請求
NSURL *url = [NSURL URLWithString:@"http://127.0.0.1:8080/YYServer/video"];
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
// 2.設(shè)置緩存策略(有緩存就用緩存秃流,沒有緩存就重新請求)
request.cachePolicy = NSURLRequestReturnCacheDataElseLoad;
// 3.發(fā)送請求
[NSURLConnection sendAsynchronousRequest:request queue:[NSOperationQueue mainQueue] completionHandler:^(NSURLResponse *response, NSData *data, NSError *connectionError) {
if (data) {
NSDictionary *dict = [NSJSONSerialization JSONObjectWithData:data options:NSJSONReadingMutableLeaves error:nil];
NSLog(@"%@", dict);
}
}];
}