作者:碼海
鏈接:https://www.zhihu.com/question/19786827/answer/2064471064
來源:知乎
著作權(quán)歸作者所有愈涩。商業(yè)轉(zhuǎn)載請聯(lián)系作者獲得授權(quán)背镇,非商業(yè)轉(zhuǎn)載請注明出處棒掠。
這篇文章讓你徹底看懂 Cookie,Session, Token 這三者的區(qū)別还惠,相信大家看完肯定有收獲僧鲁!
Cookie
1991 年 HTTP 0.9 誕生了嘿歌,當(dāng)時只是為了滿足大家瀏覽 web 文檔的要求 哪雕,所以只有 GET 請求,瀏覽完了就走了,兩個連接之間是沒有任何聯(lián)系的炕桨,這也是 HTTP 為無狀態(tài)的原因饭尝,因?yàn)樗Q生之初就沒有這個需求。
但隨著交互式 Web 的興起(所謂交互式就是你不光可以瀏覽献宫,還可以登錄钥平,發(fā)評論,購物等用戶操作的行為)姊途,單純地瀏覽 web 已經(jīng)無法滿足人們的要求涉瘾,比如隨著網(wǎng)上購物的興起,需要記錄用戶的購物車記錄捷兰,就需要有一個機(jī)制記錄每個連接的關(guān)系立叛,這樣我們就知道加入購物車的商品到底屬于誰了,于是 Cookie 就誕生了贡茅。
Cookie囚巴,有時也用其復(fù)數(shù)形式 Cookies。類型為“小型文本文件”友扰,是某些網(wǎng)站為了辨別用戶身份,進(jìn)行 Session 跟蹤而儲存在用戶本地終端上的數(shù)據(jù)(通常經(jīng)過加密)庶柿,由用戶客戶端計(jì)算機(jī)暫時或永久保存的信息 村怪。
工作機(jī)制如下
以加入購物車為例,每次瀏覽器請求后 server 都會將本次商品 id 存儲在 Cookie 中返回給客戶端浮庐,客戶端會將 Cookie 保存在本地甚负,下一次再將上次保存在本地的 Cookie 傳給 server 就行了,這樣每個 Cookie 都保存著用戶的商品 id审残,購買記錄也就不會丟失了
仔細(xì)觀察上圖相信你不難發(fā)現(xiàn)隨著購物車內(nèi)的商品越來越多梭域,每次請求的 cookie 也越來越大,這對每個請求來說是一個很大的負(fù)擔(dān)搅轿,我只是想將一個商品加入購買車病涨,為何要將歷史的商品記錄也一起返回給 server ?購物車信息其實(shí)已經(jīng)記錄在 server 了璧坟,瀏覽器這樣的操作豈不是多此一舉既穆?怎么改進(jìn)呢
Session
仔細(xì)考慮下,由于用戶的購物車信息都會保存在 Server 中雀鹃,所以在 Cookie 里只要保存能識別用戶身份的信息幻工,知道是誰發(fā)起了加入購物車操作即可,這樣每次請求后只要在 Cookie 里帶上用戶的身份信息黎茎,請求體里也只要帶上本次加入購物車的商品 id囊颅,大大減少了 cookie 的體積大小,我們把這種能識別哪個請求由哪個用戶發(fā)起的機(jī)制稱為 Session(會話機(jī)制),生成的能識別用戶身份信息的字符串稱為 sessionId踢代,它的工作機(jī)制如下
- 首先用戶登錄盲憎,server 會為用戶生成一個 session,為其分配唯一的 sessionId奸鬓,這個 sessionId 是與某個用戶綁定的焙畔,也就是說根據(jù)此 sessionid(假設(shè)為 abc) 可以查詢到它到底是哪個用戶,然后將此 sessionid 通過 cookie 傳給瀏覽器
- 之后瀏覽器的每次添加購物車請求中只要在 cookie 里帶上 sessionId=abc 這一個鍵值對即可串远,server 根據(jù) sessionId 找到它對應(yīng)的用戶后宏多,把傳過來的商品 id 保存到 server 中對應(yīng)用戶的購物車即可
可以看到通過這種方式再也不需要在 cookie 里傳所有的購物車的商品 id 了,大大減輕了請求的負(fù)擔(dān)澡罚!
另外通過上文不難觀察出 cookie 是存儲在 client 的伸但,而 session 保存在 server,sessionId 需要借助 cookie 的傳遞才有意義留搔。
session 的痛點(diǎn)
看起來通過 cookie + session 的方式是解決了問題更胖, 但是我們忽略了一個問題,上述情況能正常工作是因?yàn)槲覀兗僭O(shè) server 是單機(jī)工作的隔显,但實(shí)際在生產(chǎn)上却妨,為了保障高可用,一般服務(wù)器至少需要兩臺機(jī)器括眠,通過負(fù)載均衡的方式來決定到底請求該打到哪臺機(jī)器上彪标。
如圖示:客戶端請求后,由負(fù)載均衡器(如 Nginx)來決定到底打到哪臺機(jī)器
假設(shè)登錄請求打到了 A 機(jī)器掷豺,A 機(jī)器生成了 session 并在 cookie 里添加 sessionId 返回給了瀏覽器捞烟,那么問題來了:下次添加購物車時如果請求打到了 B 或者 C,由于 session 是在 A 機(jī)器生成的当船,此時的 B,C 是找不到 session 的题画,那么就會發(fā)生無法添加購物車的錯誤,就得重新登錄了德频,此時請問該怎么辦苍息。主要有以下三種解決方案
1、session 復(fù)制
A 生成 session 后復(fù)制到 B, C抱婉,這樣每臺機(jī)器都有一份 session档叔,無論添加購物車的請求打到哪臺機(jī)器,由于 session 都能找到蒸绩,故不會有問題
這種方式雖然可行衙四,但缺點(diǎn)也很明顯:
- 同一樣的一份 session 保存了多份,數(shù)據(jù)冗余
- 如果節(jié)點(diǎn)少還好患亿,但如果節(jié)點(diǎn)多的話传蹈,特別是像阿里押逼,微信這種由于 DAU 上億,可能需要部署成千上萬臺機(jī)器惦界,這樣節(jié)點(diǎn)增多復(fù)制造成的性能消耗也會很大挑格。
2、session 粘連
這種方式是讓每個客戶端請求只打到固定的一臺機(jī)器上沾歪,比如瀏覽器登錄請求打到 A 機(jī)器后漂彤,后續(xù)所有的添加購物車請求也都打到 A 機(jī)器上,Nginx 的 sticky 模塊可以支持這種方式灾搏,支持按 ip 或 cookie 粘連等等挫望,如按 ip 粘連方式如下
upstream tomcats {
ip_hash;
server 10.1.1.107:88;
server 10.1.1.132:80;
}
這樣的話每個 client 請求到達(dá) Nginx 后,只要它的 ip 不變狂窑,根據(jù) ip hash 算出來的值會打到固定的機(jī)器上媳板,也就不存在 session 找不到的問題了,當(dāng)然不難看出這種方式缺點(diǎn)也是很明顯泉哈,對應(yīng)的機(jī)器掛了怎么辦蛉幸?
3、session 共享
這種方式也是目前各大公司普遍采用的方案丛晦,將 session 保存在 redis奕纫,memcached 等中間件中,請求到來時烫沙,各個機(jī)器去這些中間件取一下 session 即可若锁。
缺點(diǎn)其實(shí)也不難發(fā)現(xiàn),就是每個請求都要去 redis 取一下 session斧吐,多了一次內(nèi)部連接,消耗了一點(diǎn)性能仲器,另外為了保證 redis 的高可用煤率,必須做集群,當(dāng)然了對于大公司來說, redis 集群基本都會部署乏冀,所以這方案可以說是大公司的首選了蝶糯。
Token:no session!
通過上文分析我們知道通過在服務(wù)端共享 session 的方式可以完成用戶的身份定位,但是不難發(fā)現(xiàn)也有一個小小的瑕疵:搞個校驗(yàn)機(jī)制我還得搭個 redis 集群辆沦?大廠確實(shí) redis 用得比較普遍昼捍,但對于小廠來說可能它的業(yè)務(wù)量還未達(dá)到用 redis 的程度,所以有沒有其他不用 server 存儲 session 的用戶身份校驗(yàn)機(jī)制呢肢扯,這就是我們今天要介紹的主角:token妒茬。
首先請求方輸入自己的用戶名,密碼蔚晨,然后 server 據(jù)此生成 token乍钻,客戶端拿到 token 后會保存到本地肛循,之后向 server 請求時在請求頭帶上此 token 即可。
相信大家看了上圖會發(fā)現(xiàn)存在兩個問題
1银择、 token 只存儲在瀏覽器中多糠,服務(wù)端卻沒有存儲,這樣的話我隨便搞個 token 傳給 server 也行浩考?
答:server 會有一套校驗(yàn)機(jī)制夹孔,校驗(yàn)這個 token 是否合法。
2析孽、怎么不像 session 那樣根據(jù) sessionId 找到 userid 呢搭伤,這樣的話怎么知道是哪個用戶?
答:token 本身可以帶 uid 信息绿淋,解密后就可以獲取
第一個問題闷畸,如何校驗(yàn) token 呢?我們可以借鑒 HTTPS 的簽名機(jī)制來校驗(yàn)吞滞。先來看 jwt token 的組成部分
可以看到 token 主要由三部分組成 1. header:指定了簽名算法 2. payload:可以指定用戶 id佑菩,過期時間等非敏感數(shù)據(jù) 3. Signature: 簽名,server 根據(jù) header 知道它該用哪種簽名算法裁赠,再用密鑰根據(jù)此簽名算法對 head + payload 生成簽名殿漠,這樣一個 token 就生成了。
當(dāng) server 收到瀏覽器傳過來的 token 時佩捞,它會首先取出 token 中的 header + payload绞幌,根據(jù)密鑰生成簽名,然后再與 token 中的簽名比對一忱,如果成功則說明簽名是合法的莲蜘,即 token 是合法的。而且你會發(fā)現(xiàn) payload 中存有我們的 userId帘营,所以拿到 token 后直接在 payload 中就可獲取 userid票渠,避免了像 session 那樣要從 redis 去取的開銷
畫外音:header, payload 實(shí)際上是以 base64 的形式存在的,文中為了描述方便芬迄,省去了這一步问顷。
你會發(fā)現(xiàn)這種方式確實(shí)很妙,只要 server 保證密鑰不泄露禀梳,那么生成的 token 就是安全的杜窄,因?yàn)槿绻麄卧?token 的話在簽名驗(yàn)證環(huán)節(jié)是無法通過的,就此即可判定 token 非法算途。
可以看到通過這種方式有效地避免了 token 必須保存在 server 的弊端塞耕,實(shí)現(xiàn)了分布式存儲,不過需要注意的是嘴瓤,token 一旦由 server 生成荷科,它就是有效的唯咬,直到過期,無法讓 token 失效畏浆,除非在 server 為 token 設(shè)立一個黑名單胆胰,在校驗(yàn) token 前先過一遍此黑名單,如果在黑名單里則此 token 失效刻获,但一旦這樣做的話蜀涨,那就意味著黑名單就必須保存在 server,這又回到了 session 的模式蝎毡,那直接用 session 不香嗎厚柳。所以一般的做法是當(dāng)客戶端登出要讓 token 失效時,直接在本地移除 token 即可沐兵,下次登錄重新生成 token 就好别垮。
另外需要注意的是 Token 一般是放在 header 的 Authorization 自定義頭里,不是放在 Cookie 里的扎谎,這主要是為了解決跨域不能共享 Cookie 的問題 (下文詳述)
Cookie 與 Token 的簡單總結(jié)
Cookie 有哪些局限性碳想?
1、 Cookie 跨站是不能共享的毁靶,這樣的話如果你要實(shí)現(xiàn)多應(yīng)用(多系統(tǒng))的單點(diǎn)登錄(SSO)胧奔,使用 Cookie 來做需要的話就很困難了(要用比較復(fù)雜的 trick 來實(shí)現(xiàn),有興趣的話可以看文末參考鏈接)
畫外音: 所謂單點(diǎn)登錄预吆,是指在多個應(yīng)用系統(tǒng)中龙填,用戶只需要登錄一次就可以訪問所有相互信任的應(yīng)用系統(tǒng)。
但如果用 token 來實(shí)現(xiàn) SSO 會非常簡單拐叉,如下
只要在 header 中的 authorize 字段(或其他自定義)加上 token 即可完成所有跨域站點(diǎn)的認(rèn)證岩遗。
2、 在移動端原生請求是沒有 cookie 之說的凤瘦,而 sessionid 依賴于 cookie喘先,sessionid 就不能用 cookie 來傳了,如果用 token 的話廷粒,由于它是隨著 header 的 authoriize 傳過來的,也就不存在此問題红且,換句話說 token 天生支持移動平臺坝茎,天生就就支持所有平臺,可擴(kuò)展性好
綜上所述暇番,token 具有存儲實(shí)現(xiàn)簡單嗤放,擴(kuò)展性好這些特點(diǎn)。
token 有哪些缺點(diǎn)
那有人就問了壁酬,既然 token 這么好次酌,那為什么各個大公司幾乎都采用共享 session 的方式呢恨课,可能很多少人是第一次聽到 token,token 不香嗎岳服,因?yàn)?token 有以下兩點(diǎn)劣勢:
1剂公、 token 太長了
token 是 header, payload 編碼后的樣式,所以一般要比 sessionId 長很多吊宋,很有可能超出 cookie 的大小限制(cookie 一般有大小限制的纲辽,如 4kb),如果你在 token 中存儲的信息越長璃搜,那么 token 本身也會越長拖吼,這樣的話由于你每次請求都會帶上 token,對請求來是個不小的負(fù)擔(dān)
2这吻、 不太安全
網(wǎng)上很多文章說 token 更安全吊档,其實(shí)不然,細(xì)心的你可能發(fā)現(xiàn)了唾糯,我們說 token 是存在瀏覽器的怠硼,再細(xì)問,存在瀏覽器的哪里趾断?既然它太長放在 cookie 里可能導(dǎo)致 cookie 超限拒名,那就只好放在 local storage 里,這樣會造成嚴(yán)重的安全問題芋酌,因?yàn)?local storage 這類的本地存儲是可以被 JS 直接讀取的增显,另外由上文也提到,token 一旦生成無法讓其失效脐帝,必須等到其過期才行同云,這樣的話如果服務(wù)端檢測到了一個安全威脅,也無法使相關(guān)的 token 失效堵腹。
所以 token 更適合一次性的命令認(rèn)證炸站,設(shè)置一個比較短的有效期
一些誤解: Cookie 相比 token 更不安全,比如 CSRF 攻擊
首先我們需要解釋下 CSRF 攻擊是怎么回事
攻擊者通過一些技術(shù)手段欺騙用戶的瀏覽器去訪問一個自己曾經(jīng)認(rèn)證過的網(wǎng)站并運(yùn)行一些操作(如發(fā)郵件疚顷,發(fā)消息旱易,甚至財(cái)產(chǎn)操作如轉(zhuǎn)賬和購買商品)。由于瀏覽器曾經(jīng)認(rèn)證過(cookie 里帶來 sessionId 等身份認(rèn)證的信息)腿堤,所以被訪問的網(wǎng)站會認(rèn)為是真正的用戶操作而去運(yùn)行阀坏。
比如用戶登錄了某銀行網(wǎng)站(假設(shè)為 http://www.examplebank.com/,并且轉(zhuǎn)賬地址為 http://www.examplebank.com/withdraw?amount=1000&transferTo=PayeeName)笆檀,登錄后 cookie 里會包含登錄用戶的 sessionid忌堂,攻擊者可以在另一個網(wǎng)站上放置如下代碼
<img src="http://www.examplebank.com/withdraw?account=Alice&amount=1000&for=Badman">
那么如果正常的用戶誤點(diǎn)了上面這張圖片,由于相同域名的請求會自動帶上 cookie酗洒,而 cookie 里帶有正常登錄用戶的 sessionid士修,類似上面這樣的轉(zhuǎn)賬操作在 server 就會成功枷遂,會造成極大的安全風(fēng)險(xiǎn)
CSRF 攻擊的根本原因在于對于同樣域名的每個請求來說,它的 cookie 都會被自動帶上棋嘲,這個是瀏覽器的機(jī)制決定的酒唉,所以很多人據(jù)此認(rèn)定 cookie 不安全。
使用 token 確實(shí)避免了CSRF 的問題封字,但正如上文所述黔州,由于 token 保存在 local storage,它會被 JS 讀取阔籽,從存儲角度來看也不安全(實(shí)際上防護(hù) CSRF 攻擊的正確方式只有 CSRF token)
所以不管是 cookie 還是 token流妻,從存儲角度來看其實(shí)都不安全,我們所說的的安全更多的是強(qiáng)調(diào)傳輸中的安全笆制,可以用 HTTPS 協(xié)議來傳輸绅这, 這樣的話請求頭都能被加密,也就保證了傳輸中的安全在辆。
其實(shí)我們把 cookie 和 token 比較本身就不合理证薇,一個是存儲方式,一個是驗(yàn)證方式匆篓,正確的比較應(yīng)該是 session vs token浑度。
總結(jié)
session 和 token 本質(zhì)上是沒有區(qū)別的,都是對用戶身份的認(rèn)證機(jī)制鸦概,只是他們實(shí)現(xiàn)的校驗(yàn)機(jī)制不一樣而已(一個保存在 server箩张,通過在 redis 等中間件獲取來校驗(yàn),一個保存在 client窗市,通過簽名校驗(yàn)的方式來校驗(yàn))先慷,多數(shù)場景上使用 session 會更合理,但如果在單點(diǎn)登錄咨察,一次性命令上使用 token 會更合適论熙,最好在不同的業(yè)務(wù)場景中合理選型,才能達(dá)到事半功倍的效果摄狱。