token 是一串字符串,一般由於做爲鑑權憑據,最經常使用的使用場景是 API 鑑權。php
那麼 API 鑑權通常有幾種方式呢?我大概整理了以下:html
cookie + sessionlaravel
和日常 web 登錄同樣的鑑權方式,很常見,再也不贅述。web
HTTP Basicredis
將帳號和密碼拼接而後 base64 編碼加到 header 頭中。很顯然,由於帳號和密碼幾乎是『明文』傳輸的,並且每次請求都傳,安全性可想而知。算法
HTTP Digest數據庫
將帳號和密碼加上其餘一些信息拼接而後取摘要加到 header 頭中。這個安全性比上面要好一點,由於若是是取摘要的話,即便信息段被截取,也沒法輕易破解出來(固然也是有破解的可能)。json
不過其實最大的問題仍是:每次請求都要對帳號、密碼取一次摘要,也就是說每次請求都要有帳號和密碼,也就是說帳號和密碼要麼緩存一下,要麼就每次請求要去用戶輸一次密碼,這樣顯然不合適。一樣,上面的 Basic 也存在這樣的問題。後端
Token跨域
token 經過一次登陸驗證,獲得一個鑑權字符串,而後之後帶着這個鑑權字符串進行後續操做,這樣就能夠解決每次請求都要帶帳號密碼的問題,並且也不須要反覆使用帳號和密碼。
因此咱們接下來主要探討 token 相對於 Cookie + Session 的認證方式有什麼優點呢?
token 相對於 Cookie + Session 的優勢,主要有下面兩個:
CSRF 攻擊
這個原理很少作介紹,構成這個攻擊的緣由,就在於 Cookie + Session 的鑑權方式中,鑑權數據(cookie 中的 session_id)是由瀏覽器自動攜帶發送到服務端的,藉助這個特性,攻擊者就能夠經過讓用戶誤點攻擊連接,達到攻擊效果。而 token 是經過客戶端自己邏輯做爲動態參數加到請求中的,token 也不會輕易泄露出去,所以 token 在 CSRF 防護方面存在自然優點。
適合移動應用
移動端上不支持 cookie,而 token 只要客戶端可以進行存儲就可以使用,所以 token 在移動端上也具備優點。
通常來講 token 主要三種:
以上,我仔細介紹了 API 經常使用的鑑權方式,以及 token 相對於 cookie + session 的優勢。而後接下來仔細分析 JWT。
JWT 全稱 JSON Web Tokens ,是一種規範化的 token。能夠理解爲對 token 這一技術提出一套規範,是在 RFC 7519中提出的。
一個 JWT token 是一個字符串,它由三部分組成,頭部、載荷與簽名,中間用 .
分隔,例如:xxxxx.yyyyy.zzzzz
頭部一般由兩部分組成:令牌的類型(即JWT)和正在使用的簽名算法(如HMAC SHA256 或 RSA.)。 例如:
{
"alg": "HS256",
"typ": "JWT"
}
複製代碼
而後用 Base64Url
編碼獲得頭部,即 xxxxx
。
載荷中放置了 token
的一些基本信息,以幫助接受它的服務器來理解這個 token
。同時還能夠包含一些自定義的信息,用戶信息交換。
載荷的屬性也分三類:
預約義的載荷
{
"sub": "1",
"iss": "http://localhost:8000/auth/login",
"iat": 1451888119,
"exp": 1454516119,
"nbf": 1451888119,
"jti": "37c107e4609ddbcc9c096ea5ee76c667",
"aud": "dev"
}
複製代碼
這裏面的前 7 個字段都是由官方所定義的,也就是預約義(Registered claims)的,並不都是必需的。
- iss (issuer):簽發人
- sub (subject):主題
- aud (audience):受衆
- exp (expiration time):過時時間
- nbf (Not Before):生效時間,在此以前是無效的
- iat (Issued At):簽發時間
- jti (JWT ID):編號
公有的載荷
在使用 JWT 時能夠額外定義的載荷。爲了不衝突,應該使用 IANA JSON Web Token Registry 中定義好的,或者給額外載荷加上相似命名空間的惟一標識。
私有載荷
在信息交互的雙方之間約定好的,既不是預約義載荷也不是公有載荷的一類載荷。這一類載荷可能會發生衝突,因此應該謹慎使用。
將上面的 json
進行 Base64Url
編碼獲得載荷,,即 yyyyy
。
關於載荷的理解:
這裏三種載荷的定義應該明確的一點是 —— 對於後兩種載荷,它並不是定義了載荷的種類,而後讓你去選用哪一種載荷,而是對你可能會定義出來的載荷作一個分類。
好比你定義了一個
admin
載荷,這個載荷按其分類應該是私有載荷,可能會和其餘人定義的發生衝突。但若是你加了一個前綴(命名空間),如namespace-admin
,那麼這應該就算一個公有載荷了。(但其實標準並無定義怎麼去聲明命名空間,因此嚴格來講,仍是可能會衝突)可是在現實中,團隊都是約定好的了要使用的載荷,這樣的話,好像根本不存在衝突的可能。那爲何文檔要這麼定義呢?個人理解是,RFC 是提出一種技術規範,出發點是一套通用的規範,考慮的範圍是全部開發者,而不只僅侷限於一個開發者團隊。就像用 token 作認證已是很常見的技術了,可是 JWT 的提出就至關於提出了一套較爲通用的技術規範。既然是爲了通用,那麼考慮在大環境下的衝突可能性也是必須的。
簽名時須要用到前面編碼過的兩個字符串,若是以 HMACSHA256
加密,就以下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
複製代碼
加密後再進行 base64url
編碼最後獲得的字符串就是 token
的第三部分 zzzzz
。
組合即可以獲得 token:xxxxx.yyyyy.zzzzz
。
簽名的做用:保證 JWT 沒有被篡改過,原理以下:
HMAC 算法是不可逆算法,相似 MD5 和 hash ,但多一個密鑰,密鑰(即上面的secret)由服務端持有,客戶端把 token 發給服務端後,服務端能夠把其中的頭部和載荷再加上事先共享的 secret 再進行一次 HMAC 加密,獲得的結果和 token 的第三段進行對比,若是同樣則代表數據沒有被篡改。
Hash-based Message Authentication Code
JWT 的使用有兩種方式:
?token=你的token
Authorization:Bearer 你的token
JWT 在客戶端的存儲有三種方式:
可是最推薦的仍是第三種,由於第一二種存在跨域讀取限制,而 Cookie 使用不一樣的跨域策略
由於沒開 HTTPonly,因此要注意防範 XSS 漏洞。
Cookie 的跨域策略
子能夠讀父,可是父不能夠讀子,兄弟之間不能互相訪問。
a.xxx.com 和 b.xxx.com 能夠讀 xxx.com,可是 a.xxx.com 和 b.xxx.com 不能互相讀取,xxx.com 也不能讀 a.xxx.com 和 b.xxx.com 的。
你可能會想:存 Cookie 那我不是又變得和 cookie + session 同樣了嗎?
其實否則,由於存 cookie 在這只是用到了其存儲機制,而沒有利用其去鑑權。也就是說我只是簡單存一下,並無指望瀏覽器帶上去 token 去鑑權,將 token 加入請求這部分操做仍是我手動進行的。
既然 JWT 也是一種 token,那麼它相對於普通的 token 有何優勢呢?
由於 JWT 的有效期徹底與其載荷中編碼的過時時間,服務端不維護任何狀態,所以 JWT 『通常』是『無狀態』的(爲何是通常,後面會仔細說)。無狀態最大的優點在於三點:
由於 JWT 可以在載荷中編碼了部分信息,因此若是把經常使用數據編碼進去的話,可以大大減小數據庫的查詢次數,不過有兩點須要額外注意的:
因此 JWT 就有四個優勢了:
- 防 CSRF
- 適合移動應用
- 無狀態
- 編碼數據
前兩個是 token 的優點,後兩個是 JWT 獨特的優點。
既然主要使用場景是鑑權,那麼安全問題就是不得不考慮的問題了。下面對 JWT 可能須要的安全問題都進行一次深刻的探討並尋求最佳的解決方案。
重放攻擊是經過把原先的包進行一次重放來進行攻擊的手段。須要先明確是的 cookie + session 也是存在重放攻擊的問題的。
經常使用的防範重放攻擊的措施主要有如下幾種:
timestamp
在請求中夾帶一個時間戳,設置較短的有效期,若是一個新來的請求的請求時間超過了請求中的有效期,則認爲無效。可是這種策略也存在問題,即若是一個黑客『眼疾手快』在有效期之內將你的包進行了重放, 那就來攻擊成功。
這種策略對應到 JWT 中就是給 token 設置一個較短的有效期。
nonce
在請求中夾帶一個隨機字符串,這個字符串傳送到客戶端後即存入客戶端的黑名單中,若是一個新來的請求其中存在的隨機字符串已經在黑名單中則認爲無效。可是顯然,這個策略存在巨大的問題:服務端須要維護一個黑名單庫,這個庫的大小會隨着業務運行的時間而變得無比巨大,從而嚴重影響效率。
這種策略對應到 JWT 中就是給 token 設置一個黑名單,可是不設置有效期。
timestamp + nonce
在請求中夾帶一個隨機字符串和一個時間戳,若是一個新來的請求,其隨機字符串已經在黑名單中則認爲無效,或者一個請求的的請求時間超過了其有效期,則也認爲其無效。這樣黑名單的範圍只需設置爲時間戳策略的有效期範圍便可。
這種策略對應到 JWT 中就是給 token 既設置一個黑名單,又設置一個有效期。
挑戰-應答
這個其實和 timestamp + nonce 策略同樣,只是隨機字符串是有服務端生成給客戶端的,客戶端攜帶服務端所給的隨機串來請求。這樣有什麼好處呢?服務端能夠經過一個加密算法來生成這個串,使其和時間戳相關,同時客戶端又沒法僞造。這樣就不須要維護黑名單了。一樣也是時間換空間的策略。可是顯然每次或幾回請求就要進行一次與預請求以獲得隨機串,並非特別方便,形成的額外消耗也有待考量。
序列號
經過在請求中嵌入一個序列號,每次請求依次加一,若是一個請求的序列號早已用過,則認爲無效。可是這個要用邏輯額外一個全局序列號,並非特別方便。
HTTPS
終極解決方案了,HTTPS 在握手過程當中會自動維護一個隱式序列號,解決了上面要本身維護序列號的問題。
注意:以上均沒有討論客戶端主動重放的問題,有興趣的同窗能夠本身研究一下。
由於 token 中包含了登錄狀態,所以一旦 token 被盜,那麼就會被人盜用身份。那麼 token 針對被盜的防範措施整理以下:
除了安全問題,JWT 還有許多其餘須要考慮的問題。
由於 JWT 是無狀態的,因此它的有效期徹底由其自己決定,也就是說服務端沒法讓一個 token 失效。顯然這是一個比較大的問題,對此也有諸多解決方案:
客戶端直接刪除存儲 token 的 cookie
這種方案最爲簡單,操做的結果是不管客戶端仍是服務端都沒有這個 token,可問題是,這個 token 並無真正不可以使用,而是處於一個遊離態。
黑名單策略
客戶端攜帶要註銷的 token 訪問一個註銷接口,服務端把 token 加入一個黑名單。
此策略是否會出現黑名單過大的問題?
答案是不會,由於黑名單隻需維護自己沒有過時但又要使其無效的 token,過時的 token 就能夠不用存在黑名單了。
把 token 和 uuid 用 key-value 對存儲在 redis
這種方案看上去沒問題,可是實際上,至關於本身實現了一次 cookie + session,JWT 就失去了『無狀態』這一特性,從也會失去『無狀態』特性帶來的一系列的優勢。
讓每一個用戶都有一個 secret
前面講到簽發 token 的時候用到了 secret ,這種策略的思想就是讓每一個用戶都有一個 secret,註銷一個用戶的時候修改其 secret,便可使其前面簽發的 token 沒法經過校驗而失效。
這種策略上聽上去不須要維護一個狀態,可是實際上存在更大的問題。試想一下,第一種方案是經過 uuid 在已登陸用戶的 token 表中找到要註銷的 token 註銷。cookie + session 是經過 session_id 在已登陸的用戶的 session 表中找到其對應的 session 並刪除來註銷。而此方案是經過 uuid 在全部用戶(而非已登陸用戶)中找到對於的 secret 修改來註銷。這樣看來會發現效率更低,由於查找範圍更大了。
預黑名單
把要註銷的用戶的 uuid 和當前時間(TIME) 組成 key-value 對加入預黑名單,下次請求來時,若其 uuid 和黑名單中的對應,而且簽發時間在 TIME 以前,則將其註銷。這樣查找範圍就是未過時但又要註銷的用戶。而且在實現邏輯上這個預黑名單能夠和簽名的黑名單作到一塊兒。
關於黑名單策略的補充:
有人可能會以爲黑名單也是一種狀態,用這種策略實習的 JWT 並不能算純正的無狀態。這種說法沒錯,可是考慮每次要檢索的數據範圍能夠獲得下面一個關係:
未過時但要提早註銷的用戶或 token數 < 全部已登陸用戶數 < 全部用戶數
此處的『 < 』基本能夠當作『遠遠小於』,因此黑名單策略雖然也算有狀態,可是其維護的狀態數也是特別小的。
可見 『黑名單』策略可以有效解決 JWT 的註銷問題。
session 能夠自動續簽,那 token 如何實現自動續簽呢?咱們先仔細分析一下在 web 和 app 環境中,token 分別如何續簽。先具體分析 web 續簽和 app 續簽分別是什麼樣的具體需求。
web
超過一段時間沒有請求,須要從新登陸,這個時間通常設置爲 1-2 小時
app
超過一段較長的時間沒有請求,須要從新登陸,這個時間通常爲 15-30 天
那這個需求能夠如何實現呢?
web
假設一個 token 的簽發時間爲 12:00,需求爲 2h 未進行請求就要從新登陸。則過時時間爲 1h,刷新時間爲 3h。
那麼在 12:00 - 13:00 其都是能夠正常使用的,若是在 13:00 - 15:00 進行請求,服務端自動換一個新 token 給客戶端,達成續簽。
若是 13:00 -15:00 之間沒有進行請求,而是在 15:00 以後進行的請求,那麼判斷過時,需從新登陸。
這樣的話,最終的實現效果是:token 過時 2h 後須要從新登陸 ,而不是 token 2h 未使用須要從新登陸,致使的結果是,用戶是 2 - 3h 未進行請求,須要從新登陸。比設定的需求要多一個小時的不肯定時間,但這也是沒辦法的辦法了,至於會不會對業務形成影響,看具體需求吧,大多數的狀況仍是不會的。
app
和 web 端相似,設置成更長的時間週期便可。
對使用 Laravel 開發並使用 tymon/jwt-auth 這個插件的開發者,有個必需要注意的地方。
此處進行 token 的刷新並非經過 refresh
這個操做得到新 token,由於這樣 token 在不斷的刷新過程當中會達到一個刷新時間的上限。而上面的邏輯是每次都新簽發一個 token,只要不斷籤就可以一直使用下去。 而後這裏的舊 token 放入黑名單,黑名單有效期設置爲『刷新時間』—— 3h。
固然若是開發者以爲這樣不斷籤就可以一直使用不太好,那就能夠設置更長的刷新時間,用 refresh
操做來獲取新 token,刷新時間保證每次登錄獲得 token 後,即便每次及時續簽,最終也不會超過刷新時間。
而後這裏又會出現一個新坑:
若是刷新時間設置爲 14 天,過時時間設置爲 2h。
token A 在 『 <= 14天 』時刷新獲得 token B,此時若再拿 token A 去請求刷新,確定是不容許,不然 token 會出現『 1變 N 』的問題,因此顯然必須設置一個黑名單去放這些已過時可是又已經刷新過的 token。而這個黑名單的有效期範圍應當爲 token 的刷新期,即 14 天。而後你會發現對於每一個用戶每次登錄,須要維護的黑名單 token 數目最大可達 14 * 24 / 2 = 168 個,黑名單變得很大。
因此,若是要使用
refresh
操做,刷新時間務必是過時時間的儘可能小的倍數。
web
假設一個 token 的簽發時間爲 12:00,需求爲 2h 未進行請求即過時。則設置有效期 2h,不須要設置刷新期。那麼每次請求都會把一個 token 換成一個新 token。若是 2h 沒有進行請求,那麼上一次請求的到的 token 就會過時,須要從新登陸。一樣是不斷籤就能一直使用下去。
若是想要和上面同樣,不但願永久續簽,則設置一個刷新時間便可。這個刷新時間不會致使進一步膨脹。
app
和 web 端相似,設置更長時間便可。
而後又到了問題時間:
每次都刷新 token,帶來的性能影響如何?
之前每次請求,須要進行一次 token 簽名校驗,而如今是要簽發一個新 token,進行的都是一次簽名運算,那麼運算量即從 n 變成 2n。
其次,每次刷新都要把舊 token 加入黑名單,會致使黑名單特別大,遠遠比方式一的設置刷新期大。
每次都刷新 token,併發請求時會不會由於 token 刷新而致使只有一個請求成功?
答案是確實會致使這個問題,怎麼解決呢?設置一個寬限時間,每次 token 刷新後,原來邏輯應該是馬上不可用,如今設置一個寬限時間,讓其在 n 秒以內仍然可用便可。
總之,這種策略會致使花費的 CPU 運算翻倍,並致使巨大的黑名單,而後必須設置一個寬限時間以解決併發請求問題,至於寬限時間會不會帶來安全問題,微乎其微吧。
上面講到,對於方式一【限定不能一直續簽】,會致使巨大的黑名單,對於方式二,總會致使一個更加巨大的黑名單。那有沒有解決方案呢?固然是有的。
咱們能夠這麼想,既然一個 token 進行了刷新,那麼簽發時間在此次刷新以前的便可認爲無效。因而,和上面的『預黑名單』策略相似,我刷新時不是把一個 token 加入黑名單,而是把 uuid-refresh_time 組成 key-vakue 對加入黑名單,這樣針對每一個用戶的每次登錄,要存儲到黑名單中的條目數就從 N 個變成了一個。
可是這樣還要考慮一個問題:就是一個用戶開兩個瀏覽器,在不一樣的時刻在同一個系統都登錄了(假設業務容許),那麼一個瀏覽器的 token 刷新就可能會致使另外一個瀏覽器登錄失效。因此存儲在黑名單中的 key-value 應該再加一個 key 以表明每次登錄,而且這個 key 要在 JWT 的載荷中隨着刷新一直傳承。
基於以上的優化,黑名單的大小變成了:每一個用戶同時登錄的系統個數之和,就變的和 cookie + session 同樣了。
好比,A 系統(假設 2h 過時時間,14天刷新時間),你用一個瀏覽器登錄了你的帳號,我用 Chrome 瀏覽器登錄了個人帳號,而後我又用 QQ 瀏覽器再登錄個人帳號,那麼黑名單的大小就爲 : 1 + 2 = 3
而對於方式一【限定不能一直續簽】,黑名單的大小(最大):168 + 168 * 2
而對於方式二,黑名單的大小爲:你在 2h 內請求的次數 x ,我在 Chrome 瀏覽器請求的次數 y,我在 QQ 瀏覽器請求的次數 z 之和,即:x + y + z
若是要解決續簽問題,方式一【能夠一直續簽】是個比較好的解決方案,雖然會帶來一點小問題,可是並不會有太大的影響。方式二【限定不能一直續簽】和 每次刷新會讓黑名單的維護量和有狀態差很少,可是有更高的安全性。
咱們先列舉每次刷新 token 的優缺點:
優勢:
缺點:
上面討論過,『續簽』和『重放』均可以經過其餘方式解決。只有『更安全』算半個痛點,爲何是半個痛點呢?由於若是採用 HTTPS 的話,那麼盜取 token 的手段就只要如下幾種辦法:
只有第三種方法存在一點可能性。
因此,要不要每次刷新,仍是根據各位的具體業務狀況進行選擇吧。
這個顯然很適合。
單點登陸必需要實現的:
可見,對 JWT 部署一些額外邏輯(黑名單,續簽管理)便可讓 JWT 在大部分場景代替 cookie + session。
Oauth 2.0 是幹嗎的再也不贅述,它與 JWT 其實並非一個層面的東西。Oauth2.0 是一個方便的第三方受權規範,而 JWT 是一個 token 結構規範。只是 JWT 經常使用來登錄鑑權,而 Oauth2.0 在受權時也涉及到了登錄,因此就比較容易搞混。
可是在此,我要說的是,Oauth 2.0 其實能夠和 JWT 結合使用。
如下是一個常見的 Oauth2.0 登錄返回:
{
"access_token":"kag2geh11a3eh56e23hj",
"expires_in":7200,
"refresh_token":"jgko97cq4c8wn69j",
"scope":"SCOPE"
}
複製代碼
在 Oauth2.0 中,access_token
用來進行數據請求,而 refresh_token
用來刷新 access_token
。每次刷新,上一個 access_token 就會失效,而 access_token
和 refresh_token
顯然都沒有記錄任何狀態,因此必須爲服務端進行狀態的維護。
把 JWT 和 Oauth2.0 結合後,能夠獲得這樣的返回:
{
"access_token":"xxx.yyy.zzz",
"expires_in":7200,
"refresh_token":"xxxxx.yyyyy.zzzzz",
"scope":"SCOPE"
}
複製代碼
進行結合後有以下優點:
refresh_token
的機制,讓黑名單庫中 token 的有效期從 『刷新時間』又變回『過時時間』,從而解決了這個問題。這是我從 Auth0 組織的這篇文章 10 Things You Should Know about Tokens 整理過來的:
Token 獲取到後須要保存起來以便下次使用,能夠選擇存儲在 localstorage / sessionstorage / cookie
Token 是包含有效期的,你必須部署一些邏輯來進行有效期的控制
localstorage / sessionstorage 的跨域限制較 cookie 更爲嚴格,推薦使用 cookie
在你進行異步請求時,瀏覽器通常都會發送預檢請求(option),後端應對此部署相應的邏輯
使用 cookie 能夠輕鬆處理一個文件下載請求,可是 token 通常都是經過 XHR 方式進行請求的,因此你必須部署額外的邏輯。好比生成一個實時 ticket ,以 ticket 進行訪問,而後校驗,重定向,最後下載文件。
處理 XSS 比處理 CSRF 更容易(這一點我實在沒看到他是什麼個邏輯,你們能夠去看看原文)
token 在每次請求時都會被編碼到請求中,因此請注意 token 的大小,不要編碼過多數據
若是在 token 中編碼敏感信息,請對 token 進行加密
JSON Web Token 能夠用於 Oauth2.0 的 Bearer Token 中,賦予 Oauth2.0 無狀態的優點
Token 不是銀彈,請根據實際業務須要進行選擇
轉載 JWT 超詳細分析