从零搭建 Node.js 企业级 Web 服务器(六):会话

关于会话

会话是指服务器以浏览器维度提供的上下文缓存。服务器通过在 cookie 或者 url 中维护唯一 id 来索引和管理会话缓存。会话缓存是跨服务器多个应用节点共享的,应用节点通过会话模块有序访问会话缓存:

d8d0568db97c34baf33206ad04ea6a93c38c18b0.jpg

缓存模块

Express 官方提供的会话模块 express-session 非常灵活,可以通过 store 参数任意替换会话缓存的存储方式。性能优先的方式是使用 connect-redis 存储在 Redis 缓存服务中,成本优先的方式是使用 connect-session-sequelize 存储在数据库里,本文采用成本优先的方式存储会话缓存。在上一章已完成的工程 licg9999/nodejs-server-examples - 05-database 的根目录执行以下安装命令:

$ yarn add express-session # 本地安装 express-session
# ...
info Direct dependencies
└─ express-session@1.17.1
# ...

$ # 本地安装 connect-session-sequelize 6.x 版本,配合 sequelize 5.x 版本
$ yarn add 'connect-session-sequelize@^6.1.1'
# ...
info Direct dependencies
└─ connect-session-sequelize@6.1.1
# ...

考虑到其他使用 cookie 的情况安装 cookie-parser 统一提供 cookie 处理逻辑:

$ yarn add cookie-parser # 本地安装 cookie-parser
# ...
info Direct dependencies
└─ cookie-parser@1.4.5
# ...

简易登录

现在为店铺管理加上简易的登录功能。先创建 session 数据库表格结构:

$ # 生成会话 schema 迁移文件
$ yarn sequelize migration:generate --name create-session

$ tree src/models/migrate # 展示 src/models/migrate 目录内容结构
src/models/migrate
├── 20200725045100-create-shop.js
└── 20200727025727-create-session.js

美化一下 src/models/migrate/20200727025727-create-session.js

// src/models/migrate/20200727025727-create-session.js
module.exports = {
  up: async (queryInterface, Sequelize) => {
    /**
     * Add altering commands here.
     *
     * Example:
     * await queryInterface.createTable('users', { id: Sequelize.INTEGER });
     */
  },

  down: async (queryInterface, Sequelize) => {
    /**
     * Add reverting commands here.
     *
     * Example:
     * await queryInterface.dropTable('users');
     */
  },
};

调整一下 src/models/migrate/20200727025727-create-session.js

// src/models/migrate/20200727025727-create-session.js
module.exports = {
  up: async (queryInterface, Sequelize) => {
-    /**
-     * Add altering commands here.
-     *
-     * Example:
-     * await queryInterface.createTable('users', { id: Sequelize.INTEGER });
-     */
+    await queryInterface.createTable('session', {
+      sid: {
+        type: DataTypes.STRING(36),
+      },
+      expires: {
+        type: DataTypes.DATE,
+      },
+      data: {
+        type: DataTypes.TEXT,
+      },
+
+      created_at: {
+        allowNull: false,
+        type: Sequelize.DATE,
+      },
+      updated_at: {
+        allowNull: false,
+        type: Sequelize.DATE,
+      },
+    });
  },

  down: async (queryInterface, Sequelize) => {
-    /**
-     * Add reverting commands here.
-     *
-     * Example:
-     * await queryInterface.dropTable('users');
-     */
+    queryInterface.dropTable('session');
  },
};

向数据库写入 session 表格结构:

$ yarn sequelize db:migrate # 向数据库写入表格结构,db:migrate 会根据 sequelize_meta 记录只创建 session 表格结构

接下来初始化会话模块并补充登录验证的逻辑:

<!-- public/login.html -->
<html>
  <head>
    <meta charset="utf-8" />
  </head>
  <body>
    <form method="post" action="/api/login">
      <button type="submit">一键登录</button>
    </form>
  </body>
</html>
// src/controllers/login.js
const { Router } = require('express');

class LoginController {
  async init() {
    const router = Router();
    router.post('/', this.post);
    return router;
  }

  post = (req, res) => {
    req.session.logined = true;
    res.redirect('/');
  };
}

module.exports = async () => {
  const c = new LoginController();
  return await c.init();
};
// src/controllers/index.js
const { Router } = require('express');
const shopController = require('./shop');
const chaosController = require('./chaos');
const healthController = require('./health');
+const loginController = require('./login');

module.exports = async function initControllers() {
  const router = Router();
  router.use('/api/shop', await shopController());
  router.use('/api/chaos', await chaosController());
  router.use('/api/health', await healthController());
+  router.use('/api/login', await loginController());
  return router;
};
// src/middlewares/login.js
const { parse } = require('url');

module.exports = function loginMiddleware(
  homepagePath = '/',
  loginPath = '/login.html',
  whiteList = {
    '/500.html': ['get'],
    '/api/health': ['get'],
    '/api/login': ['post'],
  }
) {
  whiteList[loginPath] = ['get'];

  return (req, res, next) => {
    const { pathname } = parse(req.url);

    if (req.session.logined && pathname == loginPath) {
      res.redirect(homepagePath);
      return;
    }

    if (
      req.session.logined ||
      (whiteList[pathname] &&
        whiteList[pathname].includes(req.method.toLowerCase()))
    ) {
      next();
      return;
    }

    res.redirect(loginPath);
  };
};
// src/middlewares/session.js
const session = require('express-session');
const sessionSequelize = require('connect-session-sequelize');
const { sequelize } = require('../models');

module.exports = function sessionMiddleware(secret) {
  const SequelizeStore = sessionSequelize(session.Store);

  const store = new SequelizeStore({
    db: sequelize,
    modelKey: 'Session',
    tableName: 'session',
  });

  return session({
    secret,
    cookie: { maxAge: 7 * 24 * 60 * 60 * 1000 },
    store,
    resave: false,
    proxy: true,
    saveUninitialized: false,
  });
};
// src/middlewares/index.js
const { Router } = require('express');
+const cookieParser = require('cookie-parser');
+const sessionMiddleware = require('./session');
const urlnormalizeMiddleware = require('./urlnormalize');
+const loginMiddleware = require('./login');
+
+const secret = '842d918ced1888c65a650f993077c3d36b8f114d';

module.exports = async function initMiddlewares() {
  const router = Router();
  router.use(urlnormalizeMiddleware());
+  router.use(cookieParser(secret));
+  router.use(sessionMiddleware(secret));
+  router.use(loginMiddleware());
  return router;
};
// src/server.js
// ...
async function bootstrap() {
+  server.use(await initMiddlewares());
  server.use(express.static(publicDir));
  server.use('/moulds', express.static(mouldsDir));
-  server.use(await initMiddlewares());
  server.use(await initControllers());
  server.use(errorHandler);
  await promisify(server.listen.bind(server, port))();
  console.log(`> Started on port ${port}`);
}
// ...

访问 http://localhost:9000/ 即可看到效果:

4a4bebfbcf110c28215cffb26f24c3c40722c6b4.gif

本章源码

licg9999/nodejs-server-examples - 06-session

更多阅读

从零搭建 Node.js 企业级 Web 服务器(零):静态服务
从零搭建 Node.js 企业级 Web 服务器(一):接口与分层
从零搭建 Node.js 企业级 Web 服务器(二):校验
从零搭建 Node.js 企业级 Web 服务器(三):中间件
从零搭建 Node.js 企业级 Web 服务器(四):异常处理
从零搭建 Node.js 企业级 Web 服务器(五):数据库访问
从零搭建 Node.js 企业级 Web 服务器(六):会话
从零搭建 Node.js 企业级 Web 服务器(七):认证登录
从零搭建 Node.js 企业级 Web 服务器(八):网络安全
从零搭建 Node.js 企业级 Web 服务器(九):配置项
从零搭建 Node.js 企业级 Web 服务器(十):日志
从零搭建 Node.js 企业级 Web 服务器(十一):定时任务
从零搭建 Node.js 企业级 Web 服务器(十二):远程调用
从零搭建 Node.js 企业级 Web 服务器(十三):断点调试与性能分析
从零搭建 Node.js 企业级 Web 服务器(十四):自动化测试
从零搭建 Node.js 企业级 Web 服务器(十五):总结与展望

认真写点好代码。

2.2k 声望
1.1k 粉丝
0 条评论
推荐阅读
再见了 Redux、Recoil、MobX、Zustand、Jotai 还有 Valtio,状态管理还可以这样做?
坚持在一线写前端代码大概有七八年了,写过一些项目,有过一些反思,越来越确信平日里一直用得心安理得某些的东西也许存在着问题,比如:在 状态管理 上一直比较流行的实践 🙏,所以试着分享出来探讨一下。

乌柏木3阅读 630

ESlint + Stylelint + VSCode自动格式化代码(2023)
安装插件 ESLint,然后 File -&gt; Preference-&gt; Settings(如果装了中文插件包应该是 文件 -&gt; 选项 -&gt; 设置),搜索 eslint,点击 Edit in setting.json

谭光志34阅读 20.8k评论 9

安全地在前后端之间传输数据 - 「3」真的安全吗?
在「2」注册和登录示例中,我们通过非对称加密算法实现了浏览器和 Web 服务器之间的安全传输。看起来一切都很美好,但是危险就在哪里,有些人发现了,有些人嗅到了,更多人却浑然不知。就像是给门上了把好锁,还...

边城32阅读 7.3k评论 5

封面图
涨姿势了,有意思的气泡 Loading 效果
今日,群友提问,如何实现这么一个 Loading 效果:这个确实有点意思,但是这是 CSS 能够完成的?没错,这个效果中的核心气泡效果,其实借助 CSS 中的滤镜,能够比较轻松的实现,就是所需的元素可能多点。参考我们...

chokcoco24阅读 2.3k评论 3

你可能不需要JS!CSS实现一个计时器
CSS现在可不仅仅只是改一个颜色这么简单,还可以做很多交互,比如做一个功能齐全的计时器?样式上并不复杂,主要是几个交互的地方数字时钟的变化开始、暂停操作重置操作如何仅使用 CSS 来实现这样的功能呢?一起...

XboxYan25阅读 1.7k评论 1

封面图
在前端使用 JS 进行分类汇总
最近遇到一些同学在问 JS 中进行数据统计的问题。虽然数据统计一般会在数据库中进行,但是后端遇到需要使用程序来进行统计的情况也非常多。.NET 就为了对内存数据和数据库数据进行统一地数据处理,发明了 LINQ (L...

边城17阅读 2k

封面图
过滤/筛选树节点
又是树,是我跟树杠上了吗?—— 不,是树的问题太多了!🔗 相关文章推荐:使用递归遍历并转换树形数据(以 TypeScript 为例)从列表生成树 (JavaScript/TypeScript) 过滤和筛选是一个意思,都是 filter。对于列表来...

边城18阅读 7.9k评论 3

封面图

认真写点好代码。

2.2k 声望
1.1k 粉丝
宣传栏