【摘要】介绍什么是 JWT,它的结构、如何创建、如何使用。
JWT ( JSON Web Token ) 是一种数据格式,它解决的是客户端传给服务器的数据不可信的问题。
假设客户端登录后,服务器返回一个凭证 token,它是一个 JSON 字符串:
{
"name": "foo",
"expire": "2024-1-1",
"permission": ["read", "write"]
}
客户端可以用这个 token 请求资源,如果 token 过期了,客户端只需修改其中的 expire 字段即可,服务器无法判断凭证是否曾被修改或伪造。
如何保证 token 不可被修改呢?答案是利用签名。
这个过程可以分为 5 步:
- 用私钥 key 对 token 执行摘要算法,得到签名。
- 将 token 和签名一起返回给客户端。
- 客户端传递 token 和签名给服务器。
- 服务器对传过来的 token 和 key 再计算一次摘要。
- 对比两个签名,签名相同表示则 token 没有被篡改。
这里关键的地方在于,key 是服务器私有的,客户端无法得到,进而无法伪造签名,从而保证 token 数据是可信的。
以上过程和 JWT 有什么关系?
JWT 就是上述过程中传递的 (token, signature)的标准化结构。
JWT 的数据结构
JWT 是一个字符串,包含 3 部分,用 . 号分开。
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJleHAiOiIyMDI0LTA5LTEzVDE0OjI0OjEzLjE0N1oiLCJuYW1lIjoiZm9vIiwiYWRtaW4iOnRydWV9.
ZjI0M2RiNmM1YTkxMDRjZjU4ODQxNzNjMTVkOGM0Mzk2ODVkMDc5N2M1NTQ3MmU4YzgyYTk5MjM4Yzc0M2M4Ng==
.号将 JWT 分成 header、payload、signature 三部分,它们都是 base64 字符串,转码后方便阅读。
{"alg":"HS256","typ":"JWT"}.
{"exp":"2024-09-13T14:24:13.147Z","name":"foo","admin":true}.
"f243db6c5a9104cf5884173c15d8c439685d0797c55472e8c82a99238c743c86"
下面依次介绍这 3 部分。
header
header 部分是一个 JSON 字符串,用来描述 JWT 的元数据。
{"alg":"HS256","typ":"JWT"}
其中,alg 表示签名的算法,默认是 HMAC SHA256,简写成 HS256;typ 表示 token 类型为 JWT。
payload
payload 部分也是 JSON,用来存放实际传递的数据。
JWT 规定了 7 个官方字段,可供选用。
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
除了官方字段,你还可以自定义字段。
{
"exp":"2024-09-13T14:24:13.147Z",
"name":"foo",
"admin":true
}
上面代码中的 name 和 admin 都是自定义的。
注意,JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。
signature
signature 是对 header 和 payload 的签名,防止数据篡改。
将这两部分转成 base64 字符串后进行签名即可。
signature = HMACSHA256(
base64(header) + '.' + base64(payload),
key
)
JWT 如何使用
JWT 通常会在 HTTP 协议的 header 中传递。
Authorization : Bearer [jwt_str]
其中,Bearer 是 HTTP 协议的授权类型
Authorization: <type> <authorization-parameters>
其他授权类型有:
- Basic:用于 http-basic 认证;
- Bearer:常见于 OAuth 和 JWT 授权;
- Digest:MD5 哈希的 http-basic 认证 ( 已弃用 )
- AWS4-HMAC-SHA256:AWS 授权
最后,若把 JWT 放在 URL 中,Base64 有三个字符 +/=在 URL 里有特殊含义,所以要被替换。+ 替换成 -,/ 替换成 _,= 被省略、。
JWT 的代码实现
下面用 Node.js 简单实现 JWT 的创建和检验(忽略 base64URL)。
JWT 包含 3 部分,signature 是对其他两部分的签名,签名的 KEY 保存在服务器中。
function jwt(header, payload, key) {
const header64 = toBase64(JSON.stringify(header))
const payload64 = toBase64(JSON.stringify(payload))
const sign64 = toBase64(
sign(`${header64}.${payload64}`, 'sha256', key)
)
return `${header64}.${payload64}.${sign64}`
}
function toBase64(str) {
return Buffer.from(str).toString('base64')
}
用 Node.js 的 crypto 模块执行 sha256 摘要算法,即签名。
const crypto = require('crypto')
function sign(info, algm = 'sha256', key) {
const hmac = crypto.createHmac(algm, key)
hmac.update(info)
return hmac.digest('hex')
}
注意签名使用的 KEY 保存在服务器上,不能交给客户端。
const KEY = '12345'
const header = { alg: 'HS256', typ: 'JWT' }
const payload = {
exp: new Date(), // 官方字段
// 自定义字段
name: 'foo',
admin: true,
}
const _jwt = jwt(header, payload, KEY)
验证 JWT 只需对客户端提交的 header 和 payload 再进行一次签名,检查两次签名是否一致。
function isVerified(jwt) {
const [header, payload, signature] = jwt.split('.')
const _signature = sign(`${header}.${payload}`, 'sha256', KEY)
return toBase64(_signature) === signature
}
isVerified(_jwt) // true
客户端对 JWT 进行任何改动,都会使得签名和数据对应不上。
const fakePayload = { name: 'hacker' }
const [header64, payload64, sign64] = _jwt.split('.')
// 伪造信息
const fakeJwt = `${header64}.${toBase64(
JSON.stringify(fakePayload)
)}.${sign64}`
// 伪造信息,伪造签名
const fakeKey = 'xxxxx'
const fakeJwt2 = jwt(header, fakePayload, fakeKey)
isVerified(fakeJwt) // false
isVerified(fakeJwt2, KEY) // false