我用 express 实现会话控制之 - JWT

395 阅读6分钟

前言

同学,你好!我是 嘟老板《前端同学应该了解的 “会话控制”》 中涉及的三种实现会话控制的方式,目前已经实现了两种:CookieSession。今天我们继续实现最后一种,也是比较常用的一种方式 - JWT

阅读本文您将收获:

  1. 了解 express 项目中,JWT 实现会话控制涉及的依赖及应用。
  2. 了解 express 中通过 JWT 实现会话控制的详细流程及代码细节。
  3. 了解 express Router 的用法及接口定义。

准备工作

本文代码基于 我用 express 实现会话控制之 - Session 中现有项目编写,想动手实操的同学,可以阅读前两篇文章或直接 GitHub 拉取。

创建 jwt 目录结构

项目根目录下执行以下命令:

cd src
mkdir jwt
cd jwt
touch index.js

以上命令创建了 jwt 目录 及 index.js,执行后项目结构如下:

安装依赖

终端输入以下命令,回车执行:

pnpm add jsonwebtoken -S

安装成功后,package.json 文件中的 dependencies 如下:

image.png

创建 jwt 路由

我们将 JWT 相关的路由封装进统一的 Router,便于管理,比如设置相同的接口前缀。

JWT 相关代码全部在 jwt/index.js 文件中编写。

创建并导出一个 Router 实例

jwt/index.js 中编写以下代码,创建并导出 jwtRouter

const express = require('express')
const router = express.Router()

module.exports = router

在根目录 index.js 中安装 jwtRouter,设置 jwt 接口 url 前缀为 /jwt

const jwtRouter = require('./src/jwt')

// 安装 Jwt Router
app.use('/jwt', jwtRouter)

登录接口

登录接口主要逻辑如下:

  1. 验证登录用户是否存在。
  2. 若不存在,报错 “用户不存在”,退出。
  3. 若存在,验证客户端传递的用户名、密码是否正确。
  4. 若不正确,报错 “用户名或密码错误”。
  5. 若正确,生成 jwt,保存到浏览器 cookie 中。
  6. 渲染“登录成功”页面。

我们继续使用现有的 users.js 中的用户列表。

const jwt = require('jsonwebtoken')
const users = require('../../users')

// jwt 加密 key
const SECRET_KEY = 'dulaoban'
// Cookie key
const COOKIE_KEY = 'jwt'

// 登录接口
router.post('/login', (req, res) => {
  const { username, password } = req.body

  // 校验用户是否存在
  const user = users[username]
  if (!user) {
    res.send('用户不存在')
    return
  }
  // 验证用户名和密码是否正确
  if (username === user.username && password === user.password) {

    // 生成 jwt
    const token = jwt.sign({ username }, SECRET_KEY, {
      expiresIn: 60 * 60
    })

    // 将 jwt 存储到 cookie 中
    res.cookie('jwt', token, {
      httpOnly: true,
      maxAge: 60 * 1000
    })
    res.send('<h1>登录成功</h1><a href="/jwt/logout">Logout</a>')
  } else {
    res.send('账号名或密码错误')
  }
})

接口中使用 jwt.sign 生成 token。该函数接受的参数如下:

  • payload:生成 jwt 的内容,可以是对象、Buffer 或字符串。
  • secretOrPrivateKey:用于生成 jwt 算法的秘钥。
  • options:(可选)配置项,如 algorithm(算法,默认 HS256),expiresIn(过期时间)等。
  • callback:(可选)用于异步生成 jwt 场景,参数接收 errtoken

接口中将生成的 jwt 保存到 cookie,主要是考虑到方便存取和传递。在实际项目中,交由前端保存到本地缓存(localStorage/sessionStorage)中,通过请求头 Authorization 标头传递也可以。

测试一下

  1. 启动服务

终端执行 pnpm start,显示以下内容即启动成功。

image.png

  1. 打开登录页面,http://localhost:3000/login?authType=jwt ,输入用户名/密码 dulaoban/123456,点击 Login

image.png

  1. 登录成功

image.png

  1. 查看浏览器控制台,确认是否生成 jwt Cookie

image.png

OK,运行正常,初步测试通过。

登出接口

登出接口主要逻辑如下:

  1. 验证当前 jwt 是否有效。
  2. 若已失效,则无需处理,直接 “登出成功“。
  3. 若未失效,则移除 jwt cookie,并将该 jwt 添加到黑名单,以使其后续不再可用。
  4. 登出成功。
// token 黑名单
const BLACK_LIST = []
// 登录链接
const LOGIN_LINK = '<a href="/login?authType=jwt">前往登录</a>'

// 登出
router.get('/logout', (req, res) => {
  const token = req.cookies[COOKIE_KEY]
  jwt.verify(token, SECRET_KEY, (err, payload) => {
    if (err) {
      res.send(`<h1>登出成功</h1>${LOGIN_LINK}`);
      return
    }
    // 清除 jwt cookie
    res.clearCookie(COOKIE_KEY)
    // 加入黑名单
    !BLACK_LIST.includes(token) && BLACK_LIST.push(token)
    res.send(`<h1>登出成功</h1>${LOGIN_LINK}`);
  })
})

接口中使用 jwt.verify 校验 token 是否有效,若恰好当前 token 已失效,就无需后续处理,直接退出成功。

方便理解,接口中使用 BLACK_LIST 数组作为 token 黑名单容器,实际项目中建议持久化到数据库中,定期清理。

为什么需要黑名单?

退出登录时,若 token 仍在有效期内,后续使用该 token 向服务端发送请求仍然可以通过验证,有了黑名单的限制,就可以避免这种漏洞情况,进一步提升安全性。

业务接口,对标真实项目接口鉴权逻辑

业务接口要保证用户有权限的前提下,执行业务逻辑

我们定义一个 helloWorld 的接口,若鉴权通过,则返回 Hello World,否则跳转登录页面。

// 业务接口,测试
router.get('/helloWorld', (req, res, next) => {
  const token = req.cookies[COOKIE_KEY]
  // 若请求为携带 token,或 token 已加入黑名单,视为”未登录“状态
  if (!token || BLACK_LIST.includes(token)) {
    res.send(`<h1>请先登录</h1>${LOGIN_LINK}`)
    return
  }

  jwt.verify(token, SECRET_KEY, (err, payload) => {
    // 若 token 已失效,重新登录
    if (err) {
      res.send(`<h1>请先登录</h1>${LOGIN_LINK}`)
      return
    }
    // 校验 token 内用户是否存在
    const username = payload.username
    const user = users[username]
    if (!user) {
      res.send(`<h1>用户不存在,请重新登录</h1>${LOGIN_LINK}`)
      return
    }
    next()
  })
}, (req, res) => {
  res.send('Hello world')
})

经验证,登录状态下可正常渲染 Hello World;退出登录后,浏览器访问 http://localhost:3000/jwt/helloWorld ,会提示 ”请先登录“,符合预期。

权限验证中间件

helloWorld 接口定义中验证 token 的函数参数,我们称之为 中间件函数,可用来控制接口的执行走向。

在实际项目中,需要验证权限的接口会有很多很多,如果都像上面 helloWorld 接口那样写,一但需要校验,就在对应接口写一个验证中间件函数,相当麻烦。后续若有调整,也必是毁灭式的灾难。

我们可以将 token 验证的逻辑抽离出来,单独维护,后续有接口需要鉴权,只要将该中间件函数引入使用即可。

middlewares 目录下新增 checkJwtAuth.js 文件,将鉴权逻辑写入并导出:

const jwt = require('jsonwebtoken')
const users = require('../users')
const { LOGIN_LINK, SECRET_KEY, COOKIE_KEY, BLACK_LIST } = require('../src/jwt/constants')

function checkJwtAuth (req, res, next) {
  const token = req.cookies[COOKIE_KEY]
  // 若请求为携带 token,或 token 已加入黑名单,视为”未登录“状态
  if (!token || BLACK_LIST.includes(token)) {
    res.send(`<h1>请先登录</h1>${LOGIN_LINK}`)
    return
  }

  jwt.verify(token, SECRET_KEY, (err, payload) => {
    // 若 token 已失效,重新登录
    if (err) {
      res.send(`<h1>请先登录</h1>${LOGIN_LINK}`)
      return
    }
    // 校验 token 内用户是否存在
    const username = payload.username
    const user = users[username]
    if (!user) {
      res.send(`<h1>用户不存在,请重新登录</h1>${LOGIN_LINK}`)
      return
    }
    next()
  })
}

module.exports = checkJwtAuth

然后在 jwt/index.js 引入,并传入到 helloWorld 定义函数:

const checkJwtAuth = require('../../middlewares/checkJwtAuth')

// 业务接口,测试
router.get('/helloWorld', checkJwtAuth, (req, res) => {
  res.send('Hello world')
})

后续若有新接口需要鉴权,以相同的方式使用就 OK 啦。

结语

本文重点介绍了基于 express 框架,通过 JWT 实现会话控制的全过程。从项目基础结构开始,到路由定义,再到 JWT 处理逻辑以及权限验证,旨在帮助同学们加深对于 JWT 应用的理解。相关代码已上传至 GitHub,喜欢的同学欢迎 star。会话控制篇章已完结,感兴趣的小伙伴可以看下前几篇文章,理论结合实践,效果更好哦。

如果您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。

技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。


往期推荐