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

256 阅读7分钟

前言

同学,你好!我是 嘟老板。之前发布了一篇《前端同学应该了解的 “会话控制”》,比较全面的讲述了会话控制的三种实现方式,今天我们使用 express 框架,实现一下基于 Session 的会话控制。

阅读本文您将收获:

  1. 了解 express 项目中,使用 Session 涉及的依赖及应用。
  2. 掌握如何在 express 中通过 Session 实现会话控制。
  3. 掌握 express Router 用法及接口定义。

准备工作

本文代码是在 我用 express 实现会话控制之 - Cookie 中实现的基础上编写,上文中有工程详细的搭建步骤,想动手实操的同学,可以去看看或直接 GitHub 拉取。

创建 session 目录结构

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

cd src
mkdir session
cd session
touch index.js

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

image.png

后续所有 Session 相关的接口,都在 session/index.js 中定义。

安装依赖

除现有依赖外,还需安装:

执行以下命令安装:

pnpm add express-session express-mysql-session -S

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

image.png

创建 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 上重新生成新的 SIDSession 实例。
  • save:将 session 内容保存到存储库中,本文中指保存到 mysql。若 req.session 数据变更,会在 http 响应结束时自动调用该方法,通常不需要手动调用。

测试一下

不想看 Cookie 篇的小伙伴,可以去 GitHub 上拉取代码。

  1. 启动服务

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

image.png

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

image.png

  1. 登录成功

image.png

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

image.png

已正常生成 Cookie

  1. 查看数据库表,确认是否存储 Session 数据。

image.png

OK,Session 存储成功,并且包含了账户名信息。

登出接口

登出接口逻辑主要 清除 Session 中用户信息数据,刷新当前 SessionIDSession,并重定向至登录页。后续接口鉴权时,只需验证 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 中是否存在用户信息,来判断是否授权;然后重新生成 SIDSession 实例,重置请求携带的 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 的应用实践,感兴趣的同学蹲一下吧。

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

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


往期推荐