一:理解單系統登入的原理及實現?
web應用採用的 browser/server 架構的,http是無狀態協議的,也就是說使用者從A頁面跳轉到B頁面會發起http請求,當伺服器返回響應後,當使用者A繼續訪問其他頁面的時候,伺服器端無法獲知該狀態,因此會使用cookie/session來記錄使用者狀態的。
session認證狀態的基本原理:當客戶端向伺服器端請求時,會建立一個session標識存在客戶端的cookie當中,每次請求的時候會將該標識隨cookie一起傳送到伺服器端,伺服器端會首先檢查這個客戶端的請求裡面是否包含了一個session的標識,如果已經包含了,那麼伺服器端就會根據該session標識來判斷使用者的狀態,否則的話,伺服器端會建立一個新的session標識傳給客戶端的cookie當中,以後每次客戶端請求的時候,會從cookie中獲取該標識傳遞過去。
二:理解單點登入的原理?
上面使用session/cookie 可以實現單個系統的登入,那如果是多個系統的話,怎麼辦?難道需要使用者一個個去登入? 一個個去登出?我們需要做的是,無論系統有多少個,我們只需要登入一次就夠了,其他的相關的系統都可以登入/登出一次即可。
單系統登入解決方案的核心是cookie,cookie會攜帶伺服器端返回的sessionId, 在瀏覽器與伺服器端維護會話狀態。但是我們知道cookie是有限制的,cookie有域的概念,瀏覽器傳送http請求時會自動匹配該本站點的cookie域。而不是所有的cookie。
那麼既然這樣,我們很容易想到的是,我們可以把所有子系統的域名都放在一個頂級域名下不就可以了?比如 "*.taobao.com",然後將他們的cookie域設定為 "taobao.com", 但是這種並不好:
第一:因為所有系統的域名需要統一,比如淘寶和天貓的域名就不相同;
第二:應用群各個系統所使用的技術需要相同,比如tomcat伺服器叫JESSIONID, 其他的伺服器可能不叫這個標識。
第三:cookie的安全性不高的。
因此我們需要一種全新的方式來實現多系統應用群的登入,這就是單點登入。
什麼是單點登入?單點登入的全稱是 Single Sign On (可以簡稱為SSO), 在多個系統中只要登入一次,便可以在其他所有系統中得到授權而無需再次登入。
SSO有一個獨立的認證中心,認證中心它可以接受使用者的使用者名稱密碼等安全資訊,其他的地方不接受登入入口,只接受認證中心的間接授權,間接授權它是通過令牌實現的。授權令牌作為引數會傳送到各個子系統,子系統拿到令牌,因此會得到了授權,因此就可以建立了區域性的會話。區域性會話和單系統登入的原理很類似的。
下面我們來打個比方理解單點登入的基本原理:
第一步:我想登入A系統,A系統發現使用者未登入,因此我們需要他們跳轉到SSO認證中心(且將自己請求的地址作為引數傳遞過去)。SSO認證中心發現未登入,會將使用者引導到登入頁面。
第二步:使用者輸入使用者名稱和密碼提交申請登入,SSO認證中心會檢測使用者名稱和密碼資訊,如果使用者名稱和密碼正確的話,那麼使用者和SSO認證中心之間會建立一個區域性會話,並且建立一個授權令牌。sso認證中心會帶著該令牌跳轉到A系統那個請求的地址去。
第三步:A系統會檢測該令牌,如果有效的話,就會跳到使用者輸入的地址頁面去,否則,還是返回登入頁面,提示錯誤資訊。
第四步:使用者訪問系統B,系統B發現使用者未登入,會跳轉到SSO認證中心(將自己請求的地址作為引數傳遞過去),sso認證中心發現使用者已經登入了,會跳轉回系統B的那個地址去,並帶上令牌,系統B拿到令牌,就會去sso認證中心去校驗該令牌是否有效。
如果有效的話,說明認證成功了,就會跳轉到系統B訪問地址的頁面上去。
使用者現在已經登入成功了,sso認證中心會與各個子系統建立會話,使用者與sso認證中心建立的會話被稱為全域性會話,使用者與各個
子系統建立的會話被稱為區域性會話,區域性會話建立之後,使用者訪問子系統資源後就不會再通過sso認證中心了。
三:什麼是JSON Web Token?
JSON Web Token 是一個開放標準協議,它定義了一種緊湊和自包含的方式,它用於各方之間作為JSON物件安全地傳輸資訊。
它有如下優點:
1. 可以適用於分散式的單點登入場景。
2. 可以使用跨域認證解決方案。
3. jwt實現自動重新整理token的方案(待認證)。
JSON Web Token,它定義了一種緊湊和自包含的方式,如何理解緊湊和自包含呢?
緊湊:就是說這個資料量比較少,並且能通過url引數,http請求提交的資料以及http header的方式來傳遞。
自包含:這個串可以包含很多資訊,比如使用者id,訂單號id等,如果其他人拿到該資訊,就可以拿到關鍵業務資訊。
3.1)JWT的基本原理,基本流程如下:
1. 客戶端使用賬號和密碼請求登入介面。
2. 登入成功後伺服器使用簽名金鑰生成JWT,然後返回JWT給客戶端。
3. 客戶端再次向服務端請求其他介面時會帶上JWT。
4. 伺服器接收到JWT後驗證簽名的有效性,對客戶端做出相應的響應。
3.2)JWT與session的區別?
session是基於cookie來傳輸的,session資訊是儲存在伺服器端中,客戶端向伺服器端發請求時,伺服器端會返回一個jessionId給客戶端中的cookie中,以後每次請求都會從cookie中的jessionid傳遞過去,伺服器通過cookie中的sessionid獲取到當前會話的使用者,對於單系統來講這是沒有問題的,但是對於多個系統的話就涉及到session如何共享的問題了,並且隨著認證使用者增多的話,session會佔用大量伺服器記憶體。
JWT是儲存在客戶端的,伺服器端不需要儲存JWT,JWT含有使用者id,伺服器拿到jwt驗證後就可以拿到使用者資訊了,jwt是無狀態的,它不與任何機器繫結的,只要簽名金鑰足夠的安全就能保證jwt的可靠性。
3.3)JWT中的token與session中的token安全性比較
session 中安全性問題:
伺服器端執行session機制的時候會生成session的口令,在Tomcat伺服器中,預設會採用 jsessionid 這個值,但是在其他伺服器上會有所不同,比如Connect預設會叫 connect_uid, 我們一般把一些敏感的資訊放在cookie中是不可取的,但是將口令放在cookie中還是可以的,如果口令被篡改了的話,就丟失了對映關係,也無法修改伺服器端存在的資料,並且session的有效期一般為20/30分鐘,如果在該時間之內客戶端和伺服器端沒有產生任何互動,伺服器端會自動將session自動清空,因此session中想要維護使用者一直登陸的狀態的話,需要客戶端每隔20分鐘使用setInterval自動發一個請求給伺服器端,這樣的話,前後端就有互動,所以就可以一直保持登陸狀態。否則的話,每次20分鐘後,登陸狀態就會失效,每隔20分鐘使用者需要重新登入,使用者體驗將會變得不好。session這樣做的最主要的是為了安全性考慮,有效期的時間非常短,防止黑客攻擊。
JWT方案中安全性問題
jwt是儲存在客戶端的,伺服器端不需要儲存jwt的,客戶端每次傳送請求時會攜帶該token,然後到伺服器端會驗證token是否正確,是否過期了,然後會通過解碼出攜帶的使用者的資訊的,但是如果token在傳輸的過程中被攻擊者擷取了的話,那麼對方就可以偽造請求,利用竊取到的token模擬正常請求,實現使用者的正常操作,而伺服器端完全不知道,因為JWT在伺服器端是無狀態的,且伺服器端不儲存jwt的。其實jwt解決的問題是認證和授權的問題,對於安全性的話,還是建議對外公佈的介面使用https.
四:理解JWT的基本資料結構
基本的JWT的資料結構是如下這樣的:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImlhdCI6MTU0Mzc1MzczNX0.h1XmQo017udxlFsH-8US9Lg8dJ0IDsSbRbjEN5Nq0l4
如上它是由三部分組成的,中間使用 . 分割成三個部分,它是有 Header(頭部), PayLoad(負載),Signature(簽名)組成的。
如:Header.Payload.Signature
4.1 Header
Header部分是一個JSON物件,描述JWT的後設資料,一般是如下的樣子:
{ "typ": "JWT", "alg": "HS256" }
如上json程式碼,alg屬性表示簽名的演算法,預設是 HMAC SHA256 (縮寫為:HS256); typ屬性表示這個令牌(token)的型別為JWT. 最後將上面的JSON物件使用 Base64URL的演算法轉成字串。
我們可以使用線上的base64編碼轉下(http://tool.oschina.net/encrypt?type=3),如下所示:
如上 alg 部分,預設加密的演算法是 HMAC SHA256, 當然我們也可以選擇下面的加密演算法,加密演算法有如下:
4.2 Payload
Payload部分也是一個JSON物件,用來存放實際需要傳遞的資料,官方提供了7個欄位,如下:
iss(issuer): 簽發人
exp (expiration time): 過期時間
sub (subject): 主題
aud (audience): 受眾
nbf (Not Before): 生效時間
iat (Issued At): 簽發時間
jti (JWT ID): 編號
payload的中文含義是載荷,它可以理解為存放有效資訊的地方。這些有效資訊一般包含如下三個部分:
4.2.1)標準中註冊的宣告:(如上就是官方提供的7個欄位)。
4.2.2)公共的宣告:公共的宣告可以新增任何的資訊,一般新增使用者的相關資訊或其他業務需要的必要資訊,但是不建議新增敏感資訊,
因為該部分在客戶單可解密。
4.2.3)私有的宣告:私有宣告是提供者和消費者所共同定義的宣告,一般不建議存放敏感資訊,因為base64是對稱解密的,該部分資訊
可以理解為明文資訊。
那麼定義一個簡單的 payload 可以如下結構:
{ "sub": '123456', "name": "kongzhi", "admin": true }
我們還是使用如上的base64編碼,會編碼成如下所示:
4.3 Signature
Signature 是對前面兩部分的簽名,防止資料被篡改。簽名是把Header和payload對應的json結構進行base64 編碼之後得到的兩個串用英文句點號拼接起來的,然後會根據header裡面的alg指定的前面演算法(預設是 HMAC SHA256)生成出來的。
如上header部分使用的是 HS256(即HMAC和SHA256),HMAC是用於生成摘要的,SHA256是用於對摘要進行數字簽名的。因此使用HMACSHA256實現signature實現的演算法如下:
HMACSHA256( base64UrlEncode(header) + '.' + base64UrlEncode(payload), secret )
如上是 Signature 簽名演算法,最後一個 secret 是加密的金鑰的含義。因此通過如上的用法我們就可以拿到JWT了。
4.4 JWT實踐
JWT的格式是由三個點分割的base64-URL字串,可以在html或http環境中傳遞,我們可以簡單的使用 https://jwt.io/ 官網來生成一個JWT了,如下是我前面定義的部分資料:
五:node中使用JWT的API
nodejs實現的jwt的github程式碼(https://github.com/auth0/node-jsonwebtoken)
它主要有3個方法:
5.1 jwt.sign(payload, secretOrPrivateKey, [options, callback])
payload 引數必須是一個object、Buffer、或 string.
注意:exp(過期時間) 只有當payload是object字面量時才可以設定。如果payload不是buffer或string,它會被強制轉換為使用的字串JSON.stringify()。
secretOrPrivateKey 引數 是包含HMAC演算法的金鑰或RSA和ECDSA的PEM編碼私鑰的string或buffer。
options 引數有如下值:
algorithm:加密演算法(預設值:HS256) expiresIn:以秒錶示或描述時間跨度zeit / ms的字串。如60,"2 days","10h","7d",含義是:過期時間 notBefore:以秒錶示或描述時間跨度zeit / ms的字串。如:60,"2days","10h","7d" audience:Audience,觀眾 issuer:Issuer,發行者 jwtid:JWT ID subject:Subject,主題 noTimestamp header
該方法如果是非同步方法,則會提供回撥,如果是同步的話,則將會 JsonWebToken返回為字串。
在expiresIn, notBefore, audience, subject, issuer沒有預設值時,可以直接在payload中使用 exp, nbf, aud, sub 和 iss分別表示。
注意:如果在jwts中沒有指定 noTimestamp的話,在jwts中會包含一個iat,它的含義是使用它來代替實際的時間戳來計算的。
下面我們在專案中使用node中jsonwebtoken來生成一個JWT的demo了,在index.js 程式碼如下:
// 生成一個token const jwt = require('jsonwebtoken'); const secret = 'abcdef'; let token = jwt.sign({ name: 'kongzhi' }, secret, (err, token) => { console.log(token); });
然後我們進入專案中的目錄,執行 node index.js 執行後看到命令列中會列印中的token了,如下所示:
當然我們也可以設定token的過期時間,比如設定token的有效期為1個小時,如下程式碼:
// 生成一個token const jwt = require('jsonwebtoken'); const secret = 'abcdef'; // 設定token為一個小時有效期 let token = jwt.sign({ name: 'kongzhi', exp: Math.floor(Date.now() / 1000) + (60 * 60) }, secret, (err, token) => { console.log(token); });
5.2 jwt.verify(token, secretOrPrivateKey, [options, callback])
該方法是驗證token的合法性
比如上面生成的token設定為1個小時,生成的token為:
'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImlhdCI6MTU0Mzc2MjYzOSwiZXhwIjoxNTQzNzY2MjM5fQ.6idR7HPpjZIfZ_7j3B3eOnGzbvWouifvvJfeW46zuCw'
下面我們使用 jwt.verify來驗證一下:
const jwt = require('jsonwebtoken'); const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImlhdCI6MTU0Mzc2MjYzOSwiZXhwIjoxNTQzNzY2MjM5fQ.6idR7HPpjZIfZ_7j3B3eOnGzbvWouifvvJfeW46zuCw'; const secret = 'abcdef'; jwt.verify(token, secret, (error, decoded) => { if (error) { console.log(error.message); } console.log(decoded); });
執行node index.js 程式碼後,生成如下資訊:
現在我們再來生成一個token,假如該token的有效期為30秒,30秒後,我再使用剛剛生成的token,再去使用 verify去驗證下,看是否能驗證通過嗎?(理論上token失效了,是不能驗證通過的,但是我們還是來實踐下)。如下程式碼:
// 生成一個token const jwt = require('jsonwebtoken'); const secret = 'abcdef'; // 設定token為30秒的有效期 let token = jwt.sign({ name: 'kongzhi', exp: Math.floor(Date.now() / 1000) + 30 }, secret, (err, token) => { console.log(token); });
在命令列中生成 jwt為: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImV4cCI6MTU0Mzc2MzU1MywiaWF0IjoxNTQzNzYzNTIzfQ.79rH3h_ezayYBeNQ2Wj8fGK_wqsEqEPgRTG9uGmvD64';
然後我們現在使用該token去驗證下,如下程式碼:
// 生成一個token const jwt = require('jsonwebtoken'); const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoia29uZ3poaSIsImV4cCI6MTU0Mzc2MzU1MywiaWF0IjoxNTQzNzYzNTIzfQ.79rH3h_ezayYBeNQ2Wj8fGK_wqsEqEPgRTG9uGmvD64'; const secret = 'abcdef'; jwt.verify(token, secret, (error, decoded) => { if (error) { console.log(error.message); } console.log(decoded); });
執行命令,如下所示:
如上可以看到token的有效期為30秒,30秒後再執行的話,就會提示jwt過期了。
5.3 jwt.decode(token, [, options])
該方法是 返回解碼沒有驗證簽名是否有效的payload。