記一場與 cookie 的相遇

簡介: cookie 翻譯過來爲 「小甜點,一種酥性甜餅乾,很美味的...」,咳咳,打住!咱們這裏說的是 「甜點」 文件,它是瀏覽器儲存在用戶電腦上的一小段純文本格式的文件。

因爲 http 是一種無狀態的協議(無狀態是指對於客戶端每次發送的請求都認爲它是一個新的請求,上一次會話和下一次會話沒有聯繫),服務器沒法知道兩個請求是否來自於同一個瀏覽器,所以 cookie 應運而生。cookie 能夠記錄用戶的有關信息,最根本的是它能夠幫助 Web 站點保存有關訪問者的信息。javascript

咱們經過一張圖來講明下 cookie 的原理:html

 

客戶端向服務端發送請求;接收到請求後,服務端在響應頭中經過 set-cookie 攜帶 cookie 信息返回給客戶端;客戶端再次發送攜帶了 cookie 的請求;服務端根據 request header 裏的 cookie 信息校驗該請求,給出響應。前端

接下來咱們須要先簡要了解下 cookie 的屬性 [1][2]。java

cookie 屬性

1. Nameios

cookie 的名字,相同域名只容許存在一個同名 cookie;一旦建立,該名稱便不可更改。程序員

document.cookie = `jxi-m-sid=${cookie}`;

jxi-m-sid 即爲新建的 cookie 名字,該名字建立後不能修改,若是須要新的 cookie 只能再次建立。web

2. Value算法

該 cookie 的值,若是值爲 Unicode 字符,須要爲字符編碼;若是值爲二進制數據,則須要使用 BASE64 編碼。axios

document.cookie = `jxi-m-sid=${cookie}`;

${cookie} 即爲 cookie jxi-m-sid 的值,當須要修改值時,好比將 jxi-m-sid 值改成 2019,操做以下:小程序

document.cookie = `jxi-m-sid=2019`;

3. Domain

能夠訪問該 cookie 的域名。默認狀況下,domain 會被設置爲建立該 cookie 的頁面所在的域名,因此當給相同域名發送請求時該 cookie 會被髮送至服務器。

有關 domain 的設置須要注意如下幾點:

(1)設置 domain 時,前面帶點和不帶點的區別是:

帶點:任何子域名均可以訪問,包括父域名;

document.cookie = `jxi-m-sid=${cookie};domain=.xx.com;path=/`;

不帶點:只有徹底同樣的域名才能夠訪問(IE除外,仍然支持子域名訪問)

document.cookie = `jxi-m-sid=${cookie};domain=xx.com;path=/`;

(2)非頂級域名,如二級域名或者三級域名,設置的 cookie 的 domain 只能爲頂級域名或者二級域名或者三級域名自己,不能設置其餘二級域名的 cookie ,不然 cookie 沒法生成;

(3)二級域名能讀取設置了 domain 爲頂級域名或者自身的 cookie,不能讀取其餘二級域名 domain 的 cookie 。因此要想 cookie 在多個二級域名中共享,須要設置 domain 爲頂級域名;

(4)頂級域名只能獲取到 domain 設置爲頂級域名的 cookie 。

4. Path

能夠訪問該 cookie 的頁面路徑。好比 domain 是 abc.com ,path是 /test ,那麼只有 /test 路徑下的頁面能夠讀取該 cookie。

document.cookie = `jxi-m-sid=${cookie};domain=.abc.com;path=/test`;

5. Expires/Max-Age

該 cookie 的超時時間。若設置其值爲一個時間,當到達此時間後,該 cookie 失效。默認有效期爲 session ,即會話 cookie 。當瀏覽器關閉(不是瀏覽器標籤頁,而是整個瀏覽器) 後,該 cookie 失效。

例如,咱們但願該 cookie jxi-m-sid 過時,則能夠將它的超時時間設置爲客戶端本地時間1分鐘之前,代碼以下:

var exp = new Date(); //獲取客戶端本地當前系統時間
exp.setTime(exp.getTime() - 60 * 1000); //將 exp 設置爲客戶端本地時間1分鐘之前
document.cookie = `jxi-m-sid=;expires=${exp.toUTCString()};domain=.xx.com;path=/`;

注意:expires 必須是 GMT 格式的時間。

6. Size

該 cookie 的大小。cookie 的大小約 4K 左右,在全部瀏覽器中,任何 cookie 大小超過限制都會被忽略,且永遠不會被設置。

7. HTTP

cookie 的 httponly 屬性。默認狀況下,httpOnly 選項爲空,容許客戶端經過 js 去訪問(包括讀取、修改、刪除等)該 cookie ;若此屬性爲 true ,則只有在 http 請求頭中會帶有此 cookie 的信息,而不能經過 document.cookie 來訪問此 cookie,意在提供一個安全措施來幫助阻止經過 JavaScript 發起的跨站腳本攻擊 (XSS) 竊取 cookie 的行爲。

好比,服務端將 uid 設置爲不容許客戶端修改:

response.addHeader("Set-Cookie", "uid=112; Path=/; HttpOnly");

8. Secure

用來設置 cookie 只在確保安全的請求中才會發送。當請求是 https 或者其餘安全協議時,包含 secure 選項的 cookie 才能被髮送至服務器。默認狀況下,secure 選項爲空,不論是 https 協議仍是 http 協議的請求,cookie 都會被髮送至服務端。若想在客戶端經過 js 去設置 secure 類型的 cookie,必須保證網頁是 https 協議的。好比:

document.cookie = `jxi-m-sid=${cookie};secure`;

cookie 的屬性介紹完了,關於其使用須要注意:

(1)每一個 web 服務器(域名)保存的 cookie 數不能超過 50 個,每一個 cookie 大小不能超過 4KB;

(2)儘可能讓 cookie 的權限範圍小一些,能子域可見 domain 毫不設爲主域;

(3)存儲非敏感的用戶信息且設置合理的過時時間,減小所以帶來的網絡流量(文檔傳輸的負載)。

cookie 的分類

cookie 能夠分爲兩類 [3]:會話 cookie 和持久 cookie。

會話 cookie 是一種臨時 cookie,若沒有設置有效期,當用戶關閉瀏覽器時,該 cookie 就會被刪除。

對於設置了有效期的 cookie 就被稱爲持久 cookie,它能夠存儲在硬盤上,當用戶關閉瀏覽器或者機器重啓時,該 cookie 依然存在,能夠再次被讀取使用。

在經常使用的數據存儲方法中,除了本文介紹的 cookie,還有 localStorage、sessionStorage、 session。下面簡要介紹下這幾種方法的區別:

cookie 與 session

session 機制是一種服務器端的機制,服務器使用一種相似於散列表的結構(也可能就是使用散列表)來保存信息。大多數的應用都是用 cookie 來實現 session 的跟蹤,第一次建立 session 的時候,服務端會在 http 協議中告訴客戶端,須要在 cookie 裏面記錄一個 sessionid ,之後每次請求把這個會話 id 發送到服務器。

cookie 與 session 的區別 [4] 以下表所示:

特性 cookie session
存取方式 只能存儲 ASCII 字符串 存取任何類型的數據
隱私策略 存儲在客戶端,對用戶可見且用戶可對其處理 存儲在服務器
有效期 能夠經過設置較大的過時時間實現長期有效 若設置的超時時間過長,容易致使內存溢出
服務器壓力 不佔用服務器資源 併發用戶過多時會耗費大量內存
瀏覽器支持 須要瀏覽器支持,不支持時能夠經過 session 和 URL 地址重寫實現 只在當前窗口和其子窗口內有效
跨域 支持 不支持

關於 cookie 與 localStorage、sessionStorage 的區別也整理了一份圖表以下所示:

特性 cookie localStorage sessionStorage
生命週期 通常由服務器生成,可設置失效時間。若是是瀏覽器端生成,默認關閉瀏覽器後失效 除非被清除,不然永久保存 僅在當前會話下有效,關閉頁面或瀏覽器後被清除
存放數據大小 4K 左右 通常爲 5MB 同 localStorage
與服務器端通訊 始終攜帶在 http 頭中,使用過多會帶來性能問題 僅在客戶端中保存,不和服務器的通訊 同 localStorage
易用性 須要程序員本身封裝,源生的 cookie 接口不友好 源生接口能夠接受,也可再次封裝 同 localStorage

須要注意的是,向 cookie、localStorage 和 sessionStorage 中存儲數據時,都須要時刻注意是否有代碼存在 XSS 注入的風險,避免存入一些敏感數據。

重點來了:結合項目實踐中的問題討論下 cookie 跨域存儲的問題。

cookie 實踐

項目背景:在 a 小程序中經過 web-view 內嵌了 b 項目(H5,域名爲 b.mm.com)和 c 項目(H5,域名爲 c.mm.com),b 和 c 都調用了公共地址接口 interface(interface 所在的域名爲 c.mm.com)。在 a 中登陸成功以後,須要獲取接口返回的 cookie 封裝在每一個接口的 request header 中,同時須要傳遞給 b、c;b 和 c 須要獲取傳遞的 cookie 以後,寫入到 interface 可訪問的域名下,實現用戶登陸狀態的驗證。

遇到的問題:小程序內訪問線上項目 c 能夠正常使用,訪問 b 時會偶爾出現登陸驗證失敗的狀況。

服務端反饋傳入的 cookie 值有誤,致使登陸驗證不經過。瀏覽器中查看 cookie 發現:存在兩個同名但 domain 不一樣的 cookie。

問題定位:項目 b、c 獲取 cookie 的方法是同樣的,區別就在於 cookie 值的存儲與傳遞。

項目 b 中 cookie 處理流程:

(1)服務端:b 系統的 domain 爲 mm.com;

//application.yml 文件
... b: m: domain: mm.com scheme: http ...

(2)前端:獲取當前傳入的 cookie,寫入 mm.com 域名下。
在項目 b 中,b.mm.com 域名下是沒法向 c.mm.com 域名下寫入 cookie,若要實現跨域寫入,只能將 cookie 寫入到其父級域名:mm.com。

document.cookie = `jxi-m-sid=${cookie};domain=.mm.com;path=/`;

注意:b 系統中的 domain 設置爲 mm.com,主要是爲了請求 c.mm.com 下的地址接口時 request header 中攜帶存入的 cookie 。

項目 c 中 cookie 處理流程:

(1)服務端:c 系統的 domain 爲 c.mm.com;

//application.yml 文件
... c: m: domain: c.mm.com scheme: http ...

(2)前端:獲取當前傳入的 cookie,寫入的域名是 c.mm.com。

document.cookie = `jxi-m-sid=${cookie};domain=c.mm.com;path=/`;

由系統 b 和 c 中 cookie 處理流程能夠發現:寫入 cookie 時設置的 domain 不一樣,致使出現了兩個 jxi-m-sid 的 cookie,當 b 中請求地址接口時,request header 中攜帶的是錯誤的 cookie。

解決方法:爲了解決同時出現兩個同 key 的 cookie 問題,將 c 中的 domain 統一修改成二級域名:mm.com。與此同時須要注意:服務器端須要將 c 系統的 domain 也改成 mm.com。

... c: m: domain: mm.com scheme: http ...
document.cookie = `jxi-m-sid=${cookie};domain=.mm.com;path=/`;

這樣處理以後,兩個同 key cookie 的問題解決了,爲了保證寫入的 cookie 是惟一的,在每次寫入 cookie 以前作了清除同 key cookie 處理(利用 cookie 的超時時間屬性)。

removeCookie: (cookieName) => { var cookies = document.cookie.split(";"); for (var i = 0; i < cookies.length; i++) { if (cookies[i].indexOf(" ") == 0) { cookies[i] = cookies[i].substring(1); } if (cookies[i].indexOf(cookieName) == 0) { var exp = new Date(); exp.setTime(exp.getTime() - 60 * 1000); document.cookie = cookies[i] + "=;expires=" + exp.toUTCString() + ";domain=.xx.com;path=/"

            break; } } } removeCookie('jxi-m-sid');

按照這種方法處理以後,很長一段時間內 cookie 的問題正常了,可是極少狀況下仍是會偶現服務端取到的 cookie 值不是最新的問題。

終極方案:咱們想出了另一種方案:在 b 和 c 系統全部請求的 request header 中攜帶 cookie,服務端校驗用戶身份時,首先會從 request header 中獲取,沒有的話再從寫入的 cookie 中取值,解決了上述先後端 cookie 值不一致問題。

接下來咱們將項目開發中整個 cookie 處理過程分爲如下四步,簡要說下每一步的處理過程:

第一步:小程序中獲取及傳遞 cookie;

登陸成功時,後端會將 cookie 種在 response header 的 set-cookie 中,咱們獲取到該 cookie 後先進行本地存儲,而後封裝到每次接口的 request header 中。

//獲取 cookie
wx.setStorageSync('cookie', res.header["Set-Cookie"]); //統一封裝 request 請求
const request = parameter => { //url必填項
    if (!parameter || parameter == {} || !parameter.url) { console.log('Data request can not be executed without URL.'); return false; } else { var murl = parameter.url; var headerCookie = wx.getStorageSync('cookie'); //判斷是否有獨自cookie請求
        var selfCookie = parameter.selfCookie; selfCookie && (headerCookie += selfCookie); wx.request({ url: murl, data: parameter.data || {}, header: { 'Cookie': headerCookie }, method: parameter.method || 'POST', success: function(res) { parameter.success && parameter.success(res); }, fail: function(e) { parameter.fail && parameter.fail(e); }, complete: function() { parameter.complete && parameter.complete(); } }); } }

這樣在小程序內,每一個接口的 request header 中都會攜帶該 cookie,服務端能夠根據該 cookie 判斷用戶的登陸狀態。

第二步:小程序向 web-view 內嵌的 H5 中傳遞 cookie;

第三步:H5 中獲取小程序傳過來的 cookie ,經過 js 寫入 [5];

獲取 cookie:

getQueryString: (name) => { let reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)"); let r = window.location.search.substr(1).match(reg); if (r != null) { return unescape(decodeURI(r[2])); return null; } } let cookie = getQueryString('cookie');

經過 js 將 cookie 寫入到父級域名:mm.com。寫入方法以下:

document.cookie = `jxi-m-sid=${cookie};domain=.mm.com;path=/`;

第四步:接口 request header 中統一封裝 key 爲 jxi-m-sid 的 cookie,以 axios 庫的 get 和 post 請求爲例( headers 裏定義):

export default { post(url, data) { return axios({ method: 'post', url, data: JSON.stringify(data), timeout: 30000, headers: { 'jxi-m-sid': cookie } }) }, get(url, params) { return axios({ method: 'get', url, params, timeout: 30000, headers: { 'jxi-m-sid': cookie } }) } }

以上就是基於咱們的項目背景,解決兩個同 key cookie 且先後端獲取值不一致問題的解決方案。涉及到 cookie 的 domain、path 問題請你們使用時高度重視,這將決定你所寫入 cookie 的惟一性。

cookie 通常是用來存儲當前登陸用戶的會話信息且是存儲在客戶端,用戶能夠隨意修改,因此存在必定的風險。針對這個問題,也有了比較成熟的解決方法,這裏咱們簡要介紹下。

cookie 防篡改機制

敏感數據存儲在服務器

敏感數據避免存儲在 cookie 裏,能夠根據 sessionid 將其存儲在服務端。須要時根據 sessionid 獲取便可。

防篡改簽名

服務端爲每一個 cookie 生成簽名,若是用戶篡改了該 cookie 則簽名是不一致的,服務端能夠以此來判斷該 cookie 是否被篡改。

具體的實現步驟能夠以下所示:
(1)服務端提供簽名生成算法 secret;
(2)根據方法生成簽名 secret(x);
(3)將生成的簽名放到 cookie 中,可使用 | 將 cookie 內容與簽名分隔開,如 name=x|yyyyy;
(4)服務端校驗收到的內容和簽名,判斷是否被篡改。

以上的方法能夠進一步確保 cookie 的數據安全 [6],在有須要的項目中你們能夠嘗試使用下。

小結

本文主要介紹了 cookie 的相關知識,包括:經常使用屬性、修改、跨域存儲、防篡改等。對於 cookie 的跨域傳遞,上述項目實踐中的方法是咱們的使用方法,你們在使用的時候能夠根據本身項目的狀況進行選擇。

敲黑板:cookie 中不要放敏感信息哦,再次友情提示~好了,關於 cookie 的問題就介紹到這裏了,若有任何疑問,歡迎留言。

擴展閱讀

[1] https://www.quirksmode.org/js/cookies.html

[2] http://bubkoo.com/2014/04/21/http-cookies-explained/

[3] http://www.allaboutcookies.org/cookies/cookies-the-same.html

[4] https://www.jianshu.com/p/25802021be63

[5] http://www.tutorialspoint.com/javascript/javascript_cookies.htm

[6] http://www.javashuo.com/article/p-eqtpbghv-dt.html

相關文章
相關標籤/搜索