Web 安全 - 同事告訴我 JWT 是明文的---
一天 “小張” 接到一個需求 “一旦用戶登陸認證成功之後,後續的請求可以攜帶一個令牌,無需再次身份認證”。
這時 “小張” 諮詢了資深搬磚工程師 “小李”,憑藉多年的搬磚經驗,同事 “小李” 說到:HTTP 協議是無狀態的,在第一次登陸認證成功後,下一次請求時,服務器也不知道請求者的身份信息。通常有兩種實現方式:
-
一種傳統的做法是在服務器上存儲用戶 session 信息,每次請求時攜帶 sessionID 進行驗證,這種方式缺點是會佔用服務器內存,當用戶越來越多會增加服務器的內存開銷、由於存儲在內存還會帶來擴展性問題。
-
第二種方法是採用 JWT 技術,它是一種無狀態的身份驗證。只做校驗,將用戶狀態分散到了客戶端,服務器端不會進行信息存儲。
“小張” 聽完後,連忙說到第二種聽着不錯哦,搜索了一些相關文章介紹之後就開始了愉快的代碼編寫。完成之後提交了代碼給同事 “小李” 做 code review,做爲資深搬磚工程師的 “小李”,一眼看出了問題:“怎麼能在 JWT 生成的 token 裏放用戶密碼呢!JWT 默認是明文的,不能存儲隱私信息”。
“小張” 不解,反問道:怎麼會是明文呢,加密之後的數據我看了的,是一堆亂碼啊,下面是打印的 token 信息。
// jwt 簽名後生成的 token
"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IuW8oOS4iSIsInBhc3N3b3JkIjoxMjM0NTYsImlhdCI6MTY2MTg2OTQxMX0.3-60HUf_cKIo44hWUviNzqdUoUGngGQfrqffg0A6uqM"
“小李” 通過一段 Node.js 代碼展示瞭如何解密出 JWT 簽名後的 token 數據。
此時的 “小張” 陷入了沉思,頓時心裏產生了兩個疑問🤔️:
-
簽名時使用了 secret 了,生成的 token 看着就是一串亂碼的字符啊,爲什麼是明文呢?
-
按照上面這樣解析 token 中籤名的數據,數據會不會被篡改呢?
帶着這兩個疑問,下一步讓我們一塊瞭解下 JWT 的原理。
JWT 原理
JWT 全稱 JSON Web Token,是一種基於 JSON 的數據對象,通過技術手段將數據對象簽名爲一個可以被驗證和信任的令牌(Token)在客戶端和服務端之間進行安全的傳輸。
JWT Token 由三部分組成:header(頭信息)、payload(消息體)、signature(簽名),之間用 .
鏈接,構成如下所示:{ typ, alg }
兩部分構成,使用 base64url(header)
算法轉爲字符串:
-
typ:表示令牌類型,JWT 令牌統一寫爲
JWT
-
alg:簽名算法,默認爲
HS256
,支持的算法爲['RS256', 'RS384', 'RS512', 'ES256', 'ES384', 'ES512', 'HS256', 'HS384', 'HS512', 'none']
Payload 部分爲消息體,用來存儲需要傳輸的數據,同樣也是一個 JSON 對象使用base64url(payload)
算法轉爲字符串,JWT 提供了 7 個可選字段供選擇,也可以自定義字段:
-
iss (issuer):簽發人
-
exp (expiration time):過期時間
-
sub (subject):主題
-
aud (audience):受衆
-
nbf (Not Before):生效時間
-
iat (Issued At):簽發時間
-
jti (JWT ID):編號
**Signature 是對 Header、Payload 兩部分數據按照指定的算法做了一個簽名,防止數據被篡改。**需要指定一個 sceret,產生簽名的公式如下:
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
your-256-bit-secret
)
生成簽名後,將 header.payload.signature
三部分鏈接在一起,形成一個令牌(token)返回給客戶端。
問題答疑
這就是 JWT 的原理,瞭解之後並沒有那麼神祕,回答上面的幾個問題。
簽名時使用了 secret,爲什麼是明文?
header、payload 部分是使用 base64 算法進行的編碼,並沒有被加密,自然也可以被解碼。但注意這裏的 base64 算法有點不一樣的地方在於,token 可能會被放在 url query 中傳輸,URL 裏面有三個特殊字符會被替換。下面是 JWT 中 base64url 的實現方式:
// 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 對象放置的內容越多,base64 之後的字符串就越大,同理簽名後的 token 也一樣。
數據會不會被篡改?
數據一旦被篡改,到服務端也會認證失敗的,服務端在生成簽名時有一個重要的參數是 secret,只要保證這個密鑰不被泄漏,就沒問題,就算篡改也是無效的。
Node.js 示例演示
在 Node.js 中使用 JWT 需要用到 jsonwebtoken 這個庫,API 很簡單,主要用到兩個方法:
-
sign():生成簽名
-
verify():驗證簽名
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)
總結
JWT 由服務端生成可以存儲在客戶端,對服務端來說是無狀態的,可擴展性好。
上文我們也講了 JWT 中傳輸數據的 payload 默認是使用 base64 算法進行的編碼,看似一串亂碼,實則是沒有加密,因此不要將涉及到安全、用戶隱私的數據存放在 payload 中,如果要存放也請先自己進行加密。
一旦 token 泄漏,任何人都可以使用,爲了減少 token 被盜用,儘可能的使用 HTTPS 協議傳輸,token 的過期時間也要設置的儘可能短。
防止數據被篡改,服務端密鑰(secret)很重要,一定要保管好。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/eVaHk_YVxtmNs470p02I2g