JWT實現token-based會話管理


上文《3種web會話管理的方式》介紹了3種會話管理的方式,其中token-based的方式有必要從實現層面了解一下。本文主要介紹這方面的內容。上文提到token-based的實現目前有一個開放的標准可用,這個標准就是JWT,從它的官網上也能看到,目前實現了JWT的技術非常多,基本上涵蓋了所有的語言平台。本文選擇expressjsonwebtoken基於nodejs來實現token-based會話管理。

相關代碼:https://github.com/liuyunzhuge/blog/tree/master/node_jwt

demo的說明我會在本文第二部分介紹,下面先介紹一下JWT的相關知識。

認識JWT

JSON Web Token(JWT)是一個開放標准(RFC 7519),它定義了一種緊湊和自包含的方式,用於在各方之間作為JSON對象安全地傳輸信息。作為標准,它沒有提供技術實現,但是大部分的語言平台都有按照它規定的內容提供了自己的技術實現,所以實際在用的時候,只要根據自己當前項目的技術平台,到官網上選用合適的實現庫即可。

使用JWT來傳輸數據,實際上傳輸的是一個字符串,這個字符串就是所謂的json web token字符串。所以廣義上,JWT是一個標准的名稱;狹義上,JWT指的就是用來傳遞的那個token字符串。這個串有兩個特點:
1)緊湊:指的是這個串很小,能通過url 參數,http 請求提交的數據以及http header的方式來傳遞;
2)自包含:這個串可以包含很多信息,比如用戶的id、角色等,別人拿到這個串,就能拿到這些關鍵的業務信息,從而避免再通過數據庫查詢等方式才能得到它們。

通常一個JWT是長這個樣子的(這個串本來是不會換行的,為了讓這個串看起來的樣子跟后面要介紹的數據結構對應起來才手工加的換行):

image 

要知道一個JWT是怎么產生以及如何用於會話管理,只要弄清楚JWT的數據結構以及它簽發和驗證的過程即可。

1)JWT的數據結構以及簽發過程

一個JWT實際上是由三個部分組成:header(頭部)、payload(載荷)和signature(簽名)。這三個部分在JWT里面分別對應英文句號分隔出來的三個串:

image

先來看header部分的結構以及它的生成方法。header部分是由下面格式的json結構生成出來:

image

這個json中的typ屬性,用來標識整個token字符串是一個JWT字符串;它的alg屬性,用來說明這個JWT簽發的時候所使用的簽名和摘要算法,常用的值以及對應的算法如下:

image

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編碼之后生成出來的:

image
(在線base64編碼:http://www1.tc711.com/tool/BASE64.htm

再來看payload部分的結構和生成過程。payload部分是由下面類似格式的json結構生成出來:

image

payload的json結構並不像header那么簡單,payload用來承載要傳遞的數據,它的json結構實際上是對JWT要傳遞的數據的一組聲明,這些聲明被JWT標准稱為claims,它的一個“屬性值對”其實就是一個claim,每一個claim的都代表特定的含義和作用。比如上面結構中的sub代表這個token的所有人,存儲的是所有人的ID;name表示這個所有人的名字;admin表示所有人是否管理員的角色。當后面對JWT進行驗證的時候,這些claim都能發揮特定的作用。

根據JWT的標准,這些claims可以分為以下三種類型:
a. Reserved claims(保留),它的含義就像是編程語言的保留字一樣,屬於JWT標准里面規定的一些claim。JWT標准里面定好的claim有:

  • iss(Issuser):代表這個JWT的簽發主體;
  • sub(Subject):代表這個JWT的主體,即它的所有人;
  • aud(Audience):代表這個JWT的接收對象;
  • exp(Expiration time):是一個時間戳,代表這個JWT的過期時間;
  • nbf(Not Before):是一個時間戳,代表這個JWT生效的開始時間,意味着在這個時間之前驗證JWT是會失敗的;
  • iat(Issued at):是一個時間戳,代表這個JWT的簽發時間;
  • jti(JWT ID):是JWT的唯一標識。

b. Public claims,略(不重要)

c. Private claims,這個指的就是自定義的claim。比如前面那個結構舉例中的admin和name都屬於自定的claim。這些claim跟JWT標准規定的claim區別在於:JWT規定的claim,JWT的接收方在拿到JWT之后,都知道怎么對這些標准的claim進行驗證;而private claims不會驗證,除非明確告訴接收方要對這些claim進行驗證以及規則才行。

按照JWT標准的說明:保留的claims都是可選的,在生成payload不強制用上面的那些claim,你可以完全按照自己的想法來定義payload的結構,不過這樣搞根本沒必要:第一是,如果把JWT用於認證, 那么JWT標准內規定的幾個claim就足夠用了,甚至只需要其中一兩個就可以了,假如想往JWT里多存一些用戶業務信息,比如角色和用戶名等,這倒是用自定義的claim來添加;第二是,JWT標准里面針對它自己規定的claim都提供了有詳細的驗證規則描述,每個實現庫都會參照這個描述來提供JWT的驗證實現,所以如果是自定義的claim名稱,那么你用到的實現庫就不會主動去驗證這些claim。

最后也是把這個json結構做base64url編碼之后,就能生成payload部分的串:

image

(在線base64編碼:http://www1.tc711.com/tool/BASE64.htm

最后看signature部分的生成過程。簽名是把header和payload對應的json結構進行base64url編碼之后得到的兩個串用英文句點號拼接起來,然后根據header里面alg指定的簽名算法生成出來的。算法不同,簽名結果不同,但是不同的算法最終要解決的問題是一樣的。以alg: HS256為例來說明前面的簽名如何來得到。按照前面alg可用值的說明,HS256其實包含的是兩種算法:HMAC算法和SHA256算法,前者用於生成摘要,后者用於對摘要進行數字簽名。這兩個算法也可以用HMACSHA256來統稱。運用HMACSHA256實現signature的算法是:

image

正好找到一個在線工具能夠測試這個簽名算法的結果,比如我們拿前面的header和payload串來測試,並把“secret”這個字符串就當成密鑰來測試:

image

https://1024tools.com/hmac

最后的結果B其實就是JWT需要的signature。不過對比我在介紹JWT的開始部分給出的JWT的舉例:

image

會發現通過在線工具生成的header與payload都與這個舉例中的對應部分相同,但是通過在線工具生成的signature與上面圖中的signature有細微區別,在於最后是否有“=”字符。這個區別產生的原因在於上圖中的JWT是通過JWT的實現庫簽發的JWT,這些實現庫最后編碼的時候都用的是base64url編碼,而前面那些在線工具都是bas64編碼,這兩種編碼方式不完全相同,導致編碼結果有區別。

以上就是一個JWT包含的全部內容以及它的簽發過程。接下來看看該如何去驗證一個JWT是否為一個有效的JWT。

2)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”就屬於驗證失敗;
  • aud(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,以實現會話的有效期管理。

以上就是我覺得需要介紹的JWT的各方面的內容,希望大家能看的明白。主要參考的資料有:

https://jwt.io/introduction/

http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html

https://www.iana.org/assignments/jwt/jwt.xml#IESG

接下來看看本文相關demo的內容。

demo要點說明

這個demo分為兩個文件夾,一個api,一個client,分別模擬一個需要登錄認證的服務,以及一個發起登錄認證請求的客戶端:

image

這兩個文件下的內容都是用express框架簡單搭建的,不了解express的話,可以去它官網上看看相關文檔,這兩個文件夾並沒有用太多express的東西,主要滿足demo的需要。

在這兩個文件夾下分別運行node app命令,就能啟動兩個服務:

image

image

然后打開瀏覽輸入http://localhost:2000,就能看到客戶端的服務了:

image

客戶端的頁面提供了三個接口調用的按鈕,作用分別是發起登錄驗證(獲取token),以及登錄驗證后獲取用戶信息(獲取用戶信息),模擬退出(銷毀token)。只有登錄驗證之后,獲取用戶信息的接口才能拿到數據。

客戶端在token-based的認證里面,主要是完成token的保存和發送工作。當token從服務器返回后,我把它直接存放到了localStorage里面:

image

然后當發送請求的時候,我會從localStorage里面拿出來,然后把token以Bearer token的形式加到http Authorization這個header里面:

image

當ajax請求發送的時候,這個token就會跟着request header一起發送到服務端:

image

服務端在token-based認證里面主要的事情有:用戶的驗證、token的簽發、從http中解析出token串、token的驗證、token的刷新等。

由於這是個簡單的demo,所以用戶的驗證,也沒有用數據庫查詢這種級別的方式,直接用用戶名密碼寫死的方式來處理,代碼都在user.js這個模塊里面。

token的簽發和認證,我用的是node-jsonwebtoken這個JWT的實現,它基於nodejs,用起來相對比較簡單,它的github主頁都有詳細的使用說明。

在前面介紹token的簽發和簽名認證的時候,我用的都是HS256的算法,這是考慮這個算法網上有在線工具可用。在demo里面,我用的是RS256的算法,這個算法由於用到RSA算法來加密解密,它是一個非對稱加密的算法。需要一對密鑰才能完成加密和解密。所以我用windows的openssl工具來生成rsa所需要的密鑰對,也就是這兩個文件:

image

這個工具可以從這個地址下載:https://indy.fulgan.com/SSL/
生成的方法可以參考:http://blog.csdn.net/yhl_jxy/article/details/51538332

在簽發token的時候,我會讀取這兩個文件用於JWT的簽發和驗證:

image

整個token的管理我都封裝在authentication.js這個模塊里面。它的邏輯並不復雜,關鍵在於理解node-jsonwebtoken的用法,所以需要花點時間去它主頁上看它使用說明才行。唯一需要補充一點的就是這個模塊內如何從http里面解析出token串:

image

其實也就是拿authorization這個header,然后按照Bearer token的格式進行解析就行了。考慮到token可能通過url傳遞,所以這里面也多加了一個直接從url解析token的處理。

客戶端的主模塊文件app.js沒有要介紹的,服務端的主模塊app.js內容較多,可以把一些要點再說明一下。首先因為token的管理都統一封裝起來了,所以我在服務啟動的時候就初始化了一個Authentication的實例:

image
它提供兩個回調,分別用來從請求中獲取用戶密碼,以及根據用戶密碼完成用戶信息的驗證。

然后我通過CORS(跨域資源共享)的設置來使得客戶端的ajax請求能夠順利地從服務端拿到數據,而不會引發跨域的攔截:

image

細心的話,在客戶端里面,發起獲取用戶信息的請求時,會從network里面看到兩個http請求,其中第一個請求是OPTIONS請求,這個是CORS導致的,如果想了解這個請求產生的具體原因,可以從以下兩篇文章詳細了解CORS的相關介紹:

https://developer.mozilla.org/en-US/docs/Web/HTTP/Server-Side_Access_Control

http://www.ruanyifeng.com/blog/2016/04/cors.html

最后在客戶端對應的請求路由里面,我會繼續用到authorization的實例來完成一些token相關的工作。比如這個登錄的路由:

image

最終通過authorization實例的generateToken方法來完成用戶的登錄信息驗證和token的簽發工作。

這個demo的代碼其實很好理解,我也是從中抽取一些我認為比較關鍵的點拿到博客里來單獨介紹,實際上你要是沒看明白上面的某些內容,完全可以自己把demo弄到本地進行研究,相信那樣會有更好的效果。如果遇到問題或者發現錯誤,歡迎隨時跟我反饋交流。

小結

以上就是整個使用JWT來完成token-based會話管理的方案介紹。它跟我在上文介紹的內容其實有一個差別,就是JWT在傳遞的過程中其實僅僅只做了base64url編碼,而不是加密處理,所以當別人攔截到正常用戶的JWT的時候是很容易解碼看到其中的信息的,尤其是一些重要的業務信息。所以在真正使用的時候,是值得對JWT做一次整體的加密和解密處理的。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM