简介
在我的前一篇小文中 express-session 小书提到了 express-session 可以更换会话储存.
那么这篇文章我们就来讲讲 express 在进行会话管理的时候如何将会话数据保存在外部数据库中, 本文中我们使用 mongodb 用作会话储存数据库.
本文中使用的模块以及版本号一览:
模块名称
版本号
express
4.16.4
mongodb
3.1.8
express-session
1.15.6
connect-mongo
2.0.3
connect-mongo 特性
支持 Express5
支持所有版本的 Connect
支持 Mongoose>=4.1.2+
支持原生 Mongodb 驱动 >=2.0.36
支持 Node.js 4 6 8 10
支持 Mongodb>=3.0
事前分析
由于 mongodb 客户端和服务器可以是多对多的关系, 故有如下组合.
一个客户端连接多个服务器
多个客户端连接一个服务器
多个客户端连接多个服务器
一个客户端连接一个服务器
本文主要讲解一个客户端连接一个服务器.
这种情况下, 一般服务器监听一个端口, 而我们希望可以共享同一个 mongodb 驱动的实例.
但是在一般情况下, 我们的 mongodb 数据库不可能只用于会话管理任务, 所以本文复用同一个连接(端口).
只要复用同一个连接可以完成, 那么使用单独的驱动实例来用作会话管理也就不在话下了.
起步
首先我们引入所有的模块:
const
Express = require(‘express’)(),
MongoClient = require(‘mongodb’).MongoClient,
ExpressSession = require(‘express-session’),
MongoStore= require(‘connect-mongo’)(ExpressSession);
看起来 connect-mongo 需要将 express-session 包装一下, 这步是固定的.
接下来我们定义几个常量用于连接数据库:
const
UrlOfDb = ‘mongodb://localhost:27017’,
NameOfDb = ‘demo’,
Client = new MongoClient(UrlOfDb);// 创建 mongodb 客户端
客户端连接数据库:
Client.connect((error) => {
if (error) {
throw error;
}
});
使用一个数据表, 并且查询几条数据:
const
DataBase = Client.db(NameOfDb),
Collection = DataBase.collection(‘sessions’);
Collection.find({}).toArray((error, result) => {
if (error) {
throw error;
}
for (const element of result) {
console.log(element);
}
});
到目前为止我们没有进行 session 管理, 你可以替换本例中的数据表名称用于测试一下运行是否正常.
完整代码:
const
Express = require(‘express’)(),
MongoClient = require(‘mongodb’).MongoClient,// 获取数据库驱动
ExpressSession = require(‘express-session’),// 获取 session 中间件
MongoStore= require(‘connect-mongo’)(ExpressSession);// 获取 session 储存插件
const
UrlOfDb = ‘mongodb://localhost:27017’,
NameOfDb = ‘demo’,
Client = new MongoClient(UrlOfDb);// 创建客户端
Client.connect((error) => {
if (error) {
throw error;
}
const
DataBase = Client.db(NameOfDb),// 获取数据库
Collection = DataBase.collection(‘sessions’); // 获取数据表
// 查询数据表
Collection.find({}).toArray((error, result) => {
if (error) {
throw error;
}
for (const element of result) {
console.log(element);
}
});
});
现在我们来使用 express-session 中间件, 并且替换掉默认的储存:
// +++++
const
DataBase = Client.db(NameOfDb),// 获取数据库
Collection = DataBase.collection(‘sessions’),// 获取数据表
MongoStoreInstance = new MongoStore({// 创建一个储存实例, 传入 db 参数对于的数据库对象
db:DataBase
});
// 使用中间件
Express.use(ExpressSession({
secret: ‘hello mongo’,// cookie 签名
cookie: {maxAge: 1800000},
rolling:true,
saveUninitialized:true,
resave: false,
store:MongoStoreInstance // 替换掉默认的储存
}));
// +++++++
注意:connect-mongo 会在该 database 下创建一个 sessions 的数据表(没有这个数据表的情况下).
添加一个路由用于完成简单的验证, 用于测试是否正常工作:
Express.get(‘/’,(request,response)=>{
if(request.session.name){
response.send(` 欢迎回来 ${request.session.name}`);
return ;
}
// 使用查询字符串当作保存的信息
request.session.name = request.query.name;
request.session.pwd = request.query.pwd;
response.send(` 欢迎登录 ${request.session.name}`);
});
// 启动服务器
Express.listen(8888, function () {
console.log(‘server is listening 8888 port!’);
});
完整代码:
const
Express = require(‘express’)(),
MongoClient = require(‘mongodb’).MongoClient,
ExpressSession = require(‘express-session’),
MongoStore= require(‘connect-mongo’)(ExpressSession);
const
UrlOfDb = ‘mongodb://localhost:27017’,
NameOfDb = ‘demo’,
Client = new MongoClient(UrlOfDb);
function destroyDb(Client) {
return destroyDb = function () {
const info = ‘Client has been closed!’;
Client.close();
Client = null;
console.log(info);
return info;
}
}
Client.connect((error) => {
if (error) {
throw error;
}
const
DataBase = Client.db(NameOfDb),
Collection = DataBase.collection(‘sessions’),
MongoStoreInstance = new MongoStore({
db:DataBase
});
Express.use(ExpressSession({
secret: ‘hello mongo’,
cookie: {maxAge: 1800000},
rolling:true,
saveUninitialized:true,
resave: false,
store:MongoStoreInstance
}));
// 使用闭包将关闭数据库挂载到全局
destroyDb(Client);
// 展示复用一个连接
Collection.find({}).toArray((error, result) => {
if (error) {
throw error;
}
for (const element of result) {
console.log(element);
}
});
Express.get(‘/’,(request,response)=>{
if(request.session.name){
response.send(` 欢迎回来 ${request.session.name}`);
return ;
}
request.session.name = request.query.name;
request.session.pwd = request.query.pwd;
response.send(` 欢迎登录 ${request.session.name}`);
});
Express.get(‘/closedatabase’, (request, respnose) => {
respnose.send(destroyDb());
});
Express.listen(8888, function () {
console.log(‘server is listening 8888 port!’);
});
});
注意: 我没有删除数据库表的常规输出, 在这个例子启动的时候, 你会发现他们共用了同一个连接, 启动的时候会先输出数据表中的内容.
测试
在浏览器中输入如下内容:
http://localhost:8888/?name=ascll&pwd=123456
浏览器输出:
欢迎登录 ascll
直接再次访问该页面:
http://localhost:8888/
浏览器输出:
欢迎回来 ascll
此时在数据库中手动查询后, 或者重启本项目, 你会在控制台中发现上次留下的 session 记录:
{_id: ‘qbP36wE0nJkvtyNqx_6Amoesjjcsr-sD’,
expires: 2018-12-14T08:27:19.809Z,
session:
‘{“cookie”:{“originalMaxAge”:1800000,”expires”:”2018-12-14T08:20:21.519Z”,”httpOnly”:true,”path”:”/”},”name”:”ascll”,”pwd”:”123456″}’ }
使用总结
引入 connect-mongo 和 express-session 然后调用 connect-mongo 将 express-sessino 传入
获取上一步返回的类, 然后使用 express-session 中间件的时候对于 store 选传入这个类的实例对象
api
创建
Express 4.x, 5.0 and Connect 3.x:
const session = require(‘express-session’);
const MongoStore = require(‘connect-mongo’)(session);
app.use(session({
secret: ‘foo’,
store: new MongoStore(options)
}));
Express 2.x, 3.x and Connect 1.x, 2.x:
const MongoStore = require(‘connect-mongo’)(express);
app.use(express.session({
secret: ‘foo’,
store: new MongoStore(options)
}));
连接到 MongoDb
使用 mongoose
const mongoose = require(‘mongoose’);
// 基本使用
mongoose.connect(connectionOptions);
app.use(session({
store: new MongoStore({mongooseConnection: mongoose.connection})
}));
// 建议使用方式, 这样可以复用连接
const connection = mongoose.createConnection(connectionOptions);
app.use(session({
store: new MongoStore({mongooseConnection: connection})
}));
使用 Mongo 原生 Node 驱动
这种情况下你需要将一个 mongodb 驱动的一个数据库实例传递给 connect-mongo. 如果数据库没有打开 connect-mongo 会自动帮你连接.
/*
这里有很多种方式来获取一个数据库实例, 具体可以参考官网文档.
*/
app.use(session({
store: new MongoStore({db: dbInstance}) // 别忘了 MongoStore 是 connect-mongo 传入 express-session 后返回的一个函数
}));
// 或者也可以使用 Promise 版本
app.use(session({
store: new MongoStore({dbPromise: dbInstancePromise})
}));
通过连接字符串创建一个连接
// Basic usage
app.use(session({
store: new MongoStore({url: ‘mongodb://localhost/test-app’})
}));
// Advanced usage
app.use(session({
store: new MongoStore({
url: ‘mongodb://user12345:foobar@localhost/test-app?authSource=admins&w=1’,
mongoOptions: advancedOptions // See below for details
})
}));
事件
一个 MongoStore 实例有如下的事件:
事件名称
描述
回调参数
create
session 创建后触发
sessionId
touch
session 被获取但是未修改
sessionId
update
session 被更新
sessionId
set
session 创建后或者更新后(为了兼容)
sessionId
destroy
session 被销毁后
sessionId
使用我们之前的例子中添加如下的代码:
// +++
MongoStoreInstance.on(‘create’,(sessionId)=>{
console.log(‘create’,sessionId);
});
MongoStoreInstance.on(‘touch’,(sessionId)=>{
console.log(‘create’, sessionId);
});
MongoStoreInstance.on(‘update’,(sessionId)=>{
console.log(‘update’, sessionId);
});
MongoStoreInstance.on(‘set’,(sessionId)=>{
console.log(‘set’, sessionId);
});
MongoStoreInstance.on(‘destroy’,(sessionId)=>{
console.log(‘destroy’, sessionId);
});
// +++
清空 cookie 后再次运行服务器, 多执行几个操作你就可以看到 session 的创建以及修改等操作.
session 过期处理
基本处理方式
connect-mongo 只会使用配置了过期时间的 cookie, 如果没有设置则会创建一个新的 cookie 并且使用 tll 选项来指定过期时间:
app.use(session({
store: new MongoStore({
url: ‘mongodb://localhost/test-app’,
ttl: 14 * 24 * 60 * 60 // 默认过期时间为 14 天
})
}));
注意: 用户的每次访问都会刷新过期时间.
删除过期 session
默认情况下 connect-mongo 使用 MongoDB’s TTL collection 特性 (2.2+) 用于自动的移出过期的 session. 但是你可以修改这种行为.
connect-mongo 会在开始的时候创建一个 TTl 索引, 前提是你的 Mongo db 版本在 (2.2+) 且有权限执行这一操作.
app.use(session({
store: new MongoStore({
url: ‘mongodb://localhost/test-app’,
autoRemove: ‘native’ // Default
})
}));
注意: 这种默认的行为不适用于高并发的情况, 这种情况下你需要禁用默认模式, 然后自行定义 TTl 索引.
使用兼容模式
如果你使用了 Mongodb 的老版本或者不希望创建 TTL 索引, 你可以指定一个间隔时间让 connect-mongo 来删除这些过期的 session.
app.use(session({
store: new MongoStore({
url: ‘mongodb://localhost/test-app’,
autoRemove: ‘interval’,
autoRemoveInterval: 10 // 单位分钟
})
}));
禁用过期 session 删除
app.use(session({
store: new MongoStore({
url: ‘mongodb://localhost/test-app’,
autoRemove: ‘disabled’
})
}));
session 懒更新
如果你使用的 express-session 版本 >=1.10, 然后不希望用户每次浏览页面的时候或刷新页面的时候都要重新保存, 你可以限制一段时间内更新 session.
app.use(express.session({
secret: ‘keyboard cat’,
saveUninitialized: false, // 如果不保存则不会创建 session
resave: false, // 如果未修改则不会保存
store: new MongoStore({
url: ‘mongodb://localhost/test-app’,
touchAfter: 24 * 3600 // 指定触发间隔时间 单位秒
})
}));
通过这样设置 session 只会在 24 小时内触发 1 次无论用户浏览多少次页面或者刷新多少次. 修改 session 除外.
其他选项
collection 指定缓存数据表的名字默认 sessions
fallbackMemory 回退处理默认使用 MemoryStore 进行存储
stringify 默认是 true, 如果为 true 则序列化和反序列化使用原生的 JSON.xxx 处理.
serialize 自定义序列化函数
unserialize 自定义反序列化函数
transformId 将 sessionId 转为你想要的任何键然后进行储存
暗坑
也不算是暗坑吧, 一用有两点:
Mongodb 客户端正常关闭后 connect-mongo 会报错, 虽然会被 Express 拦截但是这个模块没有提供 error 事件.
Express 中间件必须同步挂载? 在我的例子中尝试异步加载 express-session 中间件, 但是失败了中间件没有效果.
connect-mongo 模块 npm 地址
https://www.npmjs.com/package…