Express+Mongoose多租户分库连接及访问令牌实现问题咨询
解决方案
一、Mongoose多连接实现方案
你之前多次调用mongoose.connect()报错的原因是该方法只会复用Mongoose的默认连接,多次调用会覆盖原有连接配置,Mongoose本身支持多独立连接,需要使用mongoose.createConnection()方法实现,具体实现逻辑如下:
- 全局维护一个连接缓存对象,key为clientId,value存储对应数据库的连接实例、当前活跃用户数
- 同clientId下的用户登录时,直接复用已有连接,仅新增活跃用户计数;不存在对应连接时才新建
- 当clientId对应的活跃用户数归0时,主动断开该数据库连接
代码示例:
1. 公共库连接初始化(server.js)
// 公共库用默认连接即可 const mongoose = require('mongoose'); mongoose.connect('mongodb+srv://user:pass@host/DB_Common'); // 全局连接缓存 global.clientDbConnections = new Map();
2. 客户端连接管理工具(utils/dbManager.js)
const mongoose = require('mongoose'); // 新建/获取客户端连接 async function getClientDbConnection(clientId) { // 已存在连接直接返回 if (global.clientDbConnections.has(clientId)) { const connInfo = global.clientDbConnections.get(clientId); connInfo.activeUsers += 1; return connInfo.connection; } // 新建连接 const conn = await mongoose.createConnection(`mongodb+srv://user:pass@host/DB_Client${clientId}`, { // 配置自动重连 autoReconnect: true, maxPoolSize: 10 }); // 存入缓存 global.clientDbConnections.set(clientId, { connection: conn, activeUsers: 1 }); return conn; } // 断开客户端连接(用户登出时调用) async function releaseClientDbConnection(clientId) { if (!global.clientDbConnections.has(clientId)) return; const connInfo = global.clientDbConnections.get(clientId); connInfo.activeUsers -= 1; // 活跃用户为0时断开连接 if (connInfo.activeUsers <= 0) { await connInfo.connection.close(); global.clientDbConnections.delete(clientId); } } module.exports = { getClientDbConnection, releaseClientDbConnection };
3. 专属库模型定义注意
不要用默认的mongoose.model注册专属库的模型,要绑定对应连接实例注册:
// 示例:定义订单模型,绑定到指定的client连接 function getOrderModel(conn) { const orderSchema = new mongoose.Schema({ orderNo: String, amount: Number }); // 连接实例不存在模型时才注册,避免重复注册报错 return conn.models.Order || conn.model('Order', orderSchema); }
二、访问令牌实现逻辑
采用JWT作为访问令牌,完整逻辑如下:
- 登录签发阶段
用户提交账号密码请求登录,先查询公共库DB_Common校验用户凭证有效性,同时获取用户所属的clientId。校验通过后生成JWT,payload中存入userId、clientId、过期时间戳,签名后返回给前端。 - 请求校验中间件
const jwt = require('jsonwebtoken'); const { getClientDbConnection } = require('./utils/dbManager'); async function authMiddleware(req, res, next) { const token = req.headers.authorization?.split(' ')[1]; if (!token) return res.status(401).send('未登录'); try { // 校验token有效性,同时校验是否在黑名单(登出的token加入黑名单) const payload = jwt.verify(token, process.env.JWT_SECRET); const isBlack = await mongoose.model('TokenBlack').findOne({ token }); if (isBlack) return res.status(401).send('token已失效'); // 挂载用户信息到req对象 req.userId = payload.userId; req.clientId = payload.clientId; // 预获取对应client的数据库连接,存入req供业务逻辑调用 req.clientDb = await getClientDbConnection(payload.clientId); next(); } catch (err) { return res.status(401).send('token无效'); } }
- 业务逻辑调用示例
// 查询当前用户所属client的订单列表 app.get('/api/orders', authMiddleware, async (req, res) => { const OrderModel = getOrderModel(req.clientDb); const orders = await OrderModel.find({ userId: req.userId }); res.send(orders); });
- 登出逻辑
用户发起登出请求时,拿到请求携带的token,存入公共库的TokenBlack黑名单集合(设置过期时间和token本身的过期时间一致,自动清理),同时调用releaseClientDbConnection(req.clientId)减少对应连接的活跃用户计数。
注意事项
- 可以给JWT设置较短的过期时间(比如2小时),搭配刷新令牌实现无感续期,降低token泄露风险
- 连接缓存可以根据业务需要设置超时自动清理,避免长时间无活跃的连接占用资源
- 多连接场景下注意连接池大小配置,避免MongoDB连接数超限
内容的提问来源于stack exchange,提问作者Alejandro
相关产品推荐
相关产品推荐

