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