新聞中心
我們知道,http 是無(wú)狀態(tài)的,也就是說(shuō)上一次請(qǐng)求和下一次請(qǐng)求之間沒(méi)有任何關(guān)聯(lián)。但是我們要實(shí)現(xiàn)應(yīng)用的功能,很多時(shí)候是需要有狀態(tài)的,比如登錄之后,再添加購(gòu)物車,那就應(yīng)該識(shí)別出是登錄用戶做的。

怎么給 http 請(qǐng)求添加上狀態(tài)呢?
這個(gè)問(wèn)題的解決有兩種方案:服務(wù)端存儲(chǔ)的 session + cookie 的方案,客戶端存儲(chǔ)的 token 的方案。
但其實(shí)這兩種方案都不怎么樣,都不夠完美。
為什么這么說(shuō)呢?我們分別來(lái)看一下:
服務(wù)端存儲(chǔ)的 session + cookie
給 http 添加狀態(tài),那就給每個(gè)請(qǐng)求打上個(gè)標(biāo)記,然后在服務(wù)端存儲(chǔ)這個(gè)標(biāo)記對(duì)應(yīng)的數(shù)據(jù)。這樣每個(gè)被標(biāo)記的請(qǐng)求都可以找到對(duì)應(yīng)的數(shù)據(jù),自然可以做到登錄、權(quán)限等狀態(tài)的存儲(chǔ)。
這個(gè)標(biāo)記應(yīng)該是自動(dòng)帶上的,所以 http 設(shè)計(jì)了 cookie 的機(jī)制,在里面存儲(chǔ)的數(shù)據(jù)是每次請(qǐng)求都會(huì)帶上的。
然后根據(jù) cookie 里的標(biāo)記去查找的服務(wù)端對(duì)應(yīng)的數(shù)據(jù)叫做 session,這個(gè)標(biāo)記就是 session 的 id。
如圖,因?yàn)檎?qǐng)求自動(dòng)帶上 cookie,那兩次請(qǐng)求就都可以找到 id 為 1 對(duì)應(yīng)的 session,自然就知道當(dāng)前登錄的用戶是誰(shuí),也可以存儲(chǔ)其他的狀態(tài)數(shù)據(jù)。
這就是 session + cookie 的給 http 添加狀態(tài)的方案。
大家覺(jué)得這種方案有問(wèn)題么?
有問(wèn)題,而且問(wèn)題還挺多的。
最大的一個(gè)問(wèn)題就是臭名昭著的 CSRF(跨站請(qǐng)求偽造):
CSRF
因?yàn)?cookie 會(huì)在請(qǐng)求時(shí)自動(dòng)帶上,那你在一個(gè)網(wǎng)站登錄了,再訪問(wèn)別的網(wǎng)站,萬(wàn)一里面有個(gè)按鈕會(huì)請(qǐng)求之前那個(gè)網(wǎng)站的,那 cookie 依然能帶上。而這時(shí)候就不用再登錄了。
這樣萬(wàn)一點(diǎn)了這個(gè)按鈕之后做了一些危險(xiǎn)的操作呢?
是不是就很危險(xiǎn)。
而且一般這種利用 CSRF 漏洞的網(wǎng)站都會(huì)偽裝的很好,讓你很難看出破綻來(lái),這種網(wǎng)站叫做釣魚網(wǎng)站。
為了解決這個(gè)問(wèn)題,我們一般會(huì)驗(yàn)證 referer,就是請(qǐng)求是哪個(gè)網(wǎng)站發(fā)起的,如果發(fā)起請(qǐng)求的網(wǎng)站不對(duì),那就阻止掉。
但這樣依然不能完全解決問(wèn)題,萬(wàn)一你用的瀏覽器也是有問(wèn)題的,能偽造 referer 呢?
所以一般會(huì)用隨機(jī)值來(lái)解決,每次登錄隨機(jī)生成一個(gè)值,放到 session 中,后面的請(qǐng)求需要包含這個(gè)值才行,否則就認(rèn)為是非法的。
這個(gè)隨機(jī)值叫做 token,可以放在參數(shù)中,也可以放在 header 中,因?yàn)獒烎~網(wǎng)站拿不到這個(gè)隨機(jī)值,就算帶了 cookie 也沒(méi)發(fā)通過(guò)服務(wù)端的驗(yàn)證。
這是 session + cookie 這種方案的一個(gè)缺點(diǎn),但是是有解決方案的。
它還有別的缺點(diǎn),比如分布式的時(shí)候:
分布式 session
session 是把狀態(tài)數(shù)據(jù)保存在服務(wù)端,那么問(wèn)題來(lái)了,如果有多臺(tái)服務(wù)器呢?
當(dāng)并發(fā)量上去了,單臺(tái)服務(wù)器根本承受不了,自然需要做集群,也就需要多臺(tái)服務(wù)器來(lái)提供服務(wù)。
而且現(xiàn)在后端還會(huì)把不同的功能拆分到不同的服務(wù)中,也就是微服務(wù)架構(gòu),自然也需要多臺(tái)服務(wù)器。
那不同服務(wù)器之間的 session 怎么同步?
登錄之后 session 是保存在某一臺(tái)服務(wù)器的,之后可能會(huì)訪問(wèn)到別的服務(wù)器,這時(shí)候那臺(tái)服務(wù)器是沒(méi)有對(duì)應(yīng)的 session 的,就沒(méi)法完成對(duì)應(yīng)的功能。
這個(gè)問(wèn)題的解決有兩種方案:
一種是 session 復(fù)制,也就是通過(guò)一種機(jī)制在各臺(tái)機(jī)器自動(dòng)復(fù)制 session,并且每次修改都同步下。這個(gè)有對(duì)應(yīng)的框架來(lái)做,比如 java 的 spring-session。
各臺(tái)服務(wù)器都做了 session 復(fù)制了,那你訪問(wèn)任何一臺(tái)都能找到對(duì)應(yīng)的 session。
還有一種方案是把 session 保存在 redis,這樣每臺(tái)服務(wù)器都去那里查,只要一臺(tái)服務(wù)器登錄了,其他的服務(wù)器也就能查到 session,這樣就不需要復(fù)制了。
還好,session 在分布式時(shí)的這個(gè)問(wèn)題也算是有解決方案的。
但你你以為這就完了么?session + cookie 還有跨域的問(wèn)題:
跨域
cookie 為了安全,是做了 domain 的限制的,設(shè)置 cookie 的時(shí)候會(huì)指定一個(gè) domain,只有這個(gè) domain 的請(qǐng)求才會(huì)帶上這個(gè) cookie。
而且還可以設(shè)置過(guò)期時(shí)間、路徑等:
那萬(wàn)一是不同 domain 的請(qǐng)求呢?也就是跨域的時(shí)候,怎么帶 cookie 呢?
a.guang.com 和 b.guang.com 這種還好,只要把 domain 設(shè)置為頂級(jí)域名 guang.com 就可以了,那二三級(jí)域名不同也能自動(dòng)帶上。
但如果頂級(jí)域名也不同就沒(méi)辦法了,這種只能在服務(wù)端做下中轉(zhuǎn),把這倆個(gè)域名統(tǒng)一成同一個(gè)。
上面說(shuō)的不是 ajax 請(qǐng)求,ajax 請(qǐng)求有額外的機(jī)制:
ajax 請(qǐng)求跨域的時(shí)候是不會(huì)挾帶 cookie 的,除非手動(dòng)設(shè)置 withCredentials 為 true 才可以。
而且也要求后端代碼設(shè)置了對(duì)應(yīng)的 header:
Access-Control-Allow-Origin: "當(dāng)前域名";
Access-Control-Allow-Credentials: true
這里的 allow origin 設(shè)置 * 都不行,必須指定具體的域名才能接收跨域 cookie。
這是 session + cookie 方式的第三個(gè)坑,好在也是有解決方案的。
我們做下小結(jié):
session + cookie 的給 http 添加狀態(tài)的方案是服務(wù)端保存 session 數(shù)據(jù),然后把 id 放入 cookie 返回,cookie 是自動(dòng)攜帶的,每個(gè)請(qǐng)求可以通過(guò) cookie 里的 id 查找到對(duì)應(yīng)的 session,從而實(shí)現(xiàn)請(qǐng)求的標(biāo)識(shí)。這種方案能實(shí)現(xiàn)需求,但是有 CSRF、分布式 session、跨域等問(wèn)題,不過(guò)都是有解決方案的。
session + cookie 的方案確實(shí)不太完美,我們?cè)賮?lái)看另一種方式怎么樣:
客戶端存儲(chǔ)的 token
session + cookie 的方案是把狀態(tài)數(shù)據(jù)保存在服務(wù)端,再把 id 保存在 cookie 里來(lái)實(shí)現(xiàn)的。既然這樣的方案有那么多的問(wèn)題,那我反其道而行之,不把狀態(tài)保存在服務(wù)端了,直接全部放在請(qǐng)求里,也不放在 cookie 里了,而是放在 header 里,這樣是不是就能解決那一堆問(wèn)題了呢?
token 的方案常用 json 格式來(lái)保存,叫做 json web token,簡(jiǎn)稱 JWT,我們就拿這個(gè)來(lái)說(shuō)吧。
JWT 是保存在 request header 里的一段字符串(比如用 header 名可以叫 authorization),它分為三部分:
如圖 JWT 是由 header、payload、verify signature 三部分組成的:
header 部分保存當(dāng)前的加密算法,payload 部分是具體存儲(chǔ)的數(shù)據(jù),verify signature 部分是把 header 和 payload 還有 salt 做一次加密之后生成的。(salt,鹽,就是一段任意的字符串,增加隨機(jī)性)。
這三部分會(huì)分別做 Base64,然后連在一起就是 JWT 的 header,放到某個(gè) header 比如 authorization 中:
authorization: barer xxxxx.xxxxx.xxxx
請(qǐng)求的時(shí)候把這個(gè) header 帶上,服務(wù)端就可以解析出對(duì)應(yīng)的 header、payload、verify signature 這三部分,然后根據(jù) header 里的算法也對(duì) header、payload 加上 salt 做一次加密,如果得出的結(jié)果和 verify signature 一樣,就接受這個(gè) token。
把狀態(tài)數(shù)據(jù)都保存在 payload 部分,這樣就實(shí)現(xiàn)了有狀態(tài)的 http:
而且這種方式是沒(méi)有 session + cookie 那些問(wèn)題的,不信我們分別來(lái)看一下:
CSRF:因?yàn)椴皇峭ㄟ^(guò)自動(dòng)帶的 cookie 來(lái)關(guān)聯(lián)服務(wù)端的 session 保存的狀態(tài),所以沒(méi)有 CSRF 問(wèn)題,沒(méi)法通過(guò) cookie 攻擊。
分布式 session:因?yàn)闋顟B(tài)不是保存在服務(wù)端,所以無(wú)論訪問(wèn)哪臺(tái)服務(wù)器都行,只要能從 token 里解析出狀態(tài)數(shù)據(jù)就行。
跨域:因?yàn)椴皇?cookie 那一套,自然也沒(méi)有跨域的限制,只要手動(dòng)帶上 JWT 的 header 就行。
看起來(lái)這種方式好像很完美?
其實(shí)也不是,JWT 有 JWT 的問(wèn)題:
安全性
因?yàn)?JWT 把數(shù)據(jù)直接 Base64 之后就放在了 header 里,那別人就可以輕易從中拿到狀態(tài)數(shù)據(jù),比如用戶名等敏感信息,也能根據(jù)這個(gè) JWT 去偽造請(qǐng)求。
所以 JWT 要搭配 https 來(lái)用,讓別人拿不到 header。
性能
JWT 把狀態(tài)數(shù)據(jù)都保存在了 header 里,每次請(qǐng)求都會(huì)帶上,比起只保存?zhèn)€ id 的 cookie 來(lái)說(shuō),請(qǐng)求的內(nèi)容變多了,性能也會(huì)差一些。
所以 JWT 里也不要保存太多數(shù)據(jù)。
沒(méi)法讓 JWT 失效
session 因?yàn)槭谴嬖诜?wù)端的,那我們就可以隨時(shí)讓它失效,而 JWT 不是,因?yàn)槭潜4嬖诳蛻舳耍俏覀兪菦](méi)法手動(dòng)讓他失效的。
所以 JWT 的過(guò)期時(shí)間不要設(shè)置的太長(zhǎng)。
所以說(shuō),JWT 的方案雖然解決了很多 session + cookie 的問(wèn)題,但也不完美。
小結(jié)下:
JWT 的方案是把狀態(tài)數(shù)據(jù)保存在 header 里,每次請(qǐng)求需要手動(dòng)攜帶,沒(méi)有 session + cookie 方案的 CSRF、分布式、跨域的問(wèn)題,但是也有安全性、性能、沒(méi)法控制等問(wèn)題。
說(shuō)了這么多,還是寫下代碼心里更踏實(shí):
Nest.js 實(shí)現(xiàn)兩種方案
我們用 Nest.js 實(shí)現(xiàn)下兩種方案吧,不能光紙上談兵。
首先用 @nest/cli 快速創(chuàng)建一個(gè) Nest.js 項(xiàng)目。
npx nest new status
會(huì)生成 module、controller、service 的基礎(chǔ)代碼:
我們先實(shí)現(xiàn) session + cookie 的方式:
session + cookie
Nest.js 的底層是 express,它只是額外提供了一些架構(gòu)的劃分,所以還是 session 實(shí)現(xiàn)還是用的 express 的方案:
安裝 express-session 和它的 ts 類型定義:
npm install express-session @types/express-session
然后在入口模塊里啟用它:
指定個(gè)加密 cookie 用的密碼就行。
然后在 controller 里就可以注入 session 對(duì)象了:
我在 session 里放了個(gè) count 的變量,每次訪問(wèn)加一,然后 body 返回這個(gè) count。
這樣就可以判斷 http 請(qǐng)求是否有了狀態(tài)。
我們來(lái)測(cè)試下:
可以看到每次請(qǐng)求返回的數(shù)據(jù)都不同,而且返回了一個(gè) cookie 是 connect.sid,這個(gè)就是對(duì)應(yīng) session 的 id。
因?yàn)?cookie 在請(qǐng)求的時(shí)候會(huì)自動(dòng)帶上,就可以實(shí)現(xiàn)請(qǐng)求的標(biāo)識(shí),給 http 請(qǐng)求加上狀態(tài)。
session + cookie 的方式用起來(lái)還是很簡(jiǎn)單的,我們?cè)賮?lái)看下 jwt 的方式:
jwt
jwt 需要引入 @nestjs/jwt 這個(gè)包,然后在入口 Module 里引入 JwtModule:
引入的時(shí)候指定密碼,也就是用來(lái)加到 jwt 里的鹽,也可以指定 token 過(guò)期時(shí)間。
因?yàn)槲覀円肓?JwtModule,那就可以在 Controller 里依賴注入了:
聲明對(duì) JwtService 的依賴,Nest.js 就會(huì)自動(dòng)注入對(duì)應(yīng)的對(duì)象。
然后定義個(gè) controller 方法,通過(guò) Resonse 對(duì)象來(lái)設(shè)置 authorization 的 header:
用 jwtService 生成一個(gè) token,記錄 count,然后放到 header 里返回,同時(shí)也放在 body 里。
后面的請(qǐng)求就是取出這個(gè) header,拿到其中的數(shù)據(jù),然后 +1 之后再放回去:
這樣也實(shí)現(xiàn)了給 http 添加狀態(tài)的需求,不過(guò)是把數(shù)據(jù)保存在了 header 里。
我們通過(guò) postman 測(cè)試下:
第一次請(qǐng)求會(huì)返回一個(gè) authorization 的 header,body 是 1:
把這個(gè) header 手動(dòng)添加到請(qǐng)求 header 里,再次請(qǐng)求:
body 變成 2 了,同時(shí)也返回了一個(gè)新的 authorization 的 header。
把這個(gè)新的 authorization 放到請(qǐng)求 header 里再次請(qǐng)求:
body 變成 3 了,同時(shí)也返回了一個(gè)新的 authorization 的 header。
有同學(xué)問(wèn),我不用新的 header,還是用上次的 header 會(huì)怎么樣:
那樣會(huì)報(bào)錯(cuò):
jwt 生成一次只能用一次,這個(gè)一次性也是它的一個(gè)特點(diǎn)。
這樣,我們就分別用 Nest.js 分別實(shí)現(xiàn)了 session + cookie 和 jwt 兩種保存 http 狀態(tài)的方式。
代碼上傳到了 github:https://github.com/QuarkGluonPlasma/nestjs-exercize。
總結(jié)
http 是無(wú)狀態(tài)的,也就是請(qǐng)求和請(qǐng)求之間沒(méi)有關(guān)聯(lián),但我們很多功能的實(shí)現(xiàn)是需要保存狀態(tài)的。
給 http 添加狀態(tài)有兩種方式:
session + cookie:把狀態(tài)數(shù)據(jù)保存到服務(wù)端,session id 放到 cookie 里返回,這樣每次請(qǐng)求會(huì)帶上 cookie ,通過(guò) id 來(lái)查找到對(duì)應(yīng)的 session。這種方案有 CSRF、分布式 session、跨域的問(wèn)題。
jwt:把狀態(tài)保存在 json 格式的 token 里,放到 header 中,需要手動(dòng)帶上,沒(méi)有 cookie + session 的那些問(wèn)題,但是也有安全性、性能、沒(méi)法控制和使用一次就失效的問(wèn)題。
上面這兩種方案都不是完美的,但那些問(wèn)題也都有解決方案。
軟件領(lǐng)域很多情況下都是這樣的,某種方案都解決了一些問(wèn)題,但也相應(yīng)的帶來(lái)了一些新的問(wèn)題。沒(méi)有銀彈,還是要熟悉它們的特點(diǎn),根據(jù)不同的需求靈活選用。
網(wǎng)站題目:兩種給Http添加狀態(tài)的方式,都不完美
URL網(wǎng)址:http://www.dlmjj.cn/article/ccdsesc.html


咨詢
建站咨詢
