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

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

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


往期推荐

相关推荐
l1x1n026 分钟前
No.3 笔记 | Web安全基础:Web1.0 - 3.0 发展史
前端·http·html
昨天;明天。今天。42 分钟前
案例-任务清单
前端·javascript·css
zqx_72 小时前
随记 前端框架React的初步认识
前端·react.js·前端框架
惜.己2 小时前
javaScript基础(8个案例+代码+效果图)
开发语言·前端·javascript·vscode·css3·html5
什么鬼昵称3 小时前
Pikachu-csrf-CSRF(get)
前端·csrf
长天一色3 小时前
【ECMAScript 从入门到进阶教程】第三部分:高级主题(高级函数与范式,元编程,正则表达式,性能优化)
服务器·开发语言·前端·javascript·性能优化·ecmascript
NiNg_1_2343 小时前
npm、yarn、pnpm之间的区别
前端·npm·node.js
秋殇与星河3 小时前
CSS总结
前端·css
BigYe程普4 小时前
我开发了一个出海全栈SaaS工具,还写了一套全栈开发教程
开发语言·前端·chrome·chatgpt·reactjs·个人开发
余生H4 小时前
前端的全栈混合之路Meteor篇:关于前后端分离及与各框架的对比
前端·javascript·node.js·全栈