會話是指服務器以瀏覽器維度提供的上下文緩存。服務器經過在 cookie 或者 url 中維護惟一 id 來索引和管理會話緩存。會話緩存是跨服務器多個應用節點共享的,應用節點經過會話模塊有序訪問會話緩存:html
Express 官方提供的會話模塊 express-session 很是靈活,能夠經過 store
參數任意替換會話緩存的存儲方式。性能優先的方式是使用 connect-redis 存儲在 Redis 緩存服務中,成本優先的方式是使用 connect-session-sequelize 存儲在數據庫裏,本文采用成本優先的方式存儲會話緩存。在上一章已完成的工程 host1-tech/nodejs-server-examples - 05-database 的根目錄執行如下安裝命令:node
$ 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 處理邏輯:git
$ yarn add cookie-parser # 本地安裝 cookie-parser # ... info Direct dependencies └─ cookie-parser@1.4.5 # ...
如今爲店鋪管理加上簡易的登陸功能。先建立 session 數據庫表格結構:github
$ # 生成會話 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
:redis
// 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 表格結構:express
$ yarn sequelize db:migrate # 向數據庫寫入表格結構,db:migrate 會根據 sequelize_meta 記錄只建立 session 表格結構
接下來初始化會話模塊並補充登陸驗證的邏輯:bootstrap
<!-- 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/ 便可看到效果:segmentfault
host1-tech/nodejs-server-examples - 06-sessionapi
從零搭建 Node.js 企業級 Web 服務器(零):靜態服務
從零搭建 Node.js 企業級 Web 服務器(一):接口與分層
從零搭建 Node.js 企業級 Web 服務器(二):校驗
從零搭建 Node.js 企業級 Web 服務器(三):中間件
從零搭建 Node.js 企業級 Web 服務器(四):異常處理
從零搭建 Node.js 企業級 Web 服務器(五):數據庫訪問從零搭建 Node.js 企業級 Web 服務器(六):會話