前言
同学,你好!我是 嘟老板。之前发布了一篇《前端同学应该了解的 “会话控制”》,比较全面的讲述了会话控制的三种实现方式,今天我们使用 express 框架,实现一下基于 Session 的会话控制。
阅读本文您将收获:
- 了解 express 项目中,使用 Session 涉及的依赖及应用。
- 掌握如何在 express 中通过 Session 实现会话控制。
- 掌握 express Router 用法及接口定义。
准备工作
本文代码是在 我用 express 实现会话控制之 - Cookie 中实现的基础上编写,上文中有工程详细的搭建步骤,想动手实操的同学,可以去看看或直接 GitHub 拉取。
创建 session 目录结构
项目根目录下执行以下命令:
cd src
mkdir session
cd session
touch index.js
以上命令创建了 session 目录 及 index.js,执行后项目结构如下:
后续所有 Session 相关的接口,都在 session/index.js 中定义。
安装依赖
除现有依赖外,还需安装:
- express-session:可创建操作 Session 的中间件。
- express-mysql-session:用于使用 mysql 数据库存储 Session,与 express-session 结合使用。
执行以下命令安装:
pnpm add express-session express-mysql-session -S
安装成功后,package.json 文件中的 dependencies 如下:
创建 Session 中间件
根目录 index.js 中,导入 express-session 创建 Session 中间件并使用。
const session = require('express-session')
// session mysql store 配置对象
// 需根据自己的环境修改配置
const mysqlOptions = {
host: 'localhost',
port: 3306,
user: 'root',
password: 'admin0125',
database: 'db_session'
}
// session 中间件
const sessionMiddleware = session({
resave: false, // session 未变更不重新保存
saveUninitialized: false, // session 为初始化时不存储
secret: 'dulaoban', // 用于 sessionID 签名的秘钥
store: new MysqlStore(mysqlOptions)
})
// 安装 sessionMiddleware
app.use(sessionMiddleware)
以下是 Session 中间件常用配置项:
- secret:必选,用于 SessionID 签名的秘钥。
- cookie:SessionID cookie 的配置对象,默认
{ path: '/', httpOnly: true, secure: false, maxAge: null }
- name: 浏览器存储 SessionID cookie 的键名,也可用于在请求中读取 SessionID,默认 connect.sid。
- resave:强制重新保存 Session,即便在请求期间 Session 没有发生变更。默认
true
。 - saveUninitialized:强制保存未初始化的 Session,默认值
true
。若想实现登录会话、减少服务器存储或设置 cookie 前需获取授权等,建议设置为false
。 - store:会话存储实例,默认 MemoryStore,本文中我们选择 express-mysql-session,即存储到 mysql 数据库中。可选 store 点击查看。
创建 Session 路由
我们将 Session 相关的路由封装进统一的 Router,便于管理,比如设置相同的接口前缀。
Session 相关代码全部在 session/index.js 文件中编写。
创建并导出一个 Router 实例
在 session/index.js 中编写以下代码,创建并导出 sessionRouter。
const express = require('express')
const router = express.Router()
module.exports = router
在根目录 index.js 中安装 sessionRouter。
const sessionRouter = require('./src/session')
// 安装 Session Router
app.use('/session', sessionRouter)
登录接口
express-session 中间件会在 req 对象上添加 session 属性,用于操作存储的 session 数据,通常会被序列化为 JSON 格式,允许对象嵌套。如 req.session.username
,可获取 session 中保存的用户名。
登录接口逻辑主要 验证客户端传递的账号、密码是否正确。若正确,则生成 Session,并将 SessionID 存储到 Cookie;若不正确,则抛错提示。
我们继续使用现有的 users.js 中的用户列表。
// 登录接口
router.post('/login', (req, res) => {
const { username, password } = req.body
// 校验用户是否存在
const user = users[username]
if (!user) {
res.send('用户不存在')
return
}
// 验证用户名和密码
if (username === user.username && user.password) {
req.session.regenerate((err) => {
if (err) next(err)
// 设置 session 中存储用户名
req.session.username = username
// 将 session 保存在数据库中
req.session.save((err) => {
if (err) return next(err)
res.send('<h1>登录成功</h1><a href="/session/logout">Logout</a>')
})
})
} else {
res.send('账号名或密码错误')
}
})
代码中涉及到两个 req.session 暴露的 api:
- regenerate:重新生成 session,调用成功后,会在 req.session 上重新生成新的 SID 和 Session 实例。
- save:将 session 内容保存到存储库中,本文中指保存到 mysql。若 req.session 数据变更,会在 http 响应结束时自动调用该方法,通常不需要手动调用。
测试一下
不想看 Cookie 篇的小伙伴,可以去 GitHub 上拉取代码。
- 启动服务
终端执行 pnpm start
,显示以下内容即表示启动成功。
- 打开登录页面,http://localhost:3000/login?authType=session ,输入用户名/密码 dulaoban/123456,点击登录。
- 登录成功
- 查看浏览器控制台,确认是否生成 SessionID Cookie。
已正常生成 Cookie。
- 查看数据库表,确认是否存储 Session 数据。
OK,Session 存储成功,并且包含了账户名信息。
登出接口
登出接口逻辑主要 清除 Session 中用户信息数据,刷新当前 SessionID 和 Session,并重定向至登录页。后续接口鉴权时,只需验证 req.session 中是否存在用户信息,若无,则无法通过校验,达到会话控制的效果。
// 登出
router.get('/logout', (req, res) => {
// 清除 session 中的用户名内容
req.session.username = null
// 将无 username 的 session 保存到数据库中,后续权限验证校验 req.session.username 是否存在即可。
req.session.save((err) => {
if (err) next(err)
// 重新生成 SID 和 Session 实例,使原 SID 和 Session 实例失效
req.session.regenerate((err) => {
if (err) next(err)
res.redirect('/login');
})
})
})
登出接口先将 session 中的用户信息置空并保存,这是因为在权限验证时,可以通过检查 session 中是否存在用户信息,来判断是否授权;然后重新生成 SID 和 Session 实例,重置请求携带的 Cookie 信息,避免因未刷新 Cookie 造成原有的 Session 仍然可用,达不到退出登录状态的效果。
业务接口,对标真实项目
业务接口逻辑主要是保证用户明确有权限的情况下,处理业务逻辑。我们定义一个 helloWorld 的接口,若鉴权通过,则返回 Hello World,否则跳转登录页面。
// 业务接口,测试
router.get('/helloWorld', (req, res, next) => {
const { username } = req.session
// 验证 session 中是否存在用户名信息,若无,表示授权已失效,跳转到登录页
if (!username) {
res.redirect('/login')
return
}
const user = users[username]
if (!user) {
res.send('用户不存在,请重新登录')
return
}
next()
}, (req, res) => {
res.send('<p>Hello world</p><a href="/session/logout">Logout</a>')
})
经验证,登录状态下可正常渲染 Hello World;退出登录后,浏览器地址栏输入 http://localhost:3000/session/helloWorld 访问,会重定向到登录页面,符合预期的会话控制效果。
权限验证中间件
helloWorld 接口中验证授权的函数参数,我们称之为中间件函数,可用来控制接口的执行走向。
在实际项目中,需要验证权限的接口会有很多很多,如果都像上面 helloWorld 接口那样写,一但需要校验,就在对应接口写一个验证中间件函数,相当麻烦。后续若有调整,也必是毁灭式的灾难。
我们可以将中间件函数抽离出来,单独维护,后续有接口需要鉴权,只要将该中间件函数引入使用即可。
在 middlewares 目录下新增 checkSessionAuth.js 文件,将鉴权逻辑写入并导出:
const users = require('../users')
function checkSessionAuth (req, res, next) {
const { username } = req.session
// 验证 session 中是否存在用户名信息,若无,表示授权已失效,跳转到登录页
if (!username) {
res.redirect('/login')
return
}
const user = users[username]
if (!user) {
res.send('用户不存在,请重新登录')
return
}
next()
}
module.exports = checkSessionAuth
然后在 session/index.js 引入,并传入到 helloWorld 定义函数:
const checkSessionAuth = require('../../middlewares/checkSessionAuth')
// 业务接口,测试
router.get('/helloWorld', checkSessionAuth, (req, res) => {
res.send('<p>Hello world</p><a href="/session/logout">Logout</a>')
})
简单方便,后续若有新接口需要鉴权,以相同的方式使用就 OK 啦。
结语
本文重点介绍了基于 express 框架,实现 Session 会话控制的全过程。从项目基础结构开始,到路由定义,再到 Session 处理以及权限验证,旨在帮助同学们加深对于 Session 应用的理解。相关代码已上传至 GitHub,喜欢的同学欢迎 star。后面会继续分享 JWT 的应用实践,感兴趣的同学蹲一下吧。
如果您对文章内容有任何疑问或想深入讨论,欢迎评论区留下您的问题和见解。
技术简而不凡,创新生生不息。我是 嘟老板,咱们下期再会。