前言
同学,你好!我是 嘟老板。《前端同学应该了解的 “会话控制”》 中涉及的三种实现会话控制的方式,目前已经实现了两种:Cookie 和 Session。今天我们继续实现最后一种,也是比较常用的一种方式 - JWT。
阅读本文您将收获:
- 了解 express 项目中,JWT 实现会话控制涉及的依赖及应用。
- 了解 express 中通过 JWT 实现会话控制的详细流程及代码细节。
- 了解 express Router 的用法及接口定义。
准备工作
本文代码基于 我用 express 实现会话控制之 - Session 中现有项目编写,想动手实操的同学,可以阅读前两篇文章或直接 GitHub 拉取。
创建 jwt 目录结构
项目根目录下执行以下命令:
cd src
mkdir jwt
cd jwt
touch index.js
以上命令创建了 jwt 目录 及 index.js,执行后项目结构如下:
安装依赖
- javawebtoken:操作 jwt 的依赖包。
终端输入以下命令,回车执行:
pnpm add jsonwebtoken -S
安装成功后,package.json 文件中的 dependencies 如下:
创建 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)
登录接口
登录接口主要逻辑如下:
- 验证登录用户是否存在。
- 若不存在,报错 “用户不存在”,退出。
- 若存在,验证客户端传递的用户名、密码是否正确。
- 若不正确,报错 “用户名或密码错误”。
- 若正确,生成 jwt,保存到浏览器 cookie 中。
- 渲染“登录成功”页面。
我们继续使用现有的 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 场景,参数接收 err 和 token。
接口中将生成的 jwt 保存到 cookie,主要是考虑到方便存取和传递。在实际项目中,交由前端保存到本地缓存(localStorage/sessionStorage)中,通过请求头 Authorization 标头传递也可以。
测试一下
- 启动服务
终端执行 pnpm start,显示以下内容即启动成功。
- 打开登录页面,http://localhost:3000/login?authType=jwt ,输入用户名/密码 dulaoban/123456,点击 Login。
- 登录成功
- 查看浏览器控制台,确认是否生成 jwt Cookie。
OK,运行正常,初步测试通过。
登出接口
登出接口主要逻辑如下:
- 验证当前 jwt 是否有效。
- 若已失效,则无需处理,直接 “登出成功“。
- 若未失效,则移除 jwt cookie,并将该 jwt 添加到黑名单,以使其后续不再可用。
- 登出成功。
// 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。会话控制篇章已完结,感兴趣的小伙伴可以看下前几篇文章,理论结合实践,效果更好哦。
如果您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。
技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。