查询接口使用 ABAC 策略模型
ABAC 策略处理逻辑扩展为一个 通用 Node.js 类,它可以根据策略自动生成 MongoDB 查询过滤条件,用于在「列表查询接口」中动态控制数据可见性。
🎯 设计目标
这个类要做到以下几点:
- 支持加载多条策略;
- 根据用户信息(roles, attributes 等)和操作类型(action)生成 MongoDB 查询过滤条件;
- 支持组合策略(多策略叠加);
- 支持多种条件表达式(
==,!=,in,not in,and,or,>,<,>=,<=等); - 输出标准 MongoDB 查询对象,可直接传入
Model.find(filter)。
🧩 策略示例
const policies = [
{
name: 'operator-view-online',
effect: 'allow',
subjectExpr: 'operator',
resourceExpr: 'device',
action: 'view',
condition: {
and: [
{ in: ['operator', { var: 'user.roles' }] },
{ '==': [{ var: 'resource.status' }, 'online'] }
]
},
priority: 5
},
{
name: 'manager-view-all',
effect: 'allow',
subjectExpr: 'manager',
resourceExpr: 'device',
action: 'view',
condition: {
in: ['manager', { var: 'user.roles' }]
},
priority: 10
}
];
🧱 通用 ABAC 查询构造类
class ABACQueryBuilder {
constructor(policies = []) {
this.policies = policies;
}
/**
* 生成 MongoDB 查询过滤条件
* @param {Object} user - 用户信息,如 { id, roles, projectId, department }
* @param {String} action - 动作,如 'view', 'edit'
* @param {String} resource - 资源类型,如 'device', 'product'
* @returns {Object} MongoDB 查询条件
*/
buildQuery(user, action, resource) {
const matchedPolicies = this.policies
.filter(p => p.action === action && p.resourceExpr === resource)
.sort((a, b) => b.priority - a.priority);
if (matchedPolicies.length === 0) {
return { _id: { $exists: false } }; // 无策略则拒绝访问
}
const allowFilters = [];
const denyFilters = [];
for (const policy of matchedPolicies) {
const condition = this._evaluateCondition(policy.condition, user);
if (!condition) continue;
const filter = this._conditionToMongo(policy.condition, user);
if (policy.effect === 'allow') {
allowFilters.push(filter);
} else {
denyFilters.push(filter);
}
}
// 最终 MongoDB 查询组合规则
let query = {};
if (allowFilters.length > 0) {
query = { $or: allowFilters };
}
if (denyFilters.length > 0) {
query = { $and: [query, { $nor: denyFilters }] };
}
return query;
}
/**
* 将条件表达式转换为 MongoDB 查询过滤条件
*/
_conditionToMongo(condition, user) {
if (!condition) return {};
if (condition.and) {
return { $and: condition.and.map(c => this._conditionToMongo(c, user)) };
}
if (condition.or) {
return { $or: condition.or.map(c => this._conditionToMongo(c, user)) };
}
const key = Object.keys(condition)[0];
const [left, right] = condition[key];
const leftVal = this._resolveVar(left, user);
const rightVal = this._resolveVar(right, user);
switch (key) {
case '==':
if (this._isResourceVar(left)) {
return { [leftVal]: rightVal };
} else if (this._isResourceVar(right)) {
return { [rightVal]: leftVal };
}
return {};
case '!=':
if (this._isResourceVar(left)) {
return { [leftVal]: { $ne: rightVal } };
}
return {};
case 'in':
if (this._isResourceVar(left)) {
return { [leftVal]: { $in: rightVal } };
} else if (this._isResourceVar(right)) {
return { [rightVal]: { $in: leftVal } };
}
return {};
case 'not in':
if (this._isResourceVar(left)) {
return { [leftVal]: { $nin: rightVal } };
}
return {};
case '>':
if (this._isResourceVar(left)) {
return { [leftVal]: { $gt: rightVal } };
}
break;
case '>=':
if (this._isResourceVar(left)) {
return { [leftVal]: { $gte: rightVal } };
}
break;
case '<':
if (this._isResourceVar(left)) {
return { [leftVal]: { $lt: rightVal } };
}
break;
case '<=':
if (this._isResourceVar(left)) {
return { [leftVal]: { $lte: rightVal } };
}
break;
default:
return {};
}
return {};
}
/**
* 判断 { var: 'xxx' } 是否为资源变量
*/
_isResourceVar(value) {
return typeof value === 'object' && value.var && value.var.startsWith('resource.');
}
/**
* 从 { var: 'user.xxx' } 中解析变量
*/
_resolveVar(expr, user) {
if (typeof expr !== 'object' || !expr.var) return expr;
const path = expr.var.split('.');
if (path[0] === 'user') {
return path.slice(1).reduce((obj, key) => obj?.[key], user);
} else if (path[0] === 'resource') {
return path.slice(1).join('.'); // 资源字段返回为路径字符串
}
return expr;
}
/**
* 简单评估条件(判断用户是否符合该策略)
*/
_evaluateCondition(condition, user) {
if (!condition) return true;
if (condition.and) return condition.and.every(c => this._evaluateCondition(c, user));
if (condition.or) return condition.or.some(c => this._evaluateCondition(c, user));
const key = Object.keys(condition)[0];
const [left, right] = condition[key];
const leftVal = this._resolveVar(left, user);
const rightVal = this._resolveVar(right, user);
switch (key) {
case '==': return leftVal === rightVal;
case '!=': return leftVal !== rightVal;
case 'in': return Array.isArray(rightVal) ? rightVal.includes(leftVal) : false;
case 'not in': return Array.isArray(rightVal) ? !rightVal.includes(leftVal) : false;
case '>': return leftVal > rightVal;
case '>=': return leftVal >= rightVal;
case '<': return leftVal < rightVal;
case '<=': return leftVal <= rightVal;
default: return false;
}
}
}
✅ 使用示例
const user = { id: 'u1', roles: ['operator'], projectId: 'p1' };
const qb = new ABACQueryBuilder(policies);
// 生成设备列表过滤条件
const mongoFilter = qb.buildQuery(user, 'view', 'device');
console.log(JSON.stringify(mongoFilter, null, 2));
/*
输出:
{
"$or": [
{ "status": "online" }
]
}
*/
// 可直接用于 Mongoose 查询
// DeviceModel.find(mongoFilter)
🧠 特点总结
- 策略驱动查询:无须在代码中硬编码角色逻辑;
- 灵活扩展:新增策略只需在数据库或配置中定义;
- 兼容性好:输出标准 MongoDB 查询条件;
- 安全可控:未匹配策略默认拒绝访问。
非常好 👍,我们现在在上面的 ABACQueryBuilder 基础上,扩展出一个适用于 Node.js + Express + MongoDB (Mongoose) 的 通用权限中间件系统。
这个版本的目标是让 ABAC 策略与接口访问无缝结合,自动实现:
- 在列表查询时自动附加 MongoDB 权限过滤;
- 在详情、修改、删除操作时自动验证访问是否被允许;
- 支持 RBAC + ABAC 混合判断;
- 可热加载策略配置(从数据库或文件读取)。
🧩 系统结构概览
src/
├── auth/
│ ├── abac-query-builder.js # 通用策略查询类
│ ├── policy-service.js # 策略加载与缓存
│ ├── abac-middleware.js # Express中间件
│
├── models/
│ ├── device.js
│ ├── user.js
│
├── routes/
│ ├── device.js # 示例接口:设备列表/详情
│
└── app.js # 入口
封装abac middleeware
一、abac-query-builder.js
(上一步的核心逻辑,略做调整以便在中间件中使用)
// src/auth/abac-query-builder.js
class ABACQueryBuilder {
constructor(policies = []) {
this.policies = policies;
}
setPolicies(policies) {
this.policies = policies;
}
buildQuery(user, action, resource) {
const matchedPolicies = this.policies
.filter(p => p.action === action && p.resourceExpr === resource)
.sort((a, b) => b.priority - a.priority);
if (matchedPolicies.length === 0) {
return { _id: { $exists: false } }; // 无策略默认拒绝
}
const allowFilters = [];
const denyFilters = [];
for (const policy of matchedPolicies) {
if (!this._evaluateCondition(policy.condition, user)) continue;
const filter = this._conditionToMongo(policy.condition, user);
if (policy.effect === 'allow') allowFilters.push(filter);
else denyFilters.push(filter);
}
let query = allowFilters.length > 0 ? { $or: allowFilters } : {};
if (denyFilters.length > 0) query = { $and: [query, { $nor: denyFilters }] };
return query;
}
_conditionToMongo(condition, user) {
if (!condition) return {};
if (condition.and) return { $and: condition.and.map(c => this._conditionToMongo(c, user)) };
if (condition.or) return { $or: condition.or.map(c => this._conditionToMongo(c, user)) };
const key = Object.keys(condition)[0];
const [left, right] = condition[key];
const leftVal = this._resolveVar(left, user);
const rightVal = this._resolveVar(right, user);
switch (key) {
case '==': return { [leftVal]: rightVal };
case '!=': return { [leftVal]: { $ne: rightVal } };
case 'in': return { [leftVal]: { $in: rightVal } };
case 'not in': return { [leftVal]: { $nin: rightVal } };
case '>': return { [leftVal]: { $gt: rightVal } };
case '>=': return { [leftVal]: { $gte: rightVal } };
case '<': return { [leftVal]: { $lt: rightVal } };
case '<=': return { [leftVal]: { $lte: rightVal } };
default: return {};
}
}
_resolveVar(expr, user) {
if (typeof expr !== 'object' || !expr.var) return expr;
const path = expr.var.split('.');
if (path[0] === 'user') return path.slice(1).reduce((o, k) => o?.[k], user);
if (path[0] === 'resource') return path.slice(1).join('.');
return expr;
}
_evaluateCondition(condition, user) {
if (!condition) return true;
if (condition.and) return condition.and.every(c => this._evaluateCondition(c, user));
if (condition.or) return condition.or.some(c => this._evaluateCondition(c, user));
const key = Object.keys(condition)[0];
const [left, right] = condition[key];
const leftVal = this._resolveVar(left, user);
const rightVal = this._resolveVar(right, user);
switch (key) {
case '==': return leftVal === rightVal;
case '!=': return leftVal !== rightVal;
case 'in': return Array.isArray(rightVal) ? rightVal.includes(leftVal) : false;
case 'not in': return Array.isArray(rightVal) ? !rightVal.includes(leftVal) : false;
case '>': return leftVal > rightVal;
case '>=': return leftVal >= rightVal;
case '<': return leftVal < rightVal;
case '<=': return leftVal <= rightVal;
default: return false;
}
}
}
module.exports = ABACQueryBuilder;
二、policy-service.js
策略集中加载与管理层,可以从 MongoDB / Redis / 文件读取。
// src/auth/policy-service.js
const ABACQueryBuilder = require('./abac-query-builder');
class PolicyService {
constructor() {
this.policies = [];
this.qb = new ABACQueryBuilder();
}
async loadPolicies() {
// 实际场景:从 MongoDB / Redis / Config Server 加载
this.policies = [
{
name: 'operator-view-online',
effect: 'allow',
subjectExpr: 'operator',
resourceExpr: 'device',
action: 'view',
condition: {
and: [
{ in: ['operator', { var: 'user.roles' }] },
{ '==': [{ var: 'resource.status' }, 'online'] }
]
},
priority: 5
},
{
name: 'manager-view-all',
effect: 'allow',
subjectExpr: 'manager',
resourceExpr: 'device',
action: 'view',
condition: { in: ['manager', { var: 'user.roles' }] },
priority: 10
}
];
this.qb.setPolicies(this.policies);
}
getQueryBuilder() {
return this.qb;
}
}
module.exports = new PolicyService();
三、abac-middleware.js
核心中间件,用于自动注入过滤条件。
// src/auth/abac-middleware.js
const policyService = require('./policy-service');
/**
* Express 中间件:注入 ABAC 查询过滤条件
*/
async function abacFilter(resource, action) {
return async (req, res, next) => {
const user = req.user; // 假设身份认证中间件已注入 req.user
const qb = policyService.getQueryBuilder();
const filter = qb.buildQuery(user, action, resource);
req.abacFilter = filter;
next();
};
}
/**
* 操作权限验证(详情、编辑、删除)
*/
async function abacAuthorize(resource, action) {
return async (req, res, next) => {
const user = req.user;
const qb = policyService.getQueryBuilder();
const filter = qb.buildQuery(user, action, resource);
const Model = req.Model; // 由上层 route 注入
const target = await Model.findOne({ _id: req.params.id, ...filter });
if (!target) {
return res.status(403).json({ message: 'Forbidden: ABAC denied' });
}
next();
};
}
module.exports = { abacFilter, abacAuthorize };
四、routes/device.js
示例接口:设备列表 / 详情。
// src/routes/device.js
const express = require('express');
const router = express.Router();
const Device = require('../models/device');
const { abacFilter, abacAuthorize } = require('../auth/abac-middleware');
router.get('/',
abacFilter('device', 'view'),
async (req, res) => {
const devices = await Device.find(req.abacFilter);
res.json(devices);
}
);
router.get('/:id',
(req, res, next) => { req.Model = Device; next(); },
abacAuthorize('device', 'view'),
async (req, res) => {
const device = await Device.findById(req.params.id);
res.json(device);
}
);
module.exports = router;
五、app.js
// src/app.js
const express = require('express');
const mongoose = require('mongoose');
const policyService = require('./auth/policy-service');
const deviceRoutes = require('./routes/device');
const app = express();
app.use(express.json());
// 假设已有认证中间件注入 user
app.use((req, res, next) => {
req.user = { id: 'u1', roles: ['operator'], projectId: 'p1' };
next();
});
mongoose.connect('mongodb://localhost:27017/iot_saas');
(async () => {
await policyService.loadPolicies(); // 启动时加载策略
app.use('/devices', deviceRoutes);
app.listen(3000, () => console.log('✅ Server running at http://localhost:3000'));
})();
✅ 测试效果
用户角色
operator:/devices只能看到status = "online"的设备;/devices/:id只能访问在线设备;
用户角色
manager:- 可查看所有设备;
如果没有匹配策略 → 自动拒绝访问。
🧠 扩展建议
| 功能 | 说明 |
|---|---|
| 策略动态刷新 | |
| 策略优先级 | priority 字段可用于覆盖低优先级策略 |
| 缓存策略结果 | 结合 Redis 缓存用户策略判断结果 |
| 组合 RBAC + ABAC | 先通过角色过滤(RBAC),再执行 ABAC 精细校验 |
| 管理后台策略配置界面 | 策略 JSON 可通过 UI 配置并实时生效 |