上文《3種web會話管理的方式》介紹了3種會話管理的方式,其中token-based的方式有必要從實現層面瞭解一下。本文主要介紹這方面的內容。上文提到token-based的實現目前有一個開放的標準可用,這個標準就是JWT,從它的官網上也能看到,目前實現了JWT的技術很是多,基本上涵蓋了全部的語言平臺。本文選擇express和jsonwebtoken基於nodejs來實現token-based會話管理。html
相關代碼:https://github.com/liuyunzhuge/blog/tree/master/node_jwtnode
demo的說明我會在本文第二部分介紹,下面先介紹一下JWT的相關知識。git
JSON Web Token(JWT)是一個開放標準(RFC 7519),它定義了一種緊湊和自包含的方式,用於在各方之間做爲JSON對象安全地傳輸信息。做爲標準,它沒有提供技術實現,可是大部分的語言平臺都有按照它規定的內容提供了本身的技術實現,因此實際在用的時候,只要根據本身當前項目的技術平臺,到官網上選用合適的實現庫便可。github
使用JWT來傳輸數據,實際上傳輸的是一個字符串,這個字符串就是所謂的json web token字符串。因此廣義上,JWT是一個標準的名稱;狹義上,JWT指的就是用來傳遞的那個token字符串。這個串有兩個特色:
1)緊湊:指的是這個串很小,能經過url 參數,http 請求提交的數據以及http header的方式來傳遞;
2)自包含:這個串能夠包含不少信息,好比用戶的id、角色等,別人拿到這個串,就能拿到這些關鍵的業務信息,從而避免再經過數據庫查詢等方式才能獲得它們。web
一般一個JWT是長這個樣子的(這個串原本是不會換行的,爲了讓這個串看起來的樣子跟後面要介紹的數據結構對應起來才手工加的換行):ajax
要知道一個JWT是怎麼產生以及如何用於會話管理,只要弄清楚JWT的數據結構以及它簽發和驗證的過程便可。算法
1)JWT的數據結構以及簽發過程數據庫
一個JWT其實是由三個部分組成:header(頭部)、payload(載荷)和signature(簽名)。這三個部分在JWT裏面分別對應英文句號分隔出來的三個串:express
先來看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的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有:
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部分的串:
(在線base64編碼:http://www1.tc711.com/tool/BASE64.htm)
最後看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。
2)JWT的驗證過程
這個部分介紹JWT的驗證規則,主要包括簽名驗證和payload裏面各個標準claim的驗證邏輯介紹。只有驗證成功的JWT,才能當作有效的憑證來使用。
先說簽名驗證。當接收方接收到一個JWT的時候,首先要對這個JWT的完整性進行驗證,這個就是簽名認證。它驗證的方法其實很簡單,只要把header作base64url解碼,就能知道JWT用的什麼算法作的簽名,而後用這個算法,再次用一樣的邏輯對header和payload作一次簽名,並比較這個簽名是否與JWT自己包含的第三個部分的串是否徹底相同,只要不一樣,就能夠認爲這個JWT是一個被篡改過的串,天然就屬於驗證失敗了。接收方生成簽名的時候必須使用跟JWT發送方相同的密鑰,意味着要作好密鑰的安全傳遞或共享。
再來看payload的claim驗證,拿前面標準的claim來一一說明:
須要注意的是,在驗證一個JWT的時候,簽名認證是每一個實現庫都會自動作的,可是payload的認證是由使用者來決定的。由於JWT裏面可能不會包含任何一個標準的claim,因此它不會自動去驗證這些claim。
以登陸認證來講,在簽發JWT的時候,徹底能夠只用sub跟exp兩個claim,用sub存儲用戶的id,用exp存儲它本次登陸以後的過時時間,而後在驗證的時候僅驗證exp這個claim,以實現會話的有效期管理。
以上就是我以爲須要介紹的JWT的各方面的內容,但願你們能看的明白。主要參考的資料有:
http://self-issued.info/docs/draft-ietf-oauth-json-web-token.html
https://www.iana.org/assignments/jwt/jwt.xml#IESG
接下來看看本文相關demo的內容。
這個demo分爲兩個文件夾,一個api,一個client,分別模擬一個須要登陸認證的服務,以及一個發起登陸認證請求的客戶端:
這兩個文件下的內容都是用express框架簡單搭建的,不瞭解express的話,能夠去它官網上看看相關文檔,這兩個文件夾並無用太多express的東西,主要知足demo的須要。
在這兩個文件夾下分別運行node app命令,就能啓動兩個服務:
而後打開瀏覽輸入http://localhost:2000,就能看到客戶端的服務了:
客戶端的頁面提供了三個接口調用的按鈕,做用分別是發起登陸驗證(獲取token),以及登陸驗證後獲取用戶信息(獲取用戶信息),模擬退出(銷燬token)。只有登陸驗證以後,獲取用戶信息的接口才能拿到數據。
客戶端在token-based的認證裏面,主要是完成token的保存和發送工做。當token從服務器返回後,我把它直接存放到了localStorage裏面:
而後當發送請求的時候,我會從localStorage裏面拿出來,而後把token以Bearer token的形式加到http Authorization這個header裏面:
當ajax請求發送的時候,這個token就會跟着request header一塊兒發送到服務端:
服務端在token-based認證裏面主要的事情有:用戶的驗證、token的簽發、從http中解析出token串、token的驗證、token的刷新等。
因爲這是個簡單的demo,因此用戶的驗證,也沒有用數據庫查詢這種級別的方式,直接用用戶名密碼寫死的方式來處理,代碼都在user.js這個模塊裏面。
token的簽發和認證,我用的是node-jsonwebtoken這個JWT的實現,它基於nodejs,用起來相對比較簡單,它的github主頁都有詳細的使用說明。
在前面介紹token的簽發和簽名認證的時候,我用的都是HS256的算法,這是考慮這個算法網上有在線工具可用。在demo裏面,我用的是RS256的算法,這個算法因爲用到RSA算法來加密解密,它是一個非對稱加密的算法。須要一對密鑰才能完成加密和解密。因此我用windows的openssl工具來生成rsa所須要的密鑰對,也就是這兩個文件:
這個工具能夠從這個地址下載:https://indy.fulgan.com/SSL/
生成的方法能夠參考:http://blog.csdn.net/yhl_jxy/article/details/51538332
在簽發token的時候,我會讀取這兩個文件用於JWT的簽發和驗證:
整個token的管理我都封裝在authentication.js這個模塊裏面。它的邏輯並不複雜,關鍵在於理解node-jsonwebtoken的用法,因此須要花點時間去它主頁上看它使用說明才行。惟一須要補充一點的就是這個模塊內如何從http裏面解析出token串:
其實也就是拿authorization這個header,而後按照Bearer token的格式進行解析就好了。考慮到token可能經過url傳遞,因此這裏面也多加了一個直接從url解析token的處理。
客戶端的主模塊文件app.js沒有要介紹的,服務端的主模塊app.js內容較多,能夠把一些要點再說明一下。首先由於token的管理都統一封裝起來了,因此我在服務啓動的時候就初始化了一個Authentication的實例:
它提供兩個回調,分別用來從請求中獲取用戶密碼,以及根據用戶密碼完成用戶信息的驗證。
而後我經過CORS(跨域資源共享)的設置來使得客戶端的ajax請求可以順利地從服務端拿到數據,而不會引起跨域的攔截:
細心的話,在客戶端裏面,發起獲取用戶信息的請求時,會從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相關的工做。好比這個登陸的路由:
最終經過authorization實例的generateToken方法來完成用戶的登陸信息驗證和token的簽發工做。
這個demo的代碼其實很好理解,我也是從中抽取一些我認爲比較關鍵的點拿到博客裏來單獨介紹,實際上你要是沒看明白上面的某些內容,徹底能夠本身把demo弄到本地進行研究,相信那樣會有更好的效果。若是遇到問題或者發現錯誤,歡迎隨時跟我反饋交流。
以上就是整個使用JWT來完成token-based會話管理的方案介紹。它跟我在上文介紹的內容其實有一個差異,就是JWT在傳遞的過程當中其實僅僅只作了base64url編碼,而不是加密處理,因此當別人攔截到正經常使用戶的JWT的時候是很容易解碼看到其中的信息的,尤爲是一些重要的業務信息。因此在真正使用的時候,是值得對JWT作一次總體的加密和解密處理的。