jwt 的组成
jwt 由以下三个部分组成:
-
头部(header)
用于描述元信息,例如加密算法等:
{ typ: 'JWT', // 类型 JWT,固定的 alg: 'HS256', // 哈希算法,例如 HS256、RS512 等 }
-
载荷(payload)
这是 jwt 的主体部分,包含三个部分:
- 标准声明(Registered Claim Names)
- 公共声明(Public Claim Names)
- 私有声明(Private Claim Names)
标准声明有:
iss
签发者sub
面向的用户aud
接收方exp
有效期nbf
此时间前不可用iat
颁发时间jti
唯一标识,防止重复使用
这些声明是建议使用但并不强制要求。
公共的声明可以添加任何信息,一般添加用户的相关信息或其他业务需要的必要信息,例如 userId 等,但是不要添加敏感信息,因为 base64 是对称解密的,放在载荷里面的可以归类为明文信息。
私有的声明提供者和消费者所共同定义的声明,同样不应该存放敏感信息。
-
签名(signature)
签名由 header、payload 和密钥计算而来,算法如下:
signature = sign(base64(header) + '.' + base64(payload), secret)
也就是说把头部和载荷先转成 base64 编码,用 . 拼接起来,然后按照头部指定的算法进行加密。但是这里要注意,要把 base64 编码中的 3 个特殊字符做转换,确保传输过程中是 URL safe 的:
+
转换为中划线-
/
转换为下划线_
=
转换为空字符''
手写 jwt
这里写了一个工具类来进行 jwt 的生成、验证和解码:
const crypto = require('crypto')
const jwt = {
stringToBase64(str) { // 字符串转base64
return Buffer.from(str).toString('base64')
},
base64ToString(str) { // base64还原字符串
return Buffer.from(str, 'base64').toString()
},
stringToSafeBase64(str) { // 字符串转url安全的base64(即替换+/=三个字符)
return this.stringToBase64(str).replace(/\+/g, '-').replace(/\//g, '_').replace(/\=/g, '')
},
safeBase64ToString(str) { // url安全的base64还原字符串
str += new Array(5 - (str.length % 4)).join('=')
const base64 = str.replace(/\-/g, '+').replace(/_/g, '/')
return this.base64ToString(base64)
},
sign(content, secret) { // sha256签名
const str = crypto.createHmac('sha256', secret).update(content).digest('base64')
return this.stringToSafeBase64(str)
},
encode(header, payload, secret) { // jwt编码
const headerStr = this.stringToSafeBase64(JSON.stringify(header))
const payloadStr = this.stringToSafeBase64(JSON.stringify(payload))
const signature = this.sign(headerStr + '.' + payloadStr, secret)
return [headerStr, payloadStr, signature].join('.')
},
verify(token, secret) { // jwt验证
const [headerStr, payloadStr, signature] = token.split('.')
const newSignature = this.sign([headerStr, payloadStr].join('.'), secret)
return signature === newSignature
},
decode(token) { // jwt解码
const [headerStr, payloadStr] = token.split('.')
const header = JSON.parse(this.safeBase64ToString(headerStr))
const payload = JSON.parse(this.safeBase64ToString(payloadStr))
return { header, payload }
}
}
使用方法如下:
const header = {
typ: 'JWT',
alg: 'HS256',
}
const payload = {
iat: 1580601600000, // 颁发时间,例如 2020-02-02
exp: '2d', // 有效期,例如 2 天
uerId: '123456', // 用户id
}
const secret = '123456'
const token = jwt.encode(header, payload, secret)
这样就得到 token 了:
eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpYXQiOjE1ODA2MDE2MDAwMDAsImV4cCI6IjJkIiwidWVySWQiOiIxMjM0NTYifQ.ZWNVNzA5R0FybnZ2SjZkb1lweklDcUtJTWZYTnF4T2pleHI4RFFUQlBucz0
解码和验证:
console.log(jwt.decode(token))
console.log(jwt.verify(token, secret))
摘要算法
这里用到了 HS256 摘要算法,摘要算法又称哈希/散列算法,它通过一个函数,把任意长度的数据转换为一个长度固定的数据串。摘要一般用作验证内容的完整性,真实性。最常用的就是 md5 和 sha1,使用 crypto 模块进行 md5 加密非常简单,只有一句话:
crypto.createHash('md5').update('helloworld').digest('hex')
这里的 update()
方法可以追加内容字符串,追加后得到的摘要结果和上面得到的结果是一样的,例如:
crypto.createHash('md5').update('hello').update('world').digest('hex')
其中 digest 可以接收 latin1
、hex
或 base64
作为参数。
如果要计算 sha1,只需要把 md5
改成 sha1
即可。
这两种哈希算法都是不需要密钥的,而 sha256 属于 Hmac 算法,只要密钥发生了变化,即使同样的输入也会得到不同的签名,安全性更高。
crypto.createHmac('sha256', secret).update(content).digest('base64')