JWT 介紹 ( https://jwt.io/ )
JSON Web Token(JWT)
是一個開放标準(RFC 7519),它定義了一種緊湊和自包含的方式,用于在各方之間作為 JSON 對象安全地傳輸資訊。作為标準,它沒有提供技術實作,但是大部分的語言平台都有按照它規定的内容提供了自己的技術實作,是以實際在用的時候,隻要根據自己目前項目的技術平台,到官網上選用合适的實作庫即可。
使用
JWT
來傳輸資料,實際上傳輸的是一個字元串,這個字元串就是所謂的 json web token 字元串。是以廣義上,
JWT
是一個标準的名稱;狹義上,
JWT
指的就是用來傳遞的那個
token
字元串。這個串有兩個特點:
- 緊湊:指的是這個串很小,能通過 url 參數,http 請求送出的資料以及 http header 的方式來傳遞;
- 自包含:這個串可以包含很多資訊,比如使用者的 id、角色等,别人拿到這個串,就能拿到這些關鍵的業務資訊,進而避免再通過資料庫查詢等方式才能得到它們。
通常一個
JWT
是長這個樣子的:

要知道一個
JWT
是怎麼産生以及如何用于會話管理,隻要弄清楚
JWT
的資料結構以及它簽發和驗證的過程即可。
https://bestqliang.com/2018/06/02/%E4%BD%BF%E7%94%A8jwt%E5%AE%8C%E6%88%90sso%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95/#%E4%B8%80-JWT%E7%9A%84%E6%95%B0%E6%8D%AE%E7%BB%93%E6%9E%84%E4%BB%A5%E5%8F%8A%E7%AD%BE%E5%8F%91%E8%BF%87%E7%A8%8B 一. JWT
的資料結構以及簽發過程
JWT
一個
JWT
實際上是由三個部分組成:
header(頭部)
、
payload(載荷)
和
signature(簽名
)。這三個部分在
JWT
裡面分别對應英文句号分隔出來的三個串:
先來看
header
部分的結構以及它的生成方法。
header
部分是由下面格式的 json 結構生成出來:
這個 json 中的
typ
屬性,用來辨別整個
token
字元串是一個
JWT
字元串;它的
alg
屬性,用來說明這個
JWT
簽發的時候所使用的簽名和摘要算法,常用的值以及對應的算法如下:
typ跟alg屬性的全稱其實是type跟algorithm,分别是類型跟算法的意思。之是以都用三個字母來表示,也是基于JWT最終字串大小的考慮,同時也是跟JWT這個名稱保持一緻,這樣就都是三個字元了…typ跟alg是JWT中标準中規定的屬性名稱,雖然在簽發JWT的時候,也可以把這兩個名稱換掉,但是如果随意更換了這個名稱,就有可能在JWT驗證的時候碰到問題,因為拿到JWT的人,預設會根據typ和alg去拿JWT中的header資訊,當你改了名稱之後,顯然别人是拿不到header資訊的,他又不知道你把這兩個名字換成了什麼。JWT作為标準的意義在于統一各方對同一個事情的處理方式,各個使用方都按它約定好的格式和方法來簽發和驗證token,這樣即使運作的平台不一樣,也能夠保證token進行正确的傳遞。
一般簽發JWT的時候,header對應的 json 結構隻需要typ和alg屬性就夠了。JWT的header部分是把前面的 json 結構,經過 Base64Url 編碼之後生成出來的:
(線上 base64 編碼:
http://www1.tc711.com/tool/BASE64.htm)再來看
payload
部分的結構和生成過程。
payload
部分是由下面類似格式的 json 結構生成出來:
payload
payload
最後看
signature
部分的生成過程。簽名是把
header
payload
對應的 json 結構進行 base64url 編碼之後得到的兩個串用英文句點号拼接起來,然後根據
header
裡面
alg
指定的簽名算法生成出來的。算法不同,簽名結果不同,但是不同的算法最終要解決的問題是一樣的。以
alg: HS256
為例來說明前面的簽名如何來得到。按照前面
alg
可用值的說明,HS256 其實包含的是兩種算法:HMAC 算法和 SHA256 算法,前者用于生成摘要,後者用于對摘要進行數字簽名。這兩個算法也可以用 HMACSHA256 來統稱。運用 HMACSHA256 實作
signature
的算法是:
正好找到一個線上工具能夠測試這個簽名算法的結果,比如我們拿前面的header和payload串來測試,并把“secret”這個字元串就當成密鑰來測試:
最後的結果 B 其實就是 JWT 需要的 signature。不過對比我在介紹 JWT 的開始部分給出的 JWT 的舉例:
會發現通過線上工具生成的
header
與
payload
都與這個舉例中的對應部分相同,但是通過線上工具生成的
signature
與上面圖中
的signature
有細微差別,在于最後是否有“=”字元。這個差別産生的原因在于上圖中的
JWT
是通過
JWT
的實作庫簽發的
JWT
,這些實作庫最後編碼的時候都用的是 base64url 編碼,而前面那些線上工具都是 bas64 編碼,這兩種編碼方式不完全相同,導緻編碼結果有差別。
以上就是一個
JWT
包含的全部内容以及它的簽發過程。接下來看看該如何去驗證一個
JWT
是否為一個有效的
JWT
。
https://bestqliang.com/2018/06/02/%E4%BD%BF%E7%94%A8jwt%E5%AE%8C%E6%88%90sso%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95/#%E4%BA%8C-JWT%E7%9A%84%E9%AA%8C%E8%AF%81%E8%BF%87%E7%A8%8B 二. JWT
的驗證過程
JWT
這個部分介紹
JWT
的驗證規則,主要包括簽名驗證和
payload
裡面各個标準
claim
的驗證邏輯介紹。隻有驗證成功的
JWT
,才能當做有效的憑證來使用。
先說簽名驗證。當接收方接收到一個
JWT
的時候,首先要對這個
JWT
的完整性進行驗證,這個就是簽名認證。它驗證的方法其實很簡單,隻要把
header
做 base64url 解碼,就能知道
JWT
用的什麼算法做的簽名,然後用這個算法,再次用同樣的邏輯對
header
payload
做一次簽名,并比較這個簽名是否與
JWT
本身包含的第三個部分的串是否完全相同,隻要不同,就可以認為這個
JWT
是一個被篡改過的串,自然就屬于驗證失敗了。接收方生成簽名的時候必須使用跟
JWT
發送方相同的密鑰,意味着要做好密鑰的安全傳遞或共享。
payload
的
claim
驗證,拿前面标準的
claim
來一一說明:
iss(Issuser)
:如果簽發的時候這個
claim
的值是“a.com”,驗證的時候如果這個
claim
的值不是“a.com”就屬于驗證失敗;
sub(Subject)
claim
的值是“liuyunzhuge”,驗證的時候如果這個
claim
的值不是“liuyunzhuge”就屬于驗證失敗;
(Audience)
claim
的值是“[‘b.com’,’c.com’]”,驗證的時候這個
claim
的值至少要包含 b.com,c.com 的其中一個才能驗證通過;
exp(Expiration time)
:如果驗證的時候超過了這個
claim
指定的時間,就屬于驗證失敗;
nbf(Not Before)
:如果驗證的時候小于這個
claim
iat(Issued at)
:它可以用來做一些 maxAge 之類的驗證,假如驗證時間與這個
claim
指定的時間相差的時間大于通過 maxAge 指定的一個值,就屬于驗證失敗;
jti(JWT ID)
claim
的值是“1”,驗證的時候如果這個
claim
的值不是“1”就屬于驗證失敗;
需要注意的是,在驗證一個
JWT
的時候,簽名認證是每個實作庫都會自動做的,但是
payload
的認證是由使用者來決定的。因為
JWT
裡面可能不會包含任何一個标準的
claim
,是以它不會自動去驗證這些
claim
以登入認證來說,在簽發
JWT
的時候,完全可以隻用
sub
跟
exp
兩個
claim
,用
sub
存儲使用者的
id
exp
存儲它本次登入之後的過期時間,然後在驗證的時候僅驗證
exp
這個
claim
,以實作會話的有效期管理。
https://bestqliang.com/2018/06/02/%E4%BD%BF%E7%94%A8jwt%E5%AE%8C%E6%88%90sso%E5%8D%95%E7%82%B9%E7%99%BB%E5%BD%95/#JWT-SSO JWT SSO
場景一:使用者發起對業務系統的第一次通路,假設他第一次通路的是系統 A 的 some/page 這個頁面,它最終成功通路到這個頁面的過程是:
在這個過程裡面,我認為了解的關鍵點在于:
它用到了兩個cookie(jwt和sid)和三次重定向來完成會話的建立和會話的傳遞;
jwt的cookie是寫在 systemA.com 這個域下的,是以每次重定向到 systemA.com 的時候,jwt這個cookie隻要有就會帶過去;
sid的cookie是寫在 cas.com 這個域下的,是以每次重定向到 cas.com 的時候,sid這個cookie隻要有就會帶過去;
在驗證jwt的時候,如何知道目前使用者已經建立了 sso 的會話?
因為jwt的payload裡面存儲了之前建立的 sso 會話的sessionid,是以當 cas 拿到jwt,就相當于拿到了sessionid,然後用這個sessionid去判斷有沒有的對應的session對象即可。
還要注意的是:CAS 服務裡面的session屬于服務端建立的對象,是以要考慮sessionid唯一性以及session共享(假如 CAS 采用叢集部署的話)的問題。sessionid的唯一性可以通過使用者名密碼加随機數然後用 hash 算法如 md5 簡單處理;session共享,可以用memcached或者redis這種專門的支援叢集部署的緩存伺服器管理session來處理。
由于服務端session具有生命周期的特點,到期需自動銷毀,是以不要自己去寫session的管理,免得引發其它問題,到 github 裡找開源的緩存管理中間件來處理即可。存儲session對象的時候,隻要用sessionid作為 key,session對象本身作為value,存入緩存即可。session對象裡面除了sessionid,還可以存放登入之後擷取的使用者資訊等業務資料,友善業務系統調用的時候,從session裡面傳回會話資料。
場景二:使用者登入之後,繼續通路系統 A 的其它頁面,如 some/page2,它的處理過程是:
從這一步可以看出,即使登入之後,也要每次跟 CAS 校驗jwt的有效性以及會話的有效性,其實jwt的有效性也可以放在業務系統裡面處理的,但是會話的有效性就必須到 CAS 那邊才能完成了。當 CAS 拿到jwt裡面的sessionid之後,就能到session緩存伺服器裡面去驗證該sessionid對應的session對象是否存在,不存在,就說明會話已經銷毀了(退出)。
場景三:使用者登入了系統 A 之後,再去通路其他系統如系統 B 的資源,比如系統 B 的 some/page,它最終能通路到系統 B 的 some/page 的流程是:
這個過程的關鍵在于第一次重定向的時候,它會把sid這個cookie帶回給 CAS 伺服器,是以 CAS 伺服器能夠判斷出會話是否已經建立,如果已經建立就跳過登入頁的邏輯。
場景四:使用者繼續通路系統 B 的其它資源,如系統 B 的 some/page2:
這個場景的邏輯跟場景二完全一緻。
場景五:登出,假如它從系統 B 發起退出,最終的流程是:
最重要的是要清除sid的cookie,jwt的cookie可能業務系統都有建立,是以不可能在退出的時候還挨個去清除那些系統的cookie,隻要sid一清除,那麼即使那些jwt的cookie在下次通路的時候還會被傳遞到業務系統的服務端,由于jwt裡面的sid已經無效,是以最後還是會被重定向到 CAS 登入頁進行處理。
方案總結
以上方案兩個關鍵的前提:
整個會話管理其實還是基于服務端的session來做的,隻不過這個session隻存在于 CAS 服務裡面;
CAS 之是以信任業務系統的jwt,是因為這個jwt是 CAS 簽發的,理論上隻要認證通過,就可以認為這個jwt是合法的。
jwt本身是不可僞造,不可篡改的,但是不代表非法使用者冒充正常用法發起請求,是以正常的幾個安全政策在實際項目中都應該使用:
使用 https
使用 http-only 的cookie,針對sid和jwt
管理好密鑰
防範 CSRF 攻擊。
尤其是 CSRF 攻擊形式,很多都是鑽代碼的漏洞發生的,是以一旦出現 CSRF 漏洞,并且被人利用,那麼别人就能用獲得的jwt,冒充正常使用者通路所有業務系統,這個安全問題的後果還是很嚴重的。考慮到這一點,為了在即使有漏洞的情況将損害減至最小,可以在jwt裡面加入一個系統辨別,添加一個驗證,隻有傳過來的jwt内的系統辨別與發起jwt驗證請求的服務一緻的情況下,才允許驗證通過。這樣的話,一個非法使用者拿到某個系統的jwt,就不能用來通路其它業務系統了。
在業務系統跟 CAS 發起 attach/validate 請求的時候,也可以在 CAS 端做些處理,因為這個請求,在一次 SSO 過程中,一個系統隻應該發一次,是以隻要之前已經給這個系統簽發過 jwt 了,那麼後續 同一系統的 attach/validate 請求都可以忽略掉。
總的來說,這個方案的好處有:
完全分布式,跨平台,CAS 以及業務系統均可采用不同的語言來開發;
業務系統如系統 A 和系統 B,可實作服務端無狀态
假如是自己來實作,那麼可以輕易的在 CAS 裡面內建使用者注冊服務以及第三方登入服務,如微信登入等。
它的缺陷是:
第一次登入某個系統,需要三次重定向;
登入後的後續請求,每次都需要跟 CAS 進行會話驗證,是以 CAS 的性能負載會比較大
登陸後的後續請求,每次都跟 CAS 互動,也會增加請求響應時間,影響使用者體驗。