Web 安全 - 同事告訴我 JWT 是明文的...
一天 “小張” 接到一個(gè)需求 “一旦用戶登陸認(rèn)證成功之后,后續(xù)的請(qǐng)求可以攜帶一個(gè)令牌,無需再次身份認(rèn)證”。
這時(shí) “小張” 咨詢了資深搬磚工程師 “小李”,憑借多年的搬磚經(jīng)驗(yàn),同事 “小李” 說到:HTTP 協(xié)議是無狀態(tài)的,在第一次登陸認(rèn)證成功后,下一次請(qǐng)求時(shí),服務(wù)器也不知道請(qǐng)求者的身份信息。通常有兩種實(shí)現(xiàn)方式:
- 一種傳統(tǒng)的做法是在服務(wù)器上存儲(chǔ)用戶 session 信息,每次請(qǐng)求時(shí)攜帶 sessionID 進(jìn)行驗(yàn)證,這種方式缺點(diǎn)是會(huì)占用服務(wù)器內(nèi)存,當(dāng)用戶越來越多會(huì)增加服務(wù)器的內(nèi)存開銷、由于存儲(chǔ)在內(nèi)存還會(huì)帶來擴(kuò)展性問題。
- 第二種方法是采用 JWT 技術(shù),它是一種無狀態(tài)的身份驗(yàn)證。只做校驗(yàn),將用戶狀態(tài)分散到了客戶端,服務(wù)器端不會(huì)進(jìn)行信息存儲(chǔ)。
“小張” 聽完后,連忙說到第二種聽著不錯(cuò)哦,搜索了一些相關(guān)文章介紹之后就開始了愉快的代碼編寫。完成之后提交了代碼給同事 “小李” 做 code review,做為資深搬磚工程師的 “小李”,一眼看出了問題:“怎么能在 JWT 生成的 token 里放用戶密碼呢!JWT 默認(rèn)是明文的,不能存儲(chǔ)隱私信息”。
“小張” 不解,反問道:怎么會(huì)是明文呢,加密之后的數(shù)據(jù)我看了的,是一堆亂碼啊,下面是打印的 token 信息。
// jwt 簽名后生成的 token
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IuW8oOS4iSIsInBhc3N3b3JkIjoxMjM0NTYsImlhdCI6MTY2MTg2OTQxMX0.3-60HUf_cKIo44hWUviNzqdUoUGngGQfrqffg0A6uqM"
“小李” 通過一段 Node.js 代碼展示了如何解密出 JWT 簽名后的 token 數(shù)據(jù)。
此時(shí)的 “小張” 陷入了沉思,頓時(shí)心里產(chǎn)生了兩個(gè)疑問???:
- 簽名時(shí)使用了 secret 了,生成的 token 看著就是一串亂碼的字符啊,為什么是明文呢?
- 按照上面這樣解析 token 中簽名的數(shù)據(jù),數(shù)據(jù)會(huì)不會(huì)被篡改呢?
帶著這兩個(gè)疑問,下一步讓我們一塊了解下 JWT 的原理。
JWT 原理
JWT 全稱 JSON Web Token,是一種基于 JSON 的數(shù)據(jù)對(duì)象,通過技術(shù)手段將數(shù)據(jù)對(duì)象簽名為一個(gè)可以被驗(yàn)證和信任的令牌(Token)在客戶端和服務(wù)端之間進(jìn)行安全的傳輸。
JWT Token 由三部分組成:header(頭信息)、payload(消息體)、signature(簽名),之間用 .? 鏈接,構(gòu)成如下所示:
Header 部分由 JSON 對(duì)象 ?{ typ, alg }? 兩部分構(gòu)成,使用 base64url(header) 算法轉(zhuǎn)為字符串:
- typ:表示令牌類型,JWT 令牌統(tǒng)一寫為JWT
- alg:簽名算法,默認(rèn)為HS256?,支持的算法為['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none']
Payload 部分為消息體,用來存儲(chǔ)需要傳輸?shù)臄?shù)據(jù),同樣也是一個(gè) JSON 對(duì)象使用base64url(payload) 算法轉(zhuǎn)為字符串,JWT 提供了 7 個(gè)可選字段供選擇,也可以自定義字段:
- iss (issuer):簽發(fā)人
- exp (expiration time):過期時(shí)間
- sub (subject):主題
- aud (audience):受眾
- nbf (Not Before):生效時(shí)間
- iat (Issued At):簽發(fā)時(shí)間
- jti (JWT ID):編號(hào)
Signature 是對(duì) Header、Payload 兩部分?jǐn)?shù)據(jù)按照指定的算法做了一個(gè)簽名,防止數(shù)據(jù)被篡改。需要指定一個(gè) sceret,產(chǎn)生簽名的公式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
生成簽名后,將 header.payload.signature 三部分鏈接在一起,形成一個(gè)令牌(token)返回給客戶端。
問題答疑
這就是 JWT 的原理,了解之后并沒有那么神秘,回答上面的幾個(gè)問題。
簽名時(shí)使用了 secret,為什么是明文?
header、payload 部分是使用 base64 算法進(jìn)行的編碼,并沒有被加密,自然也可以被解碼。但注意這里的 base64 算法有點(diǎn)不一樣的地方在于,token 可能會(huì)被放在 url query 中傳輸,URL 里面有三個(gè)特殊字符會(huì)被替換。下面是 JWT 中 base64url 的實(shí)現(xiàn)方式:
// https://github1s.com/auth0/node-jws/blob/HEAD/lib/sign-stream.js#L9-L16
function base64url(string, encoding) {
return Buffer
.from(string, encoding)
.toString('base64')
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
}
還需要注意 payload 對(duì)象放置的內(nèi)容越多,base64 之后的字符串就越大,同理簽名后的 token 也一樣。
數(shù)據(jù)會(huì)不會(huì)被篡改?
數(shù)據(jù)一旦被篡改,到服務(wù)端也會(huì)認(rèn)證失敗的,服務(wù)端在生成簽名時(shí)有一個(gè)重要的參數(shù)是 secret,只要保證這個(gè)密鑰不被泄漏,就沒問題,就算篡改也是無效的。
Node.js 示例演示
在 Node.js 中使用 JWT 需要用到 jsonwebtoken 這個(gè)庫(kù),API 很簡(jiǎn)單,主要用到兩個(gè)方法:
- sign():生成簽名
- verify():驗(yàn)證簽名
const crypto = require('node:crypto');
const jwt = require('jsonwebtoken');
const secret = crypto.createHmac('sha256', 'abcdefg')
.update('')
.digest('hex');
const payload = {
"username": "張三",
"password": 123456,
iat: 1516239022
};
const token = jwt.sign(payload, secret)
const result = jwt.verify(token, secret)
總結(jié)
JWT 由服務(wù)端生成可以存儲(chǔ)在客戶端,對(duì)服務(wù)端來說是無狀態(tài)的,可擴(kuò)展性好。
上文我們也講了 JWT 中傳輸數(shù)據(jù)的 payload 默認(rèn)是使用 base64 算法進(jìn)行的編碼,看似一串亂碼,實(shí)則是沒有加密,因此不要將涉及到安全、用戶隱私的數(shù)據(jù)存放在 payload 中,如果要存放也請(qǐng)先自己進(jìn)行加密。
一旦 token 泄漏,任何人都可以使用,為了減少 token 被盜用,盡可能的使用 HTTPS 協(xié)議傳輸,token 的過期時(shí)間也要設(shè)置的盡可能短。
防止數(shù)據(jù)被篡改,服務(wù)端密鑰(secret)很重要,一定要保管好。