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

1,152 阅读7分钟

前言

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

阅读本文您将收获:

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

注:

  1. 本文涉及 express,不了解的小伙伴可以阅读 《express 基础入门》
  2. 本文仅涉及少量理论内容,多是代码实践,若对于技术有疑问,可评论区交流。

初始工程搭建

创建项目基础结构

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

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

  1. 新建目录结构
touch index.js
mkdir src
mkdir views
cd src
mkdir cookie
cd cookie
touch index.js
cd ../../views
touch login.ejs

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

image.png

其中:

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

安装依赖

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

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

pnpm add express cookie-parser ejs -S

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

image.png

编写入口代码

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

  1. 创建 express 应用实例
const express = require('express')

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

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

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

image.png

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

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

image.png

OK,没毛病。

添加 cookie 路由

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

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

创建并导出一个 Router 实例

const express = require('express')
// 创建 Router 实例
const router = express.Router()

module.exports = router

在根目录 index.js 中安装 cookieRouter

const cookieRouter = require('./src/cookie')

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

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

登录接口

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

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

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

/**
 * 用户列表
 */
module.exports = {
  dulaoban: { username: 'dulaoban', password: '123456'}
}
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 中添加以下代码:

app.use(express.urlencoded({ extended: false }))

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

登出接口

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

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

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

// 登录页
app.use('/login', (req, res) => {
  res.render('login')
})

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

这就要用到 模板引擎 了。

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

<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 目录下的文件都作为页面模板处理:

app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));

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

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

image.png

image.png

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

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

image.png

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

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

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

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,用来专门防止中间件文件。

mkdir middlewares
cd middlewares
touch checkCookieAuth.js

将校验的逻辑写入 checkCookieAuth.js

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 接口定义函数中。

const checkCookieAuth = require('../../middlewares/checkCookieAuth')

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

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

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

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

结语

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

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

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


往期推荐