簡(jiǎn)介
Session是服務(wù)端驗(yàn)證客戶端身份的一種機(jī)制。而Cookie是客戶端存儲(chǔ)的一種身份憑證,由服務(wù)端在回應(yīng)的消息頭中通過Set-Cookie字段“種”在客戶端。以后每次客戶端在向服務(wù)端請(qǐng)求時(shí)都會(huì)在消息頭中帶上Cookie字段。服務(wù)端就會(huì)根據(jù)Cookie的來判斷此次請(qǐng)求是從哪個(gè)用戶發(fā)過來的,是否是一次有效請(qǐng)求等。有關(guān)Cookie的標(biāo)準(zhǔn)定義可以參考 RFC6265。以下我們稍做介紹。
舉個(gè)例子
首次打開瀏覽器請(qǐng)求http://www.baidu.com ,我們會(huì)得到一個(gè)response消息頭如下格式。
可以看到里面包含了多條Set-Cookie,Cookie是key-value形式的。每條Cookie都有一個(gè)效信息字段,有些還含有expires、domain和path等字段。其中的domain和path字段,區(qū)分了不同的cookie可以被哪些網(wǎng)頁鏈接獲得,如未設(shè)置domain,則使用當(dāng)前的訪問鏈接替代。比如上面的BD_HOME=0,我們?cè)谙卤韀本地cookie信息]中,一樣可以看到它的domain為www.baidu.com,這是因?yàn)槲覀冊(cè)L問的就是這個(gè)地址。其中的expires和max-age定義了該Cookie的有效期,失效的Cookie在下次請(qǐng)求時(shí)不會(huì)被加入到Cookie頭中。
請(qǐng)求后我們查看瀏覽器的Cookie表,可以看到這些Cookie信息被“種”下了。
此時(shí)再發(fā)送一條請(qǐng)求,可以看到請(qǐng)求的消息頭如下:
不同網(wǎng)站的請(qǐng)求,會(huì)因?yàn)檎?qǐng)求鏈接的不同,只能從瀏覽器取得屬于自己的cookie,根據(jù)上文提到的domain和path字段來區(qū)分。關(guān)于匹配的詳細(xì)規(guī)則還是可以查看RFC6265。這邊我們用domain的匹配來稍微說下,在標(biāo)準(zhǔn)的5.1.3條目是有關(guān)domain matching的。
A string domain-matches a given domain string if at least one of the
following conditions hold:
o The domain string and the string are identical. (Note that both
the domain string and the string will have been canonicalized to
lower case at this point.)
o All of the following conditions hold:
* The domain string is a suffix of the string.
* The last character of the string that is not included in the
domain string is a %x2E (".") character.
* The string is a host name (i.e., not an IP address).
從中我們可以看出domain要滿足兩點(diǎn)才能算是匹配,第一點(diǎn),完全相同;如果第一點(diǎn)不滿足則需要,請(qǐng)求鏈接的后綴包含存儲(chǔ)的cookie的domain,并且不包含部分的最后一個(gè)字符還得是".",并且還是是個(gè)域名,而非IP地址。domain匹配成功后,還要匹配path,path的匹配要比domain復(fù)雜一些,具體可以查看標(biāo)準(zhǔn)的5.1.4。path匹配可以做到一個(gè)網(wǎng)站下的頁面可以分別存儲(chǔ)不同的cookie,也可以共享上層父頁面的cookie,比較靈活。
客戶端實(shí)現(xiàn)
Android自身所帶的HttpUrlConnection方法是默認(rèn)不開啟Cookie存儲(chǔ)的。不過我們可以用java提供的幾個(gè)類來在Android中實(shí)現(xiàn):
可以先在所有請(qǐng)求之前聲明
CookieHandler.setDefault(new CookieManager(null, CookiePolicy.ACCEPT_ALL));
開啟此開關(guān)后,每次請(qǐng)求的Set-Cookie信息都會(huì)被CookieManager處理。CookieManager又會(huì)使用第一個(gè)參數(shù)傳入的CookieStore來處理Cookie的存儲(chǔ)問題,因?yàn)榇颂巶魅肓薾ull,系統(tǒng)會(huì)默認(rèn)調(diào)用一個(gè)基于CookieStore實(shí)現(xiàn)的CookieStoreImpl類來處理Cookie的存儲(chǔ),這個(gè)類的只有基于內(nèi)存的存儲(chǔ),當(dāng)進(jìn)程被殺死后,下次再進(jìn)入應(yīng)用,保存的Cookie信息就會(huì)丟失。所以我們需要基于CookieStore這個(gè)接口實(shí)現(xiàn)一個(gè)具有內(nèi)存和本地雙存儲(chǔ)機(jī)制的Cookie存儲(chǔ)類。
可以參考:Fran Montiel實(shí)現(xiàn)的PersistentCookieStore 類:
https://gist.github.com/franmontiel/ed12a2295566b7076161
當(dāng)解決了Cookie的存儲(chǔ)后,我們就需要考慮以后我們的每次請(qǐng)求需要在請(qǐng)求的消息頭中加入Cookie字段。以上用CookieStore存儲(chǔ)下來的Cookie信息都會(huì)被保存成HttpCookie形式的信息。我們可以上面的百度例子中看到Cookie的組成樣式,所以我們可以提取CookieStore中的信息并組合。
StringBuilder cookieBuilder = new StringBuilder();
String divider = "";
for (HttpCookie cookie : getCookies()) {
cookieBuilder.append(divider);
divider = ";";
cookieBuilder.append(cookie.getName());
cookieBuilder.append("=");
cookieBuilder.append(cookie.getValue());
}
cookieString = cookieBuilder.toString();
然后把這個(gè)cookieString在以后的請(qǐng)求中加入到頭中,如果你用HttpUrlConnection,你就可以
setRequestProperty("Cookie",cookieString);
如果你需要讓應(yīng)用中打開的WebView頁面也能共享使用Cookie,則需要使用android.webkit.CookieManager類來設(shè)置,簡(jiǎn)單式例代碼如下。注意,第一個(gè)參數(shù)要使用鏈接的host部分。這樣讓web端的不同頁面也可以共享這些cookie。
for (HttpCookie cookie : getCookies()) {
CookieManager.getInstance().setCookie(Uri.parse(url).getHost(), cookie.toString());
}
OK,大功告成。
以下是我在Github上開源的一個(gè)基于Volley實(shí)現(xiàn)的網(wǎng)絡(luò)層框架,也包括Cookie機(jī)制的Http請(qǐng)求,歡迎大家fork:
https://github.com/CPPAlien/DaVinci
作者簡(jiǎn)介
彭濤(@彭濤me) 致力于讓技術(shù)變得易懂且有趣
個(gè)人博客:http://pengtao.me, GitHub地址:https://github.com/CPPAlien