前端鑒權:cookie、session、token、jwt、單點登錄全貢獻出來了!
本文你將看到:
基于 HTTP 的前端鑒權背景
cookie 為什么是最方便的存儲方案,有哪些操作 cookie 的方式
session 方案是如何實現(xiàn)的,存在哪些問題
token 方案是如何實現(xiàn)的,如何進行編碼和防篡改?jwt 是做什么的?refresh token 的實現(xiàn)和意義
session 和 token 有什么異同和優(yōu)缺點
單點登錄是什么?實現(xiàn)思路和在瀏覽器下的處理
從狀態(tài)說起
HTTP 無狀態(tài)
我們知道,HTTP 是無狀態(tài)的。也就是說,HTTP 請求方和響應方間無法維護狀態(tài),都是一次性的,它不知道前后的請求都發(fā)生了什么。
但有的場景下,我們需要維護狀態(tài)。最典型的,一個用戶登陸微博,發(fā)布、關注、評論,都應是在登錄后的用戶狀態(tài)下的。
那解決辦法是什么呢?標記。
在學?;蚬?,入學入職那一天起,會錄入你的身份、賬戶信息,然后給你發(fā)個卡,今后在園區(qū)內(nèi),你的門禁、打卡、消費都只需要刷這張卡。
前端存儲
這就涉及到一發(fā)、一存、一帶,發(fā)好辦,登陸接口直接返回給前端,存儲就需要前端想辦法了。
前提是,你要把卡帶在身上。
前端的存儲方式有很多。
最矬的,掛到全局變量上,但這是個「體驗卡」,一次刷新頁面就沒了
高端點的,存到 cookie、localStorage 等里,這屬于「會員卡」,無論怎么刷新,只要瀏覽器沒清掉或者過期,就一直拿著這個狀態(tài)。
有地方存了,請求的時候就可以拼到參數(shù)里帶給接口了。
cookie 也是前端存儲的一種,但相比于 localStorage 等其他方式,借助 HTTP 頭、瀏覽器能力,cookie 可以做到前端無感知。
瀏覽器發(fā)起請求時,會自動把 cookie 通過 HTTP 請求頭的 Cookie 字段,帶給接口
「空間范圍」的,通過 Domain(域)/ Path(路徑)兩級。「時間范圍」,通過 Expires、Max-Age 中的一種。Max-Age 屬性指定從現(xiàn)在開始 Cookie 存在的秒數(shù),比如60 60 24 * 365(即一年)。過了這個時間以后,瀏覽器就不再保留這個 Cookie。
如果同時指定了 Expires 和 Max-Age ,那么 Max-Age 的值將優(yōu)先生效。
如果 Set-Cookie 字段沒有指定 Expires 或 Max-Age 屬性,那么這個 Cookie 就是 Session Cookie,即它只在本次對話存在,一旦用戶關閉瀏覽器,瀏覽器就不會再保留這個 Cookie。
「使用方式」。HttpOnly 屬性指定該 Cookie 無法通過 JavaScript 腳本拿到,主要是Document.cookie 屬性、XMLHttpRequest 對象和 Request API 都拿不到該屬性。這樣就防止了該 Cookie 被腳本讀到,只有瀏覽器發(fā)出 HTTP 請求時,才會帶上該 Cookie。
HTTP 返回的一個 Set-Cookie 頭用于向瀏覽器寫入「一條(且只能是一條)」cookie,格式為 cookie 鍵值 + 配置鍵值。例如:
Set-Cookie: username=jimu; domain=jimu.com; path=/blog; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnlySet-Cookie: username=jimu; domain=jimu.com
Set-Cookie: height=180; domain=me.jimu.com
Set-Cookie: weight=80; domain=me.jimu.comCookie: username=jimu; height=180; weight=80cookie,如果服務端創(chuàng)建的 cookie 沒加HttpOnly,那恭喜你也可以修改他給的 cookie。調(diào)用
document.cookie可以創(chuàng)建、修改 cookie,和 HTTP 一樣,一次document.cookie能且只能操作一個 cookie。document.cookie也可以讀到 cookie,也和 HTTP 一樣,能讀到所有的非HttpOnly cookie。console.log(document.cookie);
// username=jimu; height=180; weight=80cookie 后,我們知道 cookie 是最便捷的維持 HTTP 請求狀態(tài)的方式,大多數(shù)前端鑒權問題都是靠 cookie 解決的。當然也可以選用別的存儲方式(后面也會多多少少提到)。應用方案:服務端 session
典型的 session 登陸/驗證流程:

瀏覽器登錄發(fā)送賬號密碼,服務端查用戶庫,校驗用戶
服務端把用戶登錄狀態(tài)存為 Session,生成一個 sessionId
通過登錄接口返回,把 sessionId set 到 cookie 上
此后瀏覽器再請求業(yè)務接口,sessionId 隨 cookie 帶上
服務端查 sessionId 校驗 session
成功后正常做業(yè)務處理,返回結(jié)果
Redis(推薦):內(nèi)存型數(shù)據(jù)庫,redis中文官方網(wǎng)站。以 key-value 的形式存,正合 sessionId-sessionData 的場景;且訪問快。
內(nèi)存:直接放到變量里。一旦服務重啟就沒了
數(shù)據(jù)庫:普通數(shù)據(jù)庫。性能不高。
一是從「存儲」角度,把 session 集中存儲。如果我們用獨立的 Redis 或普通數(shù)據(jù)庫,就可以把 session 都存到一個庫里。
二是從「分布」角度,讓相同 IP 的請求在負載均衡時都打到同一臺機器上。以 nginx 為例,可以配置 ip_hash 來實現(xiàn)。
npm中,已經(jīng)有封裝好的中間件,比如 express-session - npm,用法就不貼了。
封裝了對cookie的讀寫操作,并提供配置項配置字段、加密方式、過期時間等。 封裝了對session的存取操作,并提供配置項配置session存儲方式(內(nèi)存/redis)、存儲規(guī)則等。 給req提供了session屬性,控制屬性的set/get并響應到cookie和session存取上,并給req.session提供了一些方法。
應用方案:token
門衛(wèi)小哥直接對照我和學生證上的臉,確認學生證有效期、年級等信息,就可以放行了。
這種方式通常被叫做 token。

用戶登錄,服務端校驗賬號密碼,獲得用戶信息
把用戶信息、token 配置編碼成 token,通過 cookie set 到瀏覽器
此后用戶請求業(yè)務接口,通過 cookie 攜帶 token
接口校驗 token 有效性,進行正常業(yè)務接口處理
base64
比如 node 端的 cookie-session - npm 庫

eyJ1c2VyaWQiOiJhIn0=,就是 {"userid":"abb”} 的 base64 而已secret: 'iAmSecret',
signed: true,.sig cookie,里面的值就是 {"userid":"abb”} 和 iAmSecret通過加密算法計算出來的,常見的比如HMACSHA256 類 (System.Security.Cryptography) | Microsoft Docs。
eyJ1c2VyaWQiOiJhIn0=,但偽造不出 sig 的內(nèi)容,因為他不知道 secret。cookie 數(shù)量,數(shù)據(jù)本身也沒有規(guī)范的格式,所以 JSON Web Token Introduction - jwt.io 橫空出世了。eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyaWQiOiJhIiwiaWF0IjoxNTUxOTUxOTk4fQ.2jf3kl_uKWRkwjOP6uQRJFqMlwSABcgqqcJofFH5XCo
access token 用來訪問業(yè)務接口,由于有效期足夠短,盜用風險小,也可以使請求方式更寬松靈活
refresh token 用來獲取 access token,有效期可以長一些,通過獨立服務和嚴格的請求方式增加安全性;由于不常驗證,也可以如前面的 session 一樣處理

session 和 token
cookie 是瀏覽器在域下自動攜帶的,這就容易引發(fā) CSRF 攻擊。
存數(shù)據(jù):請求只需攜帶 id,可以大幅縮短認證字符串長度,減小請求體積
不存數(shù)據(jù):不需要服務端整套的解決方案和分布式處理,降低硬件成本;避免查庫帶來的驗證延遲
單點登錄

“真實”的單點登錄(主域名不同)
這要能實現(xiàn)「一次登錄,全線通用」,才是真正的單點登錄。
這種場景下,我們需要獨立的認證服務,通常被稱為 SSO。

用戶進入 A 系統(tǒng),沒有登錄憑證(ticket),A 系統(tǒng)給他跳到 SSO
SSO 沒登錄過,也就沒有 sso 系統(tǒng)下沒有憑證(注意這個和前面 A ticket 是兩回事),輸入賬號密碼登錄
SSO 賬號密碼驗證成功,通過接口返回做兩件事:一是種下 sso 系統(tǒng)下憑證(記錄用戶在 SSO 登錄狀態(tài));二是下發(fā)一個 ticket
客戶端拿到 ticket,保存起來,帶著請求系統(tǒng) A 接口
系統(tǒng) A 校驗 ticket,成功后正常處理業(yè)務請求
此時用戶第一次進入系統(tǒng) B,沒有登錄憑證(ticket),B 系統(tǒng)給他跳到 SSO
SSO 登錄過,系統(tǒng)下有憑證,不用再次登錄,只需要下發(fā) ticket
客戶端拿到 ticket,保存起來,帶著請求系統(tǒng) B 接


在 SSO 域下,SSO 不是通過接口把 ticket 直接返回,而是通過一個帶 code 的 URL 重定向到系統(tǒng) A 的接口上,這個接口通常在 A 向 SSO 注冊時約定瀏覽器被重定向到 A 域下,帶著 code 訪問了 A 的 callback 接口,callback 接口通過 code 換取 ticket
這個 code 不同于 ticket,code 是一次性的,暴露在 URL 中,只為了傳一下?lián)Q ticket,換完就失效
callback 接口拿到 ticket 后,在自己的域下 set cookie 成功
在后續(xù)請求中,只需要把 cookie 中的 ticket 解析出來,去 SSO 驗證就好
訪問 B 系統(tǒng)也是一樣
總結(jié)
HTTP 是無狀態(tài)的,為了維持前后請求,需要前端存儲標記
cookie 是一種完善的標記方式,通過 HTTP 頭或 js 操作,有對應的安全策略,是大多數(shù)狀態(tài)管理方案的基石
session 是一種狀態(tài)管理方案,前端通過 cookie 存儲 id,后端存儲數(shù)據(jù),但后端要處理分布式問題
token 是另一種狀態(tài)管理方案,相比于 session 不需要后端存儲,數(shù)據(jù)全部存在前端,解放后端,釋放靈活性
token 的編碼技術,通?;?base64,或增加加密算法防篡改,jwt 是一種成熟的編碼方案
在復雜系統(tǒng)中,token 可通過 service token、refresh token 的分權,同時滿足安全性和用戶體驗
session 和 token 的對比就是「用不用cookie」和「后端存不存」的對比
單點登錄要求不同域下的系統(tǒng)「一次登錄,全線通用」,通常由獨立的 SSO 系統(tǒng)記錄登錄狀態(tài)、下發(fā) ticket,各業(yè)務系統(tǒng)配合存儲和認證 ticket

