引語
最近正好在獨(dú)立開發(fā)一個后臺管理系統(tǒng),涉及到了基于Token的身份認(rèn)證,自己邊學(xué)邊用邊做整理和總結(jié),對基于JWT實現(xiàn)的Token的身份認(rèn)證做一次相對比較全面的認(rèn)識。
一、基于session的跨域身份驗證
Internet服務(wù)無法與用戶身份驗證分開。一般過程如下。
用戶向服務(wù)器發(fā)送用戶名和密碼。驗證服務(wù)器后,相關(guān)數(shù)據(jù)(如用戶角色,登錄時間等)將保存在當(dāng)前會話中。服務(wù)器向用戶返回session_id,session信息都會寫入到用戶的Cookie。用戶的每個后續(xù)請求都將通過在Cookie中取出session_id傳給服務(wù)器。服務(wù)器收到session_id并對比之前保存的數(shù)據(jù),確認(rèn)用戶的身份。
這種模式最大的問題是,沒有分布式架構(gòu),無法支持橫向擴(kuò)展。如果使用一個服務(wù)器,該模式完全沒有問題。但是,如果它是服務(wù)器群集或面向服務(wù)的跨域體系結(jié)構(gòu)的話,則需要一個統(tǒng)一的session數(shù)據(jù)庫庫來保存會話數(shù)據(jù)實現(xiàn)共享,這樣負(fù)載均衡下的每個服務(wù)器才可以正確的驗證用戶身份。
例如蟲蟲舉一個實際中常見的單點(diǎn)登陸的需求:站點(diǎn)A和站點(diǎn)B提供同一公司的相關(guān)服務(wù)。現(xiàn)在要求用戶只需要登錄其中一個網(wǎng)站,然后它就會自動登錄到另一個網(wǎng)站。怎么做?
一種解決方案是聽過持久化session數(shù)據(jù),寫入數(shù)據(jù)庫或文件持久層等。收到請求后,驗證服務(wù)從持久層請求數(shù)據(jù)。該解決方案的優(yōu)點(diǎn)在于架構(gòu)清晰,而缺點(diǎn)是架構(gòu)修改比較費(fèi)勁,整個服務(wù)的驗證邏輯層都需要重寫,工作量相對較大。而且由于依賴于持久層的數(shù)據(jù)庫或者問題系統(tǒng),會有單點(diǎn)風(fēng)險,如果持久層失敗,整個認(rèn)證體系都會掛掉。
總結(jié)基于服務(wù)器驗證方式暴露的一些明顯的問題:
Session:每次認(rèn)證用戶發(fā)起請求時,服務(wù)器需要去創(chuàng)建一個記錄來存儲信息。當(dāng)越來越多的用戶發(fā)請求時,內(nèi)存的開銷也會不斷增加。可擴(kuò)展性:在服務(wù)端的內(nèi)存中使用Seesion存儲登錄信息,伴隨而來的是可擴(kuò)展性問題。CORS(跨域資源共享):當(dāng)我們需要讓數(shù)據(jù)跨多臺移動設(shè)備上使用時,跨域資源的共享會是一個讓人頭疼的問題。在使用Ajax抓取另一個域的資源,就可以會出現(xiàn)禁止請求的情況。CSRF(跨站請求偽造):用戶在訪問銀行網(wǎng)站時,他們很容易受到跨站請求偽造的攻擊,并且能夠被利用其訪問其他的網(wǎng)站。在這些問題中,可擴(kuò)展行是最突出的。因此我們有必要去尋求一種更有行之有效的方法。基于Token認(rèn)證的身份認(rèn)證方案
那么有什么更好的方案嗎?當(dāng)然有,那就是基于Token的身份認(rèn)證方案。
那么基于Token的身份驗證可以解決哪些問題呢?
Token 完全由應(yīng)用管理,所以它可以避開同源策略Token 可以避免 CSRF 攻擊Token 可以是無狀態(tài)的,可以在多個服務(wù)間共享基于Token認(rèn)證的原理
Token 是在服務(wù)端產(chǎn)生的,是無狀態(tài)的,我們不將用戶信息存在服務(wù)器或Session中。如果客戶端使用用戶名/密碼向服務(wù)端請求認(rèn)證,服務(wù)端認(rèn)證成功,那么在服務(wù)端會返回 Token 給客戶端。
客戶端可以在每次請求的時候帶上 Token 證明自己的合法地位。如果這個 Token 在服務(wù)端持久化(比如存入數(shù)據(jù)庫),那它就是一個永久的身份令牌。基于Token的身份驗證這種概念解決了在服務(wù)端存儲信息時的許多問題。NoSession意味著你的程序可以根據(jù)需要去增減機(jī)器,而不用去擔(dān)心用戶是否登錄。
基于Token的身份驗證的方案過程如下:
用戶通過用戶名和密碼發(fā)送請求。服務(wù)端驗證。服務(wù)端返回一個簽名的token 給客戶端。客戶端儲存token,并且每次發(fā)送請求都會攜帶token。服務(wù)端驗證token并返回數(shù)據(jù)。Token的優(yōu)勢無狀態(tài)、可擴(kuò)展
在客戶端存儲的Tokens是無狀態(tài)的,并且能夠被擴(kuò)展。基于這種無狀態(tài)和不存儲Session信息,負(fù)載負(fù)載均衡器能夠?qū)⒂脩粜畔囊粋€服務(wù)傳到其他服務(wù)器上。
安全性
請求中發(fā)送token而不再是發(fā)送cookie能夠防止CSRF(跨站請求偽造)。即使在客戶端使用cookie存儲token,cookie也僅僅是一個存儲機(jī)制而不是用于認(rèn)證。不將信息存儲在Session中,讓我們少了對session操作。
Token是有時效的,一段時間之后用戶需要重新驗證。我們也不一定需要等到token自動失效,token有撤回的操作,通過token revocataion可以使一個特定的token或是一組有相同認(rèn)證的token無效。
可擴(kuò)展性
Tokens能夠創(chuàng)建與其它程序共享權(quán)限的程序。例如,能將一個隨便的社交帳號和自己的大號(Fackbook或是Twitter)聯(lián)系起來。當(dāng)通過服務(wù)登錄Twitter(我們將這個過程Buffer)時,我們可以將這些Buffer附到Twitter的數(shù)據(jù)流上(we are allowing Buffer to post to our Twitter stream)。
使用tokens時,可以提供可選的權(quán)限給第三方應(yīng)用程序。當(dāng)用戶想讓另一個應(yīng)用程序訪問它們的數(shù)據(jù),我們可以通過建立自己的API,得出特殊權(quán)限的tokens。
多平臺跨域
我們提前先來談?wù)撘幌翪ORS(跨域資源共享),對應(yīng)用程序和服務(wù)進(jìn)行擴(kuò)展的時候,需要介入各種各種的設(shè)備和應(yīng)用程序。
Having our API just serve data, we can also make the design choice to serve assets from a CDN. This eliminates the issues that CORS brings up after we set a quick header configuration for our application.
只要用戶有一個通過了驗證的token,數(shù)據(jù)和資源就能夠在任何域上被請求到。
基于標(biāo)準(zhǔn)
創(chuàng)建token的時候,你可以設(shè)定一些選項。我們在后續(xù)的文章中會進(jìn)行更加詳盡的描述,但是標(biāo)準(zhǔn)的用法會在JSON Web Tokens體現(xiàn)。
最近的程序和文檔是供給JSON Web Tokens的。它支持眾多的語言。這意味在未來的使用中你可以真正的轉(zhuǎn)換你的認(rèn)證機(jī)制。
無狀態(tài) Token
如果我們把所有狀態(tài)信息都附加在 Token 上,服務(wù)器就可以不保存。但是服務(wù)端仍然需要認(rèn)證 Token 有效。不過只要服務(wù)端能確認(rèn)是自己簽發(fā)的 Token,而且其信息未被改動過,那就可以認(rèn)為 Token 有效——“簽名”可以作此保證。平時常說的簽名都存在一方簽發(fā),另一方驗證的情況,所以要使用非對稱加密算法。但是在這里,簽發(fā)和驗證都是同一方,所以對稱加密算法就能達(dá)到要求,而對稱算法比非對稱算法要快得多(可達(dá)數(shù)十倍差距)。更進(jìn)一步思考,對稱加密算法除了加密,還帶有還原加密內(nèi)容的功能,而這一功能在對 Token 簽名時并無必要——既然不需要解密,摘要(散列)算法就會更快。可以指定密碼的散列算法,自然是 HMAC。
上面說了這么多,還需要自己去實現(xiàn)嗎?不用! JWT 已經(jīng)定義了詳細(xì)的規(guī)范,而且有各種語言的若干實現(xiàn)。
在使用無狀態(tài) Token 的時候,有兩點(diǎn)需要注意:
Refresh Token 有效時間較長,所以它應(yīng)該在服務(wù)器端有狀態(tài),以增強(qiáng)安全性,確保用戶注銷時可控應(yīng)該考慮使用二次認(rèn)證來增強(qiáng)敏感操作的安全性基于JWT實現(xiàn)的Token認(rèn)證方案JSON Web Token是什么?
JSON Web Token(JWT)是目前最流行的跨域身份驗證解決方案。
JSON Web Token(JWT)是一個開放標(biāo)準(zhǔn)(RFC 7519),它定義了一種緊湊且自包含的方式,用于在各方之間作為JSON對象安全地傳輸信息。由于此信息是經(jīng)過數(shù)字簽名的,因此可以被驗證和信任。可以使用秘密(使用HMAC算法)或使用RSA或ECDSA的公用/專用密鑰對對JWT進(jìn)行簽名。
JWT架構(gòu):
什么時候應(yīng)該使用 JSON Web Token?身份驗證
這是使用JWT的最常見方案。一旦用戶登錄,每個后續(xù)請求將包括JWT,從而允許用戶訪問該令牌允許的路由,服務(wù)和資源。
單一登錄是當(dāng)今廣泛使用JWT的一項功能,因為它的開銷很小并且可以在不同的域中輕松使用。
信息交換
JSON Web令牌是在各方之間安全地傳輸信息的一種好方法。因為可以對JWT進(jìn)行簽名(例如,使用公鑰/私鑰對),所以您可以確定發(fā)件人是本人。
另外,由于簽名是使用標(biāo)頭和有效負(fù)載計算的,因此您還可以驗證內(nèi)容是否未被篡改。
JSON Web Token 結(jié)構(gòu)
JSON Web令牌以緊湊的形式由三部分組成,這些部分由點(diǎn) (. )分隔,分別是:
因此,JWT通常如下所示
xxxxx.yyyyy.zzzzz
Header:標(biāo)頭
讓我們分解不同的部分。
標(biāo)頭通常由兩部分組成:令牌的類型(即JWT)和所使用的簽名算法,例如HMAC SHA256或RSA。
例如:
{
"alg": "HS256",
"typ": "JWT"
}
然后,此JSON被Base64Url編碼以形成JWT的第一部分。
Payload: 有效載荷
令牌的第二部分是包含聲明的有效負(fù)載。聲明是關(guān)于實體(通常是用戶)和其他數(shù)據(jù)的聲明。有三種類型的聲明:已注冊聲明、公共聲明和私有聲明。
已注冊的聲明:這些是一組預(yù)定義的聲明,它們不是強(qiáng)制的登錄token無效,而是推薦的,以提供一組有用的、可互操作的聲明。主要有:
請注意,聲明名稱僅是三個字符,因為JWT是緊湊的。
公共聲明:這些聲明可以由使用JWTs的用戶隨意定義。但是,為了避免沖突,應(yīng)該在IANA JSON Web令牌注冊表中定義它們,或者將它們定義為包含防沖突命名空間的URI。
私有聲明:這些是為在同意使用它們的各方之間共享信息而創(chuàng)建的自定義索賠,既不是注冊索賠,也不是公開索賠。
有效負(fù)載示例可以是:
{
"sub": "1234567890",

"name": "John Doe",
"admin": true
}
對有效負(fù)載進(jìn)行Base64Url編碼,以形成JSON Web令牌的第二部分。
請注意,對于已簽名的令牌登錄token無效,此信息盡管可以防止篡改,但任何人都可以讀取。除非將其加密,否則請勿將機(jī)密信息放入JWT的有效負(fù)載或報頭元素中。簽名
要創(chuàng)建簽名部分,您必須獲取編碼的頭、編碼的負(fù)載、密鑰、頭中指定的算法,并對其進(jìn)行簽名。
例如,如果要使用HMAC SHA256算法,則將通過以下方式創(chuàng)建簽名:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
簽名用于驗證消息在整個過程中沒有更改,并且對于使用私鑰進(jìn)行簽名的令牌,它還可以驗證JWT的發(fā)送者是它所說的真實身份。
整合在一起
輸出是三個由點(diǎn)分隔的Base64 URL字符串,這些點(diǎn)可以在HTML和HTTP環(huán)境中輕松傳遞,同時與基于XML的標(biāo)準(zhǔn)(如SAML)相比更加緊湊。
下面顯示了一個JWT,它對前一個報頭和有效負(fù)載進(jìn)行了編碼,并用一個秘密進(jìn)行了簽名。
可以從此圖中看出JWT生成的令牌的格式與其對應(yīng)餓原文之間的關(guān)聯(lián)。這里也順帶推薦一下jwt官網(wǎng)的JWT debuger工具。
JSON Web Token工作原理
在身份驗證中,當(dāng)用戶使用其憑據(jù)成功登錄時,將返回一個JSON Web Token。由于Token是憑據(jù),必須非常小心地防止安全問題。一般來說,您不應(yīng)該將令牌保留的時間超過所需的時間。
由于缺乏安全性,也不應(yīng)將敏感會話數(shù)據(jù)存儲在瀏覽器存儲中。
當(dāng)用戶想要訪問受保護(hù)的路由或資源時,用戶代理應(yīng)該發(fā)送JWT,通常在授權(quán)頭中使用承載模式。標(biāo)題的內(nèi)容應(yīng)如下所示:
Authorization: Bearer
在某些情況下,這可以是無狀態(tài)授權(quán)機(jī)制。服務(wù)器的受保護(hù)路由將檢查授權(quán)頭中是否存在有效的JWT,如果存在,則允許用戶訪問受保護(hù)的資源。如果JWT包含必要的數(shù)據(jù),則可以減少查詢數(shù)據(jù)庫以執(zhí)行某些操作的需要,盡管情況并非總是如此。
如果令牌在授權(quán)頭中發(fā)送,則跨源資源共享(CORS)不會成為問題,因為它不使用cookies。
下圖顯示了如何獲取JWT并將其用于訪問API或資源:
應(yīng)用程序或客戶端向授權(quán)服務(wù)器請求授權(quán)。這是通過不同的授權(quán)流之一執(zhí)行的。例如,典型的符合OpenID Connect的web應(yīng)用程序?qū)⑹褂檬跈?quán)代碼流通過/oauth/authorize端點(diǎn)。當(dāng)授權(quán)被授予時,授權(quán)服務(wù)器將向應(yīng)用程序返回一個訪問Token。應(yīng)用程序使用訪問令牌訪問受保護(hù)的資源(如API)。
請注意:使用簽名的Token,Token中包含的所有信息都將向用戶或其他方公開,即使他們無法更改它。這意味著您不應(yīng)將機(jī)密信息放入Token中。
JWT的使用生成令牌
jwt.sign(payload, secretOrPrivateKey, [options, callback])
// paylod: 有效載荷
// secretOrPrivateKey: 加密密鑰或私鑰
// option(可選): 生成令牌設(shè)置
// callback(可選): 回調(diào)函數(shù)
其中option可配置屬性,屬性均為可選:
algorithm: 算法(默認(rèn): HS256)
noTimestamp: 無時間戳
header: 頭部
keyid: 鍵值編號
mutatePayload: 是否對payload進(jìn)行轉(zhuǎn)化,若為true則會用payload初始值生成令牌
以下6相即可在payload中配置也可在option中配置,注意只可在一處出現(xiàn)。
expiresIn: 令牌過期時間(可為數(shù)字(單位秒)或帶單位的字符串,例如 60, "2 days", "10h", "7d"等)
notBefore: 在此之前不可用(格式如上述expiresIn)
audience: 用戶
issuer: 發(fā)布者
jwtid: 令牌id
subject: 主題
生成令牌實例
// 異步回調(diào)方式
Let privateKey = 'Cloudy'
jwt.sign({ id: '1', exp:'7d' }, privateKey, { algorithm: 'RS256' }, function(err, token) {
console.log(token);
});
// 同步方式(推薦用promise對其進(jìn)行進(jìn)一步封裝)
let token = jwt.sign({ foo: 'bar' }, privateKey, { algorithm: 'RS256', expiresIn: '1h' });
令牌驗證
jwt.verify(token, secretOrPublicKey, [options, callback])
// token: 令牌
// secretOrPublicKey: 密鑰或公鑰
// option: 生成令牌設(shè)置(可選)

// callback: 回調(diào)函數(shù)(可選)
其中option可配置屬性有:
algorithms: 算法
audience: 如果你想驗證用戶,為其提供一個字符串或正則表達(dá)式
complete: Boolean值,若為true則完整輸出令牌
issuer(可選): 如果你想驗證發(fā)布者,為其提供一個字符串或正則表達(dá)式
ignoreExpiration: Boolean值,若為true則不會驗證過期時間
subject: 如果你想驗證主題,為其提供一個字符串或正則表達(dá)式
clockTolerance: 時鐘容忍,在檢查nbf和exp聲明時,處理不同服務(wù)器之間的小時鐘差異所允許的秒數(shù)
maxAge: 允許令牌的最大允許年齡仍然有效。它以秒或描述時間跨度zeit/ms的字符串表示。Eg: 1000, "2 days", "10h", "7d".
clockTimestamp: 時間戳,應(yīng)用作所有必要比較的當(dāng)前時間(秒)。
nonce:如果要檢查nonce聲明,請在此處提供一個字符串值。
驗證令牌實例
// 同步驗證(對稱加密算法)
var decoded = jwt.verify(token, 'shhhhh');
console.log(decoded.foo) // bar
// 異步驗證用戶(使用不對稱加密算法)
var cert = fs.readFileSync('public.pem'); // 獲取公鑰
jwt.verify(token, cert, { audience: 'urn:foo', jwtid: 'jwtid', subject: 'subject' }, function(err, decoded) {
// if audience mismatch, err == invalid audience
});
簡單解碼(無驗證)
(同步)返回解碼的有效負(fù)載,而不驗證簽名是否有效。
jwt.decode(token [, options])
解碼options可配置屬性:
json 在負(fù)載上強(qiáng)制JSON.parse,即使頭不包含“typ”:“JWT”。

complete 返回一個帶有解碼有效負(fù)載和頭的對象。
支持的算法數(shù)組。目前支持以下算法。alg Parameter ValueDigital Signature or MAC Algorithm
HS256
HMAC using SHA-256 hash algorithm
HS384
HMAC using SHA-384 hash algorithm
HS512
HMAC using SHA-512 hash algorithm
RS256
RSASSA-PKCS1-v1_5 using SHA-256 hash algorithm
RS384
RSASSA-PKCS1-v1_5 using SHA-384 hash algorithm
RS512
RSASSA-PKCS1-v1_5 using SHA-512 hash algorithm
PS256
RSASSA-PSS using SHA-256 hash algorithm (only node ^6.12.0 OR >=8.0.0)
PS384
RSASSA-PSS using SHA-384 hash algorithm (only node ^6.12.0 OR >=8.0.0)
PS512
RSASSA-PSS using SHA-512 hash algorithm (only node ^6.12.0 OR >=8.0.0)
ES256
ECDSA using P-256 curve and SHA-256 hash algorithm
ES384
ECDSA using P-384 curve and SHA-384 hash algorithm
ES512
ECDSA using P-521 curve and SHA-512 hash algorithm
none
No digital signature or MAC value included
JWT問題和趨勢
1、JWT默認(rèn)不加密,但可以加密。生成原始令牌后,可以使用改令牌再次對其進(jìn)行加密。
2、當(dāng)JWT未加密方法是,一些私密數(shù)據(jù)無法通過JWT傳輸。
3、JWT不僅可用于認(rèn)證,還可用于信息交換。善用JWT有助于減少服務(wù)器請求數(shù)據(jù)庫的次數(shù)。
4、JWT的最大缺點(diǎn)是服務(wù)器不保存會話狀態(tài),所以在使用期間不可能取消令牌或更改令牌的權(quán)限。也就是說,一旦JWT簽發(fā),在有效期內(nèi)將會一直有效。
5、JWT本身包含認(rèn)證信息,因此一旦信息泄露,任何人都可以獲得令牌的所有權(quán)限。為了減少盜用,JWT的有效期不宜設(shè)置太長。對于某些重要操作,用戶在使用時應(yīng)該每次都進(jìn)行進(jìn)行身份驗證。
6、為了減少盜用和竊取,JWT不建議使用HTTP協(xié)議來傳輸代碼,而是使用加密的HTTPS協(xié)議進(jìn)行傳輸。
結(jié)語
最近正好在獨(dú)立開發(fā)一個后臺管理系統(tǒng),涉及到了Token驗證,如果覺得文章對你有用,請你幫我點(diǎn)個贊吧,你的點(diǎn)贊和關(guān)注是我一直堅持分享的動力!