一、概述
JWT 全称为 JSON Web Token,是一份开源的标准协议,它定义了一种传输内容基于 JSON、轻量级、安全的数据传输方式。
二、细节
每个 JWT 都由 Header、Payload、Signature 3 部分组成,同时用点进行拼接,形式如下:
代码语言:txt复制Header.Payload.Signature
Header
Header 部分是一个经过 Base64 编码后的 JSON 对象。对象的内容通常包括 2 个字段,形式如下:
代码语言:txt复制{
"typ": "JWT",
"alg": "HS256"
}
其中,typ(全称为 type)指明当前的 Token 类型为 JWT,alg(全称为 algorithm)指明当前的签名算法是 HS256。
Payload
Payload 部分也是一个经过 Base64 编码后的 JSON 对象,对象的属性可以划分成 3 部分:保留字段、公共字段、私有字段。
保留字段是 JWT 内部声明,具有特殊作用的字段,包括
- iss(全称为 issuer),指明 JWT 是由谁签发的
- sub(全称为 subject),指明 JWT 的主题(也可理解为面向用户的类型)
- aud(全称为 audience),指明 JWT 希望谁签收
- exp(全称为 expiration time),指明 JWT 的过期时间,过期时间需大于签发时间
- nbf(全称为 not before time),指明 JWT 在哪个时间点生效
- iat(全称为 issued at time),指明 JWT 的签发时间
- jti(全称为 JWT ID),指明 JWT 唯一 ID,用于避免重放攻击
公共字段和私有字段都是用户可以任意添加的字段,区别在于公共字段是一些约定俗成,被普遍使用的字段,而私有字段更符合实际的应用场景。
当前已有的公共字段可以从 JSON Web Token Claims 中找到。
Payload 的结构形式如下:
代码语言:txt复制{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
Signature
Signature 部分是 JWT 根据已有的字段生成的,它的计算方式是使用 Header 中定义的算法,使用用户定义的密钥,对经过 Base64 编码后的 Header 和 Payload 组成的字符串进行加密,形式如下:
代码语言:txt复制HMACSHA256(base64(header) '.' base64(payload))
三、应用场景
业界普遍认可的应用场景主要有以下几种:
防止传输数据篡改
数据数据篡改指的是数据在传输过程中被截获,修改的行为。
JWT 本身可以使用加密算法对传输内容进行签名,即使数据被截获,也很难同时篡改签名和传输内容。
鉴权
鉴权指的是验证用户是否有访问系统的权利。
部分人使用 JWT 来取代传统的 Session Cookie,理由是:
- 服务器开销小。使用 Session Cookie 需要服务器缓存用户数据,而使用 JWT 则是直接将用户数据下发给客户端,每次请求附带一并发送给服务器。
- 扩展性好。服务器不缓存用户数据的好处是可以很方便的进行横向扩容
- 适用于单点登录。JWT 很适合做跨域情况下的单点登录
- 适用于搭配 RESTFul API 使用。基于 RESTFul 架构设计的 API 需遵循 RESTFul 的无状态原则,而基于 JWT 的鉴权恰恰是把状态转移到了客户端
基于 JWT 的鉴权一般处理逻辑是:
基于 JWT 的鉴权方案也存在一些争议:
- 服务器签发 JWT 后,并不能主动注销,若存在恶意请求则很难制止。其实可以通过 Token 黑名单的方式去解决。
- JWT 减少了服务器的开销,却增加了带宽的开销,JWT 生成的 Token 在体积上比 SessionID 大很多,意味着每次请求相比之前要携带更多的数据量。这个确实是这样,所以应该尽量只在 JWT 内放必要的数据。
- JWT 在鉴权方面并非完全优于 Session-Cookie,举个例子,SessionID 也可以通过签名的方式来防止篡改。
四、使用
以下使用 Node.js 和 JavaScript 演示 JWT 在鉴权方面的应用,涉及的库有:
- koa
- jsonwebtoken
- axios
如何生成 Token
Token 的生成一般是客户端发送登录请求,服务器使用密钥生成 Token 并放入响应体中,以下为服务端的 Token 生成逻辑。
代码语言:txt复制// 文件位置:controller/v1/token.js
const config = require('config') // 加载服务器配置
const jwt = require('jsonwebtoken') // 加载 jwt Node.js 语言实现
/**
* 创建 Token 控制器
* @param {Object} ctx 请求上下文
*/
async function create(ctx) {
const username = ctx.request.body.username
const password = ctx.request.body.password
if (!username || !password) {
ctx.throw(400, '参数错误')
return
}
// 省略:用户名密码数据库校验
const user = { id: '5e54c02a2b073de564fe8034' } // 用户信息
const secret = config.get('secret') // 获取保存于配置中的密钥
const opt = { expiresIn: '2d' } // 设置 Token 过期时间为 2 天
ctx.body = jwt.sign(user, secret, opt) // 生成并返回 token
}
module.exports = {
create,
}
客户端携带 Token 进行请求
客户端一般情况下将 Token 放在 Http Header 的 Authorization 中,随请求发送给服务器。
代码语言:txt复制// 文件位置:views/index.pug
var request = axios.create({ baseURL: '/api/v1' }) // 创建请求实例
var token // 为了方便这里使用全局变量,正常情况下应该放入其他存储介质中,如,localStorage,此处省略获取逻辑
// 监听正常请求按钮单击事件,发起请求
document.querySelector('#normal').addEventListener('click', function() {
if (!token) {
alert('请登录')
return
}
request.get('/users', {
headers: {
Authorization: 'Bearer ' token, // 绑定 token 到 header 中
},
}).then(function({ data }) {
document.querySelector('#response').innerHTML = JSON.stringify(data)
}).catch(function(err) {
console.log('Request Error: ', err)
})
})
服务器如何验证 Token
验证操作一般放在服务器的中间件。
代码语言:txt复制const config = require('config') // 加载服务器配置
const jwt = require('jsonwebtoken') // 加载 jwt Node.js 语言实现
// 定义中间件函数
module.exports = async (ctx, next) => {
const path = ctx.url // 获取请求 URL
const method = ctx.method.toLowerCase() // 获取请求方法
// 请求白名单,白名单中的请求不经过中间件 token 校验
const whiteList = [
{ path: /^/api/v[1-9]/tokens/, method: 'post' },
{ path: /^/api/, reverse: true }, // 非 /api 开头的资源都不需要经过请求校验
]
// 请求白名单检查函数
const checker = (i) => {
const matchPath = i.path.test(path)
const matchMethod = i.method ? i.method === method : true
return (i.reverse ? !matchPath : matchPath) && matchMethod
}
// 白名单逻辑判断
if (whiteList.some(checker)) {
await next()
return
}
// 获取 http header 中的 token
const token = (ctx.header.authorization || '').replace('Bearer ', '')
// token 有效性校验
try {
const data = jwt.verify(token, config.secret)
ctx.userInfo = data
} catch (e) {
ctx.throw(400, 'Token 错误')
}
await next()
}
查看完整代码请前往 GitHub 搜索用户 yo-squirrel
觉得写得不错可以关注下微信公众号「松鼠专栏」