基于三段式命令及筛选器的rbac权限控制方案
admin
2024-04-05 19:38:52
0

引用自 摸鱼wiki

1. 权限命令的表示

1.1 权限命令

采用三段式设计,将命令划分为类型、操作、属性/对象三个层级。相较于采用常量声明,这样的写法可以支持使用通配符进行设置,减少调用时的代码量。

// 常量
File_Add
File_Delete
File_Switch_Page
File_Switch_Step// 三段式
File::Add
File::Delete
File::Switch::Page
File::Switch::Step// 通配符(常量无法做到)
File::Switch::* = [File::Switch::Page,File::Switch::Step
]
File::*::* = [File::Add,File::Delete,File::Switch::Page,File::Switch::Step
]

1.2 权限作用域筛选器

形如 属性名/满足条件的属性值 结构,表示允许通过当前权限的必要属性以及对应的属性值。

// 属性名/满足条件的值
creator/user1  仅允许具有creator属性,且值为user1的对象进行操作
operator/user2  仅允许操作者为user2进行操作
color/red,black 仅允许具有color属性,且值为red或者black##的对象进行操作

2. 权限校验模块

该模块主要分为五大功能:权限命令解析,权限作用域筛选器解析,添加权限命令,移除权限命令以及权限校验。

2.1 权限命令解析

权限命令是三段式命令,中间采用 :: 隔开。因此只要按照规范进行字符串分拆即可。

/*** 权限命令解析器* @param permissionName 权限命令*/
function _cmdParser(permissionName: string) {const [type, action = '*', attr = '*'] = permissionName.split('::');return [type, action, attr];
}

由于允许用户传入通配符格式的权限命令,为了方便计算,内部需要把通配符命令转换成具名命令,方便内部进行匹配

/*** 预定义的权限命令表*/
PermissionHandler.PermissionMap: { [type: string]: { [action: string]: { [attr: string]: boolean } } } = {};/*** 判断命令是否合法* @param cmd 解析后的命令*/
function _valid(cmd: string[]) {const [type, action, attr] = cmd;if (type === '*' || action == '*') {return true;}return !!(PermissionHandler.PermissionMap[type] &&PermissionHandler.PermissionMap[type][action] &&PermissionHandler.PermissionMap[type][action][attr]);
}/*** 获取显式权限命令(Type和Action不能为*)* @param cmd 权限命令* @returns*/
function _extractExplicitPermission(cmd: readonly string[]) {let typeKeys: string[] = [];let actionKeys: string[][] = [];let explicitPermission: string[] = [];if (cmd[0] === '*') {typeKeys = Object.keys(PermissionHandler.PermissionMap);} else {typeKeys = [cmd[0]];}if (cmd[1] === '*') {typeKeys.forEach((typeKey) => {actionKeys.push(Object.keys(PermissionHandler.PermissionMap[typeKey]));});} else {typeKeys.forEach((typeKey) => {actionKeys.push([cmd[1]]);});}for (let i = 0; i < typeKeys.length; i++) {for (let j = 0; j < actionKeys[i].length; j++) {const newCmd = [typeKeys[i], actionKeys[i][j], cmd[2]];if (_valid(newCmd)) {explicitPermission.push(newCmd.join('::'));}}}return explicitPermission;
}

2.2 权限作用域筛选器解析

权限作用域筛选器的格式形如 x/y,z,而且输入可能出现 [x/y, x/z],因此需要把相同属性名进行聚合,并且保证满足条件的属性值不重复。为了提高查找时的性能,这里采用了 Map + Set 结构进行存储,保证在查找时满足 O(1) 时间复杂度。

/*** 筛选器解析器* @param filters 筛选器列表(形如:creator/xxx)*/
function _filterParser(filters: readonly string[]): Map> {const filterMap = new Map>();for (const filter of filters) {const [attr, conditions] = filter.split('/');const nSet = new Set(conditions.split(','));const oSet = filterMap.get(attr);if (oSet) {filterMap.set(attr, new Set([...oSet, ...nSet]));} else {filterMap.set(attr, nSet);}}return filterMap;
}

2.3 添加权限命令

这一模块用于批量添加权限命令及其对应的筛选器。主要工作流程如下:

  1. 解析作用域筛选器
  2. 解析命令,并提取具名权限命令
  3. 具名权限命令设置作用域筛选器
/*** 权限拦截器存储结构*/
const interceptorMap = new Map>>();/*** 添加权限拦截器* @param permissions 权限名称* @param filter 筛选器*/
function setInterceptor(permissions: string[], filter: readonly string[]) {let explicitCmds: string[] = [];// 解析作用域筛选器const filters = _filterParser(filter);// 解析命令,并提取具名权限命令permissions.forEach((permissionName: string) => {const cmd = _cmdParser(permissionName);if (!_valid(cmd)) {return;}const explicitCmd = _extractExplicitPermission(cmd);explicitCmds = explicitCmds.concat(explicitCmd);});// 具名权限命令设置作用域筛选器explicitCmds.forEach((dCmd) => {const newFilters = filters;if (interceptorMap.has(dCmd)) {const old = interceptorMap.get(dCmd);old?.forEach((val, key) => {if (!newFilters.has(key)) {newFilters.set(key, val);}})}interceptorMap.set(dCmd, newFilters);});
}

2.4 移除权限命令

这一模块用于批量删除权限命令及其对应的筛选器。主要工作流程如下:

  1. 解析命令,并提取具名权限命令
  2. 移除对应权限命令
/*** 移除权限拦截器* @param permissions 权限名称*/
removeInterceptor(permissions: string[]) {let explicitCmds: string[] = [];permissions.forEach((permissionName: string) => {const cmd = _cmdParser(permissionName);if (!_valid(cmd)) {return;}const explicitCmd = _extractExplicitPermission(cmd);explicitCmds = explicitCmds.concat(explicitCmd);});explicitCmds.forEach((dCmd) => {interceptorMap.delete(dCmd);});
}

2.5 权限校验

这个功能是整个模块的灵魂,前面的所有功能都是为其服务的。其主要工作流程可划分为下列模块:

2.5.1 解析命令

const cmd = _cmdParser(permissionName);
// 命令不合法,跳过权限校验
if (!_valid(cmd)) {return true;
}

2.5.2 获取最高权限对应的作用域筛选器

由于权限命令是三段命令式,用户可能在不同的层级上设置了不同的权限作用域筛选器。因此,需要给各个层级定义筛选器生效的优先级。

本方案的优先级定义是:

三级具名命令 > 三级模糊命令 > 二级具名命令 > 二级模糊命令 > 一级具名命令 > 一级模糊命令

// 以 File::Switch::Page 举例
// 优先级从高到低依次为File::Switch::Page
File::Switch::*
File::*::*
*::*::*

对应的代码实现为:

/*** 获取最高权限的筛选器* @param cmd 权限命令*/
function getHighestAuthorityFilter(cmd: readonly string[]) {const realCmd = cmd.concat([]);let loop = 3;let cmdStr = '';while (loop) {cmdStr = realCmd.join('::');if (interceptorMap.has(cmdStr)) {return interceptorMap.get(cmdStr);}realCmd[--loop] = '*';}return undefined;
}

2.5.3 权限可通过性判断

conditions 为待判断的对象,通过遍历对象上与作用域筛选器相同的字段,检测对象上的对应值是否在筛选器的可通过条件列表中。如是,返回校验通过,否则,返回校验不通过。

function checkHasPermission(conditions: any[], filters: Map>) {let hasPermission = true;if (filters) {filters.forEach((valSet, attr) => {// 短路运算(已评为不满足/属性值带有*)if (!hasPermission || valSet.has('*')) return;// 任意属性均需要满足条件if (attr === '*') {for (const obj of conditions) {for (const attrKey of Object.keys(obj)) {if (!valSet.has(obj[attrKey])) {hasPermission = false;break;}}if (!hasPermission) {break;}}} else {for (const obj of conditions) {if (!valSet.has(obj[attr])) {hasPermission = false;break;}}}});}return hasPermission;
}

2.5.4 模块代码总览

/*** 判断是否具有操作权限* @param permissionName 权限名称* @param conditions 受影响的对象*/
function checkPermission(permissionCmd: string, conditions: readonly { [key: string]: any }[]): boolean {// 解析命令const cmd = _cmdParser(permissionCmd);if (!_valid(cmd)) {return true;}// 获取最高权限对应的作用域筛选器const filters = getHighestAuthorityFilter(cmd);// 权限可通过性判断return checkHasPermission(conditions, filters);
}

3. 使用案例

// 禁用全部操作权限setInterceptor(['*::*::*'], ['operator/']);
// 'operator/' 表示对于操作者(operator)这个条件,符合的值是空的,意味着没有操作者能够被允许通过这个权限校验checkPermission('File::Switch::Page', [{ operator: 'xxx' }])
// false
// 启用全部操作权限setInterceptor(['*::*::*'], ['operator/*']);
// 'operator/*' 表示对于操作者(operator)这个条件,符合的值是任意的,意味着所有操作者能够被允许通过这个权限校验checkPermission('File::Switch::Page', [{ operator: 'xxx' }])
// true

相关内容

热门资讯

“双标”换卡背后,银行还需多些... 新华社记者 颜之宏、杨深深 持到期银行卡和身份证去银行网点换新卡,却被要求“必须交回旧卡才能取新卡”...
“离境退税2.0”带动“中国购... 【环球时报综合报道】编者的话:5月18日,商务部等6部门联合发布《关于加力优化离境退税措施扩大入境消...
一年烧掉2000亿、市值蒸发3... 商业润点 |Biz Run Review 三国归晋,用了六十年。即时零售的"三国杀",才刚刚开局...
原创 金... 2026年5月22日,国内黄金市场呈现出令人咋舌的价格鸿沟。基础金价徘徊在每克995.3元,而回收价...
原创 人... SpaceX的星舰V3终于在全球瞩目中成功升空。北京时间5月23日清晨,这颗高达124米的巨型火箭顺...
原创 被... 5月19日,欧洲议会掀起了一场引人注目的风暴,以压倒性的票数通过了最新的钢铁进口规定。 这套规则...
光纤量价齐升,烽火通信加快布局... 烽火通信(600498)5月22日披露的投资者关系活动记录表显示,公司于5月21日参加了中国信息通信...
原创 突... 今天5月24日一大早,打开行情一看,国际现货黄金报4508.25美元/盎司,单日跌了26.68美元,...
企业快讯 | 携手联通!狄耐克... 狄耐克 厦门总商会副会长企业 厦门狄耐克智能科技股份有限公司 与中国联通厦门分公司 将5G智慧“嵌入...
美银策略师警告:SpaceX与... 环球网 据彭博社报道,美国银行首席投资策略师迈克尔·哈特奈特(Michael Hartnett)最新...
卸任55天后,知名基金经理任相... 【导读】卸任55天后,知名基金经理任相栋“奔私”谜底揭晓 见习记者 闫军 知名基金经理任相栋“奔私”...
原创 大... “免签+手机刷一切”就能让老外连夜订机票?2026年一季度,阿根廷人来华暴涨九倍,北京三源里菜市场三...
从泰山顶峰掉落!“大佬背后的大... 文/刘工昌 他曾是柳传志的“大哥”,助力联想完成混合所有制改革;是史玉柱眼中的“贵人”,帮他东山再起...
原创 2... 最近网上流传出一份2030年GDP10强预测榜单,其中一些城市位次的变化也挺有趣的。上海排在第一,深...
原创 全... 2026年3月的全球美债市场迎来剧烈变动,彻底打破了长期稳定的持仓格局。 根据美国财政部发布的国际资...
全球都在给这几只“疯牛”烧钱 近段时间,AI行情再次成为全球资本市场主线,但舞台中央的“主角”发生了变化:投资者不再只偏好云厂商和...
【财闻联播】“硬刚监管”?老虎... ★ 宏观动态 ★ 商务部:1—4月全国吸收外资2876.9亿元人民币 据商务部网站,2026年1—4...
燕京啤酒营收净利双增:U8增速... 蓝鲸新闻5月22日讯(记者 朱欣悦)燕京啤酒(000729.SZ)打了一个翻身仗。 2025年燕京啤...
原创 帮... 老铁们,这周有个事儿挺有意思,估计不少基民都看懵了:都说科技是主线,芯片是未来,可数据显示,年内火爆...
4家银行AIC现身存储巨头股东... 近日,资本市场热度颇高的两家存储巨头长鑫科技集团股份有限公司(以下简称“长鑫科技”)、长江存储控股股...