多租户Express应用授权层实现方案技术问询
多租户Express API授权层实现方案
针对你的多租户Express API授权需求,结合你给出的项目结构,我整理了一套可落地的实现方案,从核心思路到代码实现一步步拆解:
核心设计思路
我们基于**角色+租户(Company)**的双重校验逻辑来构建授权层:
- superadmin:拥有最高权限,可跨公司执行所有操作,跳过租户校验
- admin:仅能操作所属公司内的所有资源(用户、公司信息等)
- user:仅能操作自身相关的个人资源
1. 构建授权中间件(对应services/authorisation.js)
这是授权层的核心,我们封装不同粒度的权限校验中间件,方便路由层调用:
// services/authorisation.js const User = require('../models/User'); // 校验用户是否有权限访问目标公司(superadmin直接放行,其他角色校验所属公司) exports.checkCompanyAccess = (req, res, next) => { const { companyId, userId } = req.params; const currentUser = req.user; // 由Passport认证后挂载到请求对象 // superadmin直接跳过校验 if (currentUser.role === 'superadmin') { return next(); } // 校验当前用户是否属于目标公司 if (currentUser.companyId.toString() !== companyId.toString()) { return res.status(403).json({ message: '无权限访问该公司资源' }); } // 如果是操作特定用户,额外校验该用户是否属于当前公司(防止admin跨公司操作) if (userId) { User.findById(userId) .then(targetUser => { if (!targetUser || targetUser.companyId.toString() !== currentUser.companyId.toString()) { return res.status(403).json({ message: '无权限操作该用户' }); } next(); }) .catch(err => res.status(500).json({ message: '权限校验失败', error: err })); } else { next(); } }; // 校验用户是否为superadmin exports.checkSuperadmin = (req, res, next) => { if (req.user.role !== 'superadmin') { return res.status(403).json({ message: '仅超级管理员可执行此操作' }); } next(); }; // 校验用户是否为admin或superadmin exports.checkAdminOrSuperadmin = (req, res, next) => { if (!['admin', 'superadmin'].includes(req.user.role)) { return res.status(403).json({ message: '仅管理员或超级管理员可执行此操作' }); } next(); }; // 校验用户是否为自身(用于普通用户修改个人信息场景) exports.checkUserOwnership = (req, res, next) => { const { userId } = req.params; const currentUser = req.user; if (currentUser.role === 'superadmin' || currentUser._id.toString() === userId.toString()) { return next(); } return res.status(403).json({ message: '无权限操作该用户资源' }); };
2. Passport集成(对应services/passport.js)
确保认证成功后,把包含role和companyId的完整用户信息挂载到req.user,为授权校验提供数据支撑:
// services/passport.js const passport = require('passport'); const JwtStrategy = require('passport-jwt').Strategy; const ExtractJwt = require('passport-jwt').ExtractJwt; const User = require('../models/User'); const opts = { jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), secretOrKey: process.env.JWT_SECRET, // 从环境变量读取密钥 }; passport.use(new JwtStrategy(opts, (jwtPayload, done) => { // 查询完整用户信息,包含role和companyId字段 User.findById(jwtPayload.id) .then(user => { if (user) { return done(null, user); // user对象会被挂载到req.user } return done(null, false); }) .catch(err => done(err, false)); })); module.exports = passport;
3. 路由层集成授权中间件
在路由中组合认证和授权中间件,实现接口的权限控制:
公司路由(routers/companies.js)
const express = require('express'); const router = express.Router(); const companiesController = require('../controllers/companies'); const { checkCompanyAccess, checkSuperadmin, checkAdminOrSuperadmin } = require('../services/authorisation'); const passport = require('../services/passport'); // 所有公司接口需先通过JWT认证 router.use(passport.authenticate('jwt', { session: false })); // superadmin可查看所有公司列表 router.get('/', checkSuperadmin, companiesController.getAllCompanies); // 普通admin仅能查看自身公司信息 router.get('/:companyId', checkCompanyAccess, companiesController.getCompanyById); // 仅superadmin可创建公司 router.post('/', checkSuperadmin, companiesController.createCompany); // superadmin或目标公司admin可更新公司信息 router.put('/:companyId', checkAdminOrSuperadmin, checkCompanyAccess, companiesController.updateCompany); // 仅superadmin可删除公司 router.delete('/:companyId', checkSuperadmin, companiesController.deleteCompany); module.exports = router;
用户路由(routers/users.js)
const express = require('express'); const router = express.Router(); const usersController = require('../controllers/users'); const { checkCompanyAccess, checkAdminOrSuperadmin, checkUserOwnership } = require('../services/authorisation'); const passport = require('../services/passport'); const User = require('../models/User'); router.use(passport.authenticate('jwt', { session: false })); // superadmin或目标公司admin可查看公司内所有用户 router.get('/company/:companyId', checkAdminOrSuperadmin, checkCompanyAccess, usersController.getUsersByCompany); // superadmin或目标公司admin可创建用户 router.post('/', checkAdminOrSuperadmin, (req, res, next) => { // 将请求体中的companyId传入权限校验中间件 req.params.companyId = req.body.companyId; checkCompanyAccess(req, res, next); }, usersController.createUser); // superadmin或用户所属公司admin可更新用户信息 router.put('/:userId', checkAdminOrSuperadmin, (req, res, next) => { // 先查询目标用户所属公司,再执行权限校验 User.findById(req.params.userId) .then(targetUser => { req.params.companyId = targetUser.companyId; checkCompanyAccess(req, res, next); }) .catch(err => res.status(500).json({ message: '权限校验失败', error: err })); }, usersController.updateUser); // 普通用户仅能修改自身信息 router.put('/profile/:userId', checkUserOwnership, usersController.updateUserProfile); // 仅superadmin可删除用户 router.delete('/:userId', checkSuperadmin, usersController.deleteUser); module.exports = router;
4. 模型层辅助优化(可选)
在User模型中添加静态方法,简化权限校验逻辑:
// models/User.js const mongoose = require('mongoose'); const userSchema = new mongoose.Schema({ username: { type: String, required: true, unique: true }, role: { type: String, enum: ['superadmin', 'admin', 'user'], default: 'user' }, companyId: { type: mongoose.Schema.Types.ObjectId, ref: 'Company', required: true }, }); // 静态方法:校验用户是否有权限访问目标公司 userSchema.statics.hasCompanyAccess = function(userId, targetCompanyId) { return this.findById(userId) .then(user => { if (!user) return false; if (user.role === 'superadmin') return true; return user.companyId.toString() === targetCompanyId.toString(); }); }; module.exports = mongoose.model('User', userSchema);
之后你可以在授权中间件中调用User.hasCompanyAccess(req.user._id, companyId)来替代重复的校验逻辑。
5. 边界场景处理
- 资源不存在校验:在权限校验前先校验资源(如Company、User)是否存在,避免泄露资源存在性信息
- 跨公司操作拦截:确保admin角色无法访问其他公司的任何资源
- 权限粒度细化:可根据业务需求添加更多角色或权限规则(如readonly admin)
内容的提问来源于stack exchange,提问作者Ashishkumar Pandey
相关产品推荐
相关产品推荐

