一:理解單系統登錄的原理及實現?
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。