查询接口使用 ABAC 策略模型

ABAC 策略处理逻辑扩展为一个 通用 Node.js 类,它可以根据策略自动生成 MongoDB 查询过滤条件,用于在「列表查询接口」中动态控制数据可见性。


🎯 设计目标

这个类要做到以下几点:

  1. 支持加载多条策略;
  2. 根据用户信息(roles, attributes 等)和操作类型(action)生成 MongoDB 查询过滤条件;
  3. 支持组合策略(多策略叠加);
  4. 支持多种条件表达式(==, !=, in, not in, and, or, ><>=<=等);
  5. 输出标准 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 配置并实时生效