開放、平等、協(xié)作、快速、分享
本質(zhì)上它是一段簽名的 JSON 格式的數(shù)據(jù)。由于它是帶有簽名的,因此接收者便可以驗(yàn)證它的真實(shí)性。同時(shí)由于它是 JSON 格式的因此它的體積也很小。如果你想了解有關(guān)它的正式定義,可以在 RFC 7519 中找到。
這篇文章發(fā)布于黑客新聞上。在這里也可以看一下關(guān)于這篇文章的案例分析,它主要包含了文章內(nèi)容的公開分析、SEO 影響、性能影響以及更多其他的內(nèi)容。
數(shù)據(jù)簽名已經(jīng)不是什么新事物了 - 令人值得興奮的是如何在不依靠 sessions 的情況下使用 JWT 創(chuàng)建真正的 RESTful 服務(wù),目前這個(gè)想法已經(jīng)被事實(shí)證明有一段時(shí)間了。下面是介紹它在現(xiàn)實(shí)中具體實(shí)現(xiàn)的工作原理 - 首先在這里我來做一個(gè)類比:
想象一下你剛從國(guó)外度完假回來,你在邊境上說 - 你可以讓我通過,我是這里的公民。這樣的回答很好也沒有問題,但是你要如何去支持你的說法呢?最有可能的方案是你攜帶了護(hù)照來證明你的身份。這里我們假設(shè)邊境工作人員也都被要求去核實(shí)護(hù)照是真正由你的國(guó)家的護(hù)照辦簽發(fā)的。那么護(hù)照就會(huì)被核實(shí),這樣他們也才會(huì)放你回國(guó)。
現(xiàn)在,讓我們從 JWT 的角度看一下這個(gè)故事,它們各自又都扮演著什么樣的角色:
護(hù)照辦 - 發(fā)布 JWT 的身份驗(yàn)證服務(wù)。
護(hù)照 - 你通過"護(hù)照辦"獲得的 JWT 簽名。你的身份對(duì)于任何人都是可讀的,但是只有它是真實(shí)的時(shí)候相關(guān)方才會(huì)對(duì)其核實(shí)。
公民資格 - 在 JWT 中包含的你的聲明(你的護(hù)照)。
邊境 - 你的應(yīng)用程序的安全層,在被允許訪問受保護(hù)的資源之前由它來核實(shí)你的 JWT 令牌身份,在這種情況下指的是 - 國(guó)家。
國(guó)家 - 你想要獲取的資源(例如 API)。
簡(jiǎn)單來說,JWT 非常的酷,因?yàn)槟悴挥迷贋榱髓b別用戶而在你的服務(wù)器上去保留你的 session 數(shù)據(jù)。這個(gè)工作流將會(huì)變得像下面這樣:
用戶調(diào)用身份驗(yàn)證服務(wù),通常是發(fā)送了用戶名及密碼。
身份驗(yàn)證服務(wù)響應(yīng)并返回了簽名的 JWT,上面包含了用戶是誰的內(nèi)容。
用戶向安全服務(wù)發(fā)送請(qǐng)求收到安全服務(wù)返回的令牌。
安全層檢驗(yàn)令牌上的簽名并且在簽名為真實(shí)的時(shí)候授權(quán)予以通過。
讓我們考慮一下這樣做的結(jié)果。
沒有 sessions 意味著你沒有會(huì)話存儲(chǔ)。但除非您的應(yīng)用程序需要橫向擴(kuò)展,否則這也不太重要,如果你的應(yīng)用程序是運(yùn)行在多個(gè)服務(wù)器上的,那么共享 session 數(shù)據(jù)將會(huì)成為一個(gè)負(fù)擔(dān)。你需要一個(gè)專門的服務(wù)器來只存儲(chǔ)會(huì)話數(shù)據(jù)或是共享磁盤空間或是在負(fù)載均衡上粘滯會(huì)話。當(dāng)你不使用 sessions 時(shí)上面的這些也就自然不再需要了。
通常來講 sessions 需要留意過期和垃圾收集的情況。JWT 可以在用戶數(shù)據(jù)中包含自己的過期日期。因此安全層在檢驗(yàn) JWT 的授權(quán)時(shí)可以同時(shí)核對(duì)它的過期時(shí)間來拒絕訪問。
只有在無 sessions 的情況下你可以創(chuàng)建真正的 RESTful 服務(wù),因?yàn)樗徽J(rèn)為是無狀態(tài)的。 JWT 很小所以它可以在每一個(gè)請(qǐng)求中被一起發(fā)出去,就像一個(gè) session cookie一樣。然而與 session cookie 不同的是,它并不指向服務(wù)器上的任何存儲(chǔ)數(shù)據(jù), JWT 本身包含了這些數(shù)據(jù)。
在我們更深入討論之前,有一件事需要了解。JWT 自身并不是一個(gè)東西。它是 JSON 網(wǎng)絡(luò)簽名(JWS)或 JSON 網(wǎng)絡(luò)加密 (JWE)中的一種類型。它的定義如下:
一個(gè) JWT 的聲明內(nèi)容會(huì)被編碼為一個(gè) JSON 對(duì)象,它被作為 JSON 網(wǎng)絡(luò)簽名結(jié)構(gòu)的有效載荷或是作為 JSON 網(wǎng)絡(luò)加密結(jié)構(gòu)的明文信息。
前者給我們的只是一個(gè)簽名并且它包含的數(shù)據(jù)(或是平時(shí)所稱呼的 "claims" 的命名)是對(duì)任何人都可讀的。后者則提供了加密的內(nèi)容,所以只有擁有密鑰的人可以解密它。JWS 在實(shí)現(xiàn)上更加容易并且基本用法上是不需要加密的 - 畢竟如果你在客戶端上有密鑰的話,你還不如把所有的東西不加密的好。因此 JWS 在大多數(shù)情況下都是適用的,也因此在之后我將主要關(guān)注 JWS。
頭部 - 關(guān)于簽名算法的信息,以 JSON 格式的負(fù)載類型(JWT)等等。
負(fù)載 - JSON 格式的實(shí)際的數(shù)據(jù)(或是聲明)。
簽名 - 額... 就是簽名。
我將在之后具體解釋這些細(xì)節(jié)?,F(xiàn)在讓我們先來分析下基礎(chǔ)要素。
上述所提到的每一部分(頭部,負(fù)載和簽名)是基于 base64url 編碼的,然后他們用 '.' 作為分隔符粘連起來組成 JWT。 下面是這個(gè)實(shí)現(xiàn)方式可能看上去的樣子:
var header = { // The signing algorithm. "alg": "HS256", // The type (typ) property says it's "JWT", // because with JWS you can sign any type of data. "typ": "JWT" }, // Base64 representation of the header object. headerB64 = btoa(JSON.stringify(header)), // The payload here is our JWT claims. payload = { "name": "John Doe", "admin": true }, // Base64 representation of the payload object. payloadB64 = btoa(JSON.stringify(payload)), // The signature is calculated on the base64 representation // of the header and the payload. signature = signatureCreatingFunction(headerB64 + '.' + payloadB64), // Base64 representation of the signature. signatureB64 = btoa(signature), // Finally, the whole JWS - all base64 parts glued together with a '.' jwt = headerB64 + '.' + payloadB64 + '.' + signatureB64;
由此得到的 JWS 結(jié)果看上去整潔而優(yōu)雅,有點(diǎn)像這樣:
`eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.OLvs36KmqB9cmsUrMpUutfhV52_iSz4bQMYJjkI_TLQ`
你也可以試著在 jwt.io 這個(gè)網(wǎng)站上來創(chuàng)建令牌試試。
有一點(diǎn)相當(dāng)重要,那就是簽名是依據(jù)頭部和負(fù)載計(jì)算出來的。因此頭部和負(fù)載的授權(quán)也很容易同樣被檢驗(yàn):
[headerB64, payloadB64, signatureB64] = jwt.split('.');if (atob(signatureB64) === signatureCreatingFunction(headerB64 + '.' + payloadB64) { // good} else // no good}
事實(shí)上,JWT 頭部被稱為 JOSE 頭部。JOSE 表示的是 JSON 對(duì)象的簽名和加密。也正如你期望的那樣,JWS 和 JWE 都是這樣的一個(gè)頭部,然而它們各自之間存在著一套稍微不同的注冊(cè)參數(shù)。下面是在 JWS 中使用的頭部注冊(cè)參數(shù)列表。所有的參數(shù)除了第一個(gè)參數(shù)(alg)以外,其他參數(shù)都是可選的:
alg 算法 (必選項(xiàng))
typ 類型 (如果是 JWT 那么就帶有一個(gè)值 JWT
,如果存在的話)
kid 密鑰 ID
cty 內(nèi)容類型
jku JWK 指定 URL
jwk JSON 網(wǎng)絡(luò)值
x5u X.509 URL
x5c X.509 證書鏈
x5t X.509 證書 SHA-1 指紋
x5t#S256 X.509 證書 SHA-256 指紋
crit 臨界值
前兩個(gè)參數(shù)是最常用的,所以典型的頭部看起來有點(diǎn)類似下面這樣:
{ "alg": "HS256", "typ": "JWT"}
上面列出的第三個(gè)參數(shù) kid
是基于安全原因使用的。cty
參數(shù)在另一方面應(yīng)該只被用于處理嵌套的 JWT。剩下的參數(shù)你可以在規(guī)范文檔中閱讀了解,我認(rèn)為它們不適合在這篇文章中被提及。
alg
參數(shù)的值可以是 JSON 網(wǎng)絡(luò)算法(JWA)中的任意指定值 - 這是我所知道的另一個(gè)規(guī)范。下面是 JWS 的注冊(cè)列表:
HS256 - HMAC 使用 SHA-256 算法
HS384 - HMAC 使用 SHA-384 算法
HS512 - HMAC 使用 SHA-512 算法
RS256 - RSASSA-PKCS1-v1_5 使用 SHA-256 算法
RS384 - RSASSA-PKCS1-v1_5 使用 SHA-384 算法
RS512 - RSASSA-PKCS1-v1_5 使用 SHA-512 算法
ES256 - ECDSA 使用 P-256 和 SHA-256 算法
ES384 - ECDSA 使用 P-384 和 SHA-384 算法
ES512 - ECDSA 使用 P-521 和 SHA-512 算法
PS256 - RSASSA-PSS 使用 SHA-256 和基于 SHA-256 算法的 MGF1
PS384 - RSASSA-PSS 使用 SHA-384 和基于 SHA-384 算法的 MGF1
PS512 - RSASSA-PSS 使用 SHA-512 和基于 SHA-512 算法的 MGF1
none - 沒有數(shù)字簽名或 MAC 執(zhí)行
請(qǐng)注意最后一個(gè)值 none
,從安全性的角度來看這是最有趣的。這是已知的被用來進(jìn)行降級(jí)防御攻擊的方法。它是如何工作的呢?想象一個(gè)客戶端生成的帶有一些聲明的 JWT 。它在頭部指定 none
值的簽名算法并進(jìn)行發(fā)送驗(yàn)證。如果攻擊者比較單純,那么它會(huì)使 alg
參數(shù)為真來確保被授權(quán)通過,然而實(shí)際上則是不會(huì)被允許的。
底線是,你的應(yīng)用的安全層應(yīng)該總是對(duì)頭部的 alg
參數(shù)進(jìn)行校驗(yàn)。那里就是 kid
參數(shù)用的上的地方。
這一個(gè)參數(shù)非常簡(jiǎn)單。如果它是已知的,那么它就是 JWT,因?yàn)閼?yīng)用不會(huì)去索取其他的值,如果這個(gè)參數(shù)沒有值就會(huì)被忽視掉。因此它是可選的。如果需要被指定值,它應(yīng)該按大寫字母拼寫 - JWT
。
在某些情況下,當(dāng)應(yīng)用程序接受到?jīng)]有 JWT 類型的請(qǐng)求卻又包含了 JWT 時(shí),去重新指定它是很重要的,因?yàn)檫@樣應(yīng)用程序才不會(huì)崩潰。
如果你的應(yīng)用程序中的安全層只使用了一個(gè)算法來簽名 JWTs,你不用太擔(dān)心 alg
參數(shù),因?yàn)槟銜?huì)總是使用相同的密鑰和算法來校驗(yàn)令牌的完整性。但是,如果你的應(yīng)用程序使用了一堆不同的算法和密鑰,你就需要能夠分辨出是由誰簽署的令牌。
正如我們之前看到的,單獨(dú)依靠 alg
參數(shù)可能會(huì)導(dǎo)致一些...不便。然而,如果你的應(yīng)用維護(hù)了一個(gè)密鑰/算法的列表,并且每一對(duì)都有一個(gè)名稱(id),你可以添加這個(gè)密鑰 id 到頭部,這樣在之后驗(yàn)證 JWT 時(shí)你會(huì)有更多的信心去選擇算法。這就是頭部參數(shù) kid
- 你的應(yīng)用中用來簽名令牌所使用的密鑰 id 。這個(gè) id 是由你來任意指定的。最重要的是 - 這是你給的 id ,所以你可以驗(yàn)證。
這里把規(guī)范介紹的很清楚,所以這里我就只是引用了:
在通常情況下,在不使用嵌套簽名或是加密操作時(shí),是不推薦使用這個(gè)頭部參數(shù)的。而在使用嵌套簽名或加密時(shí),這個(gè)頭部參數(shù)必須存在;在這種情況下,它的值必須是 "JWT",來表明這是一個(gè)在 JWT 中嵌套的 JWT。雖然媒體類型名字對(duì)大小寫并不敏感,但這里為了與現(xiàn)有遺留實(shí)現(xiàn)兼容還是推薦始終用 "JWT" 大寫字母來拼寫。
"claims" 這個(gè)名稱是否讓你感到困惑?在最初它也確實(shí)讓我很困惑。我相信你需要重復(fù)讀幾次來嘗試適應(yīng)它。簡(jiǎn)而言之,claims 是 JWT 的主要內(nèi)容 - 是我們十分關(guān)心的簽名的數(shù)據(jù)。它被叫做 "claims" 是因?yàn)橥ǔK褪锹暶鬟@個(gè)意思 - 客戶端聲明了用戶名,用戶角色或者其他什么的來讓它可以獲得對(duì)資源的訪問。
還記得我在最開始提到的那個(gè)可愛的故事嗎?你的公民資格就是你的聲明而你的護(hù)照則就是 - JWT
你可以在聲明中放置任何你想要的參數(shù),這兒有一個(gè)注冊(cè)表應(yīng)當(dāng)被視為公認(rèn)的參考實(shí)現(xiàn)方法。請(qǐng)注意這里的每一個(gè)參數(shù)都是可選的并且大多數(shù)是應(yīng)用程序特定的,下面就是這個(gè)列表:
exp - 過期時(shí)間
nbf - 有效起始日期
iat - 發(fā)行時(shí)間
sub - 主題
iss - 發(fā)行者
aud - 受眾
jti - JWT ID
值得注意的是,除了最后三個(gè)(issuer ,audience 和 JWT ID)參數(shù)通常是在更復(fù)雜的情況下(例如包含多個(gè)發(fā)行者時(shí))才被使用。下面讓我們來討論一下它們吧。
exp
是時(shí)間戳值表示著在什么時(shí)候令牌會(huì)失效。規(guī)范上要求"當(dāng)前日期/時(shí)間"必須在指定的 exp
值之前,從而保證令牌可以得到處理。這里也表明了存在一些余地(幾分鐘)來應(yīng)對(duì)時(shí)間差。
nbf
是時(shí)間戳值表示著在什么時(shí)候令牌開始生效。規(guī)范上要求"當(dāng)前日期/時(shí)間"必須與指定的 nbf
值相等或在其之后,從而保證令牌可以得到處理。這里也表明了存在一些余地(幾分鐘)來應(yīng)對(duì)時(shí)間差。
iat
是時(shí)間戳值表示什么時(shí)候令牌被發(fā)行。
sub
在規(guī)范上被要求"是JWT 中的聲明中通常用于陳述主題的值"。這里主題必須是內(nèi)容中唯一的發(fā)行者或全局上的唯一值。sub
聲明可以用來鑒別用戶,例如 JIRA 文檔上那樣。
iss
是被用來確認(rèn)令牌的發(fā)行者的字符串值。如果值中包含 :
那么它就是一個(gè) URI。如果有很多的發(fā)行者而在一個(gè)安全層中應(yīng)用程序需要去識(shí)別發(fā)行人時(shí),它將會(huì)是有用的。例如 Salesforce 要求了去使用 OAuth client_id 來作為 iss
的值。
aud
是被用來確認(rèn)令牌的可能接受者的字符串值或數(shù)組。如果值中包含 :
那么它就是一個(gè) URI。 通常使用 URI 資源的聲明是有效的。例如,在 OAuth 中,接受者是授權(quán)服務(wù)器。應(yīng)用程序處理令牌時(shí),在針對(duì)不同的接受者的情況下,必須驗(yàn)證接受者是否是正確的或者拒絕令牌。
令牌的唯一標(biāo)識(shí)符。每個(gè)發(fā)布的令牌的 jti
必須是唯一的,即使有很多發(fā)行人也是一樣。jti
聲明可以用于一次性的不能重放的令牌。
在最常見的場(chǎng)景中,客戶端的瀏覽器將在認(rèn)證服務(wù)中認(rèn)證并接受返回的 JWT。然后客戶端用某種方式(如內(nèi)存,localStorage)存儲(chǔ)這個(gè)令牌并與受保護(hù)的資源一起發(fā)送返回。通常令牌發(fā)送時(shí)是作為 cookie 或是 HTTP 請(qǐng)求中 Authorization
頭部。
GET /api/secured-resource HTTP/1.1 Host: example.com Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoiSm9obiBEb2UiLCJhZG1pbiI6dHJ1ZX0.OLvs36KmqB9cmsUrMpUutfhV52_iSz4bQMYJjkI_TLQ
首選頭部方法是出于安全的原因 - cookies 會(huì)很容易受 CSRF (跨站請(qǐng)求偽造)的影響,除非 CSRF 令牌是使用過的。
其次,cookies 只能發(fā)送返回到被發(fā)出的相同的域下(或者最多二級(jí)域下)。如果身份驗(yàn)證服務(wù)駐留在不同的域下,那么 cookies 得需要更強(qiáng)烈的創(chuàng)造性才行。
因?yàn)闆]有 session 數(shù)據(jù)存儲(chǔ)在服務(wù)端了,所以不能再通過破壞 session 來注銷了。因此登出成為了客戶端的職責(zé) - 一旦客戶丟失了令牌不能再被授權(quán),就可以被認(rèn)為是登出了。
我認(rèn)為 JWTs 是一個(gè)在脫離 sessions 的情況下非常聰明的授權(quán)方式。它允許創(chuàng)建真正的服務(wù)端無狀態(tài)的基于 RESTful 的服務(wù),這也意味著不需要 session 存儲(chǔ)。
與瀏覽器自動(dòng)發(fā)送 session cookie 到任意匹配域/路徑組合(老實(shí)說,在大多數(shù)情況下這里只有域的情況)的 URL 不一樣的是,JWTs 可以選擇性的只向需要身份授權(quán)的資源來發(fā)送。
對(duì)于客戶端和服務(wù)端來說,它的實(shí)現(xiàn)非常簡(jiǎn)單,特別是已經(jīng)有專門的庫(kù)來制造簽名和驗(yàn)證令牌了。
感謝閱讀!
如果你喜歡這篇文章的話,歡迎分享它。同樣也十分歡迎你對(duì)它進(jìn)行評(píng)論!
24小時(shí)免費(fèi)咨詢
請(qǐng)輸入您的聯(lián)系電話,座機(jī)請(qǐng)加區(qū)號(hào)