我用 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 的应用实践,感兴趣的同学蹲一下吧。

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

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


往期推荐

相关推荐
Larcher1 分钟前
新手也能学会,100行代码玩AI LOGO
前端·llm·html
徐子颐13 分钟前
从 Vibe Coding 到 Agent Coding:Cursor 2.0 开启下一代 AI 开发范式
前端
小月鸭26 分钟前
如何理解HTML语义化
前端·html
jump6801 小时前
url输入到网页展示会发生什么?
前端
诸葛韩信1 小时前
我们需要了解的Web Workers
前端
brzhang1 小时前
我觉得可以试试 TOON —— 一个为 LLM 而生的极致压缩数据格式
前端·后端·架构
yivifu1 小时前
JavaScript Selection API详解
java·前端·javascript
这儿有一堆花1 小时前
告别 Class 组件:拥抱 React Hooks 带来的函数式新范式
前端·javascript·react.js
十二春秋2 小时前
场景模拟:基础路由配置
前端
六月的可乐2 小时前
实战干货-Vue实现AI聊天助手全流程解析
前端·vue.js·ai编程