JWT 到底应该怎么用才对?

2020-04-02 11:02:02 浏览数 (1)

一、概述

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

觉得写得不错可以关注下微信公众号「松鼠专栏

0 人点赞