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

前言

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

阅读本文您将收获:

  1. 了解 express 项目中,使用 cookie 涉及的依赖及应用流程。

  2. 掌握如何在 express 中通过 cookie 实现会话控制。

  3. 掌握 express Router 用法及接口定义。
    注:

  4. 本文涉及 express ,不了解的小伙伴可以阅读 《express 基础入门》

  5. 本文仅涉及少量理论内容,多是代码实践,若对于技术有疑问,可评论区交流。

初始工程搭建

创建项目基础结构

  1. 新建项目根目录,命名为 express-explorer
shell 复制代码
mkdir express-explorer
  1. pnpm 初始化
shell 复制代码
cd express-explorer
pnpm init

需全局安装 pnpm,npm i -g pnpm

  1. 新建目录结构
shell 复制代码
touch index.js
mkdir src
mkdir views
cd src
mkdir cookie
cd cookie
touch index.js
cd ../../views
touch login.ejs

一套命令执行下来,可以创建出以下结构:

其中:

  • index.js 为项目的入口文件。
  • src/cookie/index.jscookie 相关接口,后续还会有 sessionjwt 等。
  • views 是模板目录,其中 login.ejs 是登录页面。

安装依赖

  • express: Nodejs 框架。
  • cookie-parser: express 中间件,可以解析请求头的 Cookie 标头,生成以 Cookie 名称 为键的对象,填充到 request.cookies 上。
  • ejs: 模板引擎,用于在 express 中渲染页面。

执行以下命令,一次性安装:

shell 复制代码
pnpm add express cookie-parser ejs -S

执行成功后,查看 package.json 文件中的 dependencies,如下则表示安装成功:

编写入口代码

打开根目录的 index.js 文件,开始写代码:

  1. 创建 express 应用实例
javascript 复制代码
const express = require('express')

// express 应用实例
const app = express()
  1. 安装 CookieParser 中间件
javascript 复制代码
const express = require('express')
const cookieParser = require('cookie-parser')

// express 应用实例
const app = express()
// 安装 CookieParser 中间件
app.use(cookieParser())
  1. 启动服务,监听 3000 端口
javascript 复制代码
// 定义服务启用端口号
const port = 3000

app.listen(port, () => {
  console.log(`服务已启动... \n访问 http://localhost:${port}`)
})
  1. 写一个临时接口,测试下服务(ps: 测试通过后记得删除)。
javascript 复制代码
// 测试接口
app.get('/', (req, res) => {
    res.send('Hello World')
})
  1. 入口代码搞定,使用 nodemon 启动服务,控制台执行 nodemon index.js,显示如下则启动成功。

nodemon 是启动 node 服务的工具,可以在检测到文件变更后自动重启服务。需要全局安装 (npm i -g nodemon) 才能在命令行直接使用。

  1. 浏览器访问 http://localhost:3000 看下效果。

OK,没毛病。

添加 cookie 路由

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

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

创建并导出一个 Router 实例

javascript 复制代码
const express = require('express')
// 创建 Router 实例
const router = express.Router()

module.exports = router

在根目录 index.js 中安装 cookieRouter

javascript 复制代码
const cookieRouter = require('./src/cookie')

// 安装 Cookie Router
app.use('/cookie', cookieRouter)

后续所有 CookieRouter 中定义的接口,访问时都要加上 /cookie 前缀,比如 http://localhost:3000/cookie/login。

登录接口

登录接口逻辑主要验证客户端传递的账号、密码是否正确 。若正确,则创建 Cookie 返回客户端;若不正确,则抛错提示。

方便起见,我们在代码中写死用户数据。实际项目中需要与数据库中的账号密码做匹配。

根目录下新建 users.js,保存用户数据。

javascript 复制代码
/**
 * 用户列表
 */
module.exports = {
  dulaoban: { username: 'dulaoban', password: '123456'}
}
javascript 复制代码
const users = require('../../users')

// 账号相关 cookie 名称
const COOKIES = {
  USERNAME: 'username',
  PWD: 'password'
}

// 登录
router.get('/login', (req, res) => {
  const {name, password} = req.query
  // 校验用户是否存在
  const user = users[username]
  if (!user) {
    res.send('用户不存在')
    return
  }
  // 匹配用户名和密码
  if (username === user.username && password === user.password) {
    // cookie 配置
    const cookieOptions = {
      httpOnly: true, // 不允许客户端修改
      maxAge: 60 * 1000
    }
    res.cookie(COOKIES.USERNAME, username, cookieOptions)
    res.send('<h1>登录成功</h1><a href="/cookie/logout">登出</a>')
  } else {
    res.send('账号名或密码错误')
  }
})

由于 /cookie/loginpost 接口,涉及到 payload(载荷) 的解析,我们需要安装 express 内置的中间件:urlencoded ,该中间件会解析 post 接口的请求体参数,并生成一个新对象,赋给 req.body ,接口中只需要从 req.body 中取指定参数即可。

根目录 index.js 中添加以下代码:

javascript 复制代码
app.use(express.urlencoded({ extended: false }))

OK,登录接口齐活,因为是 post 接口,我们先不测试,等下一步完善登录页面后,一起看效果。

登出接口

登出接口逻辑主要清除用户相关 Cookie ,并重定向至登录页。后续客户端访问业务接口时,因为请求没带 Cookie,无法通过校验,达到会话控制的效果。

javascript 复制代码
// 登出
router.get('/logout', (req, res) => {
  res.clearCookie(COOKIES.USERNAME)
  res.redirect('/login');
})

我们需要在根目录 index.js 中添加 login 接口,用于导航到登录页。

javascript 复制代码
// 登录页
app.use('/login', (req, res) => {
  res.render('login')
})

跟着写的同学,要有疑问了,这里渲染的 login 页面哪来的?

这就要用到 模板引擎 了。

首先我们在 view/login.ejs 中编写登录页面结构:

html 复制代码
<h1>登录</h1>
<form method="post" action="/cookie/login">
  <p>
    <label for="username">用户名:</label>
    <input type="text" name="username" id="username" placeholder="dulaoban">
  </p>
  <p>
    <label for="password">密码:</label>
    <input type="text" name="password" id="password" placeholder="123456">
  </p>
  <p>
    <input type="submit" value="Login">
  </p>
</form>

然后在根目录 index.js 中启用 ejs 模板引擎,并指定模板目录(views ),即 views 目录下的文件都作为页面模板处理:

javascript 复制代码
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

OK,齐活,我们来看看效果。

浏览器打开 http://localhost:3000/login 打开登录页,输入用户名/密码:dulaoban/123456,点击登录。

登录成功,然后点击 Logout 按钮,退出登录。

ok,如预期一样,重定向到了登录页。

那是否真正的实现了会话控制呢,我们写个业务接口测试一下。

业务接口,对标真实项目的权限验证

业务接口逻辑主要是保证用户明确有权限的情况下,处理业务逻辑 。我们定义一个 helloWorld 的接口,若校验通过,则返回 Hello World,否则跳转登录页面。

javascript 复制代码
router.get('/helloWorld', (req, res, next) => {
  const { username } = req.cookies
  if (!username) {
    res.redirect('/login')
    return
  }

  const user = users[username]
  if (!user) {
    res.send('用户不存在,请重新登录')
    return
  }
  next()
}, (req, res) => {
  res.send('Hello world')
})

以上代码中,为 helloWorld 接口增加了一个中间件函数 ,即第二个参数,用于校验请求 cookie 是否存在且合法。若校验不通过,则重新登录或抛错,否则调用 next ,继续向下执行,返回 HelloWorld

经测试,登录状态正常显示 HelloWorld,登出状态会重定向到登录页。符合预期。

权限验证中间件

实际项目中,不会只有一个业务接口,如按照上面的写法,就要为每个接口都加上中间件函数,一旦有逻辑有调整,要挨个修改,那无疑是毁灭式的灾难。

我们可以将通用的校验逻辑抽离单独的中间件进行维护。

根目录下新建一个中间件目录:middlewares,用来专门防止中间件文件。

shell 复制代码
mkdir middlewares
cd middlewares
touch checkCookieAuth.js

将校验的逻辑写入 checkCookieAuth.js

javascript 复制代码
const users = require('../users')

function checkCookieAuth (req, res, next) {
  const { username } = req.cookies
  if (!username) {
    res.redirect('/login')
    return
  }

  const user = users[username]
  if (!user) {
    res.send('用户不存在,请重新登录')
    return
  }
  next()
}

module.exports = checkCookieAuth

然后在 cookie/index.js 中引入,并传入 helloDulaoban 接口定义函数中。

javascript 复制代码
const checkCookieAuth = require('../../middlewares/checkCookieAuth')

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

后续如果有新的接口,需要校验权限,只需要定义接口时传入 checkCookieAuth 即可。比如我又定义一个 helloDulaoban 的接口:

javascript 复制代码
// 业务接口,测试
router.get('/helloDulaoban', checkCookieAuth, (req, res) => {
  res.send('Hello dulaoban')
})

一处定义,到处使用,十分方便,这就是中间件的优势所在。

结语

本文重点介绍了基于 express 框架,实现 cookie 会话控制的全过程,从基础工程搭建,到入口代码,再到接口定义,以及最终的应用验证,旨在帮助同学们加深对于 Cookie 应用的理解。相关代码已上传至 GitHub,若喜欢欢迎 star 。后面会继续分享 sessionJWT 的应用实践,感兴趣的同学蹲一下吧。

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

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


往期推荐

相关推荐
xiaofeichaichai3 小时前
Webpack
前端·webpack·node.js
问心无愧05133 小时前
ctf show web入门111
android·前端·笔记
唐某人丶4 小时前
模型越来越强,我们还需要 Agent 工程吗?—— 从价值重估到 Harness 实践
前端·agent·ai编程
智码看视界4 小时前
现代Web开发基础:全栈工程师的起航点
前端·后端·c5全栈
JS菌4 小时前
手写一个 AI Agent 全栈项目:从沙箱执行到子智能体的完整实现
前端·人工智能·后端
excel5 小时前
HLS TS 文件损坏的元凶:Git 提交与拉取
前端
Aphasia3115 小时前
https连接传输流程
前端·面试
徐小夕5 小时前
万字长文!千万级文档 RAG 知识库系统落地实践
前端·算法·github
threelab6 小时前
Three.js 物理模拟着色器 | 三维可视化 / AI 提示词
开发语言·前端·javascript·人工智能·3d·着色器