Express使用mongodb管理会话储存 connect-mongo模块简介

简介

在我的前一篇小文中express-session小书提到了express-session可以更换会话储存.

那么这篇文章我们就来讲讲express在进行会话管理的时候如何将会话数据保存在外部数据库中,本文中我们使用mongodb用作会话储存数据库.

本文中使用的模块以及版本号一览:

模块名称版本号
express4.16.4
mongodb3.1.8
express-session1.15.6
connect-mongo2.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"}' }

使用总结

  1. 引入connect-mongoexpress-session然后调用connect-mongoexpress-sessino传入
  2. 获取上一步返回的类,然后使用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实例有如下的事件:

事件名称描述回调参数
createsession创建后触发sessionId
touchsession被获取但是未修改sessionId
updatesession被更新sessionId
setsession创建后或者更新后(为了兼容)sessionId
destroysession被销毁后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转为你想要的任何键然后进行储存

暗坑

也不算是暗坑吧,一用有两点:

  1. Mongodb客户端正常关闭后connect-mongo会报错,虽然会被Express拦截但是这个模块没有提供error事件.
  2. Express中间件必须同步挂载?在我的例子中尝试异步加载express-session中间件,但是失败了中间件没有效果.

connect-mongo模块npm地址

https://www.npmjs.com/package...

相关推荐