RBAC权限管理

RBAC权限管理

近 2 年一直使用蚂蚁金服的 Ant Design UI 框架以及其开箱即用的中台前端/设计解决方案 ANT DESIGN PRO (去年的圣诞风波有点影响,希望不再发生类似的事情),框架是一直更新一直迭代,不过里面涉及权限管理的部分的使用场景还是比较有限,兼容不了需要细化到各模块中的具体动作的场景。授人以鱼不如授人以渔,没有就自己撸一个呗。

设计思想

虽然是自己撸,但还是得站在前辈的肩膀上,离开设计的代码都不够优雅。向我司的校长(霸气绰号,具体为什么叫校长可以在 https://www.luweitech.cn/ 上找找,可能能找到 (#^.^#))学习——写代码要写得像诗一样优雅。

找了一圈,最终选了一个设计思想——RBAC,RBAC 以角色为基础的访问控制(英语:Role-based access control,RBAC),简单可以归纳为 who、what、how,即,who 对 what进行了 how 的操作,翻译成广东话就系:“有一个靓仔企一个野里面做左滴野”。

一张简单的图(图是盗来的~)理解:

image

即,张三、李四是“销售角色”,而“销售角色拥有查看“客户列表”和“编辑客户”两个动作的权限,自然而然的,张三、李四就拥有查看“客户列表”和“编辑客户”两个动作的权限。

完整一点就是(图也是盗来的):

image

由上可以看出,核心就三步:

  1. 定义角色
  2. 授权角色拥有的权限
  3. 给用户指定角色

准备

我觉得核心还是上面的设计思路,具体的代码实现只是思路的表达,后续封装得更通用再放出完整版出来吧。

ps:使用的 ant-design-pro 版本是 2.2.1,有比较多旧系统,还没一下子升级到最新的,各位可以用最新来撸

授权角色拥有的权限

定义角色这一步比较简单,就直接跳过了~

先说第二步,给角色授权权限。先上效果图:

image

这一步有几个关键步骤:

  1. router.config.js 转化成上图中用于展示数据
  2. 构建好上图中的交互逻辑
  3. 把用户选择的权限按特定格式发给后台

一部分 router.config.js,如下

export default [
  // user
  ...节省位置,省略

  // app
  {
    path: '/',
    component: '../layouts/BasicLayout',
    Routes: ['src/pages/Authorized'],
    routes: [
      {
        path: '/',
        redirect: '/welcome',
      },

      {
        name: 'welcome',
        path: '/welcome',
        icon: 'smile',
        component: './Welcome/Welcome',
        power: ['MENU'],
      },

      {
        name: 'revenueManagement',
        path: '/revenueManagement',
        icon: 'pay-circle',
        power: ['MENU'],
        routes: [
          { 
              name: 'userDeposit',
              path: '/revenueManagement/userDeposit',
              component: './UserDeposit/UserDeposit',
              power: ['MENU', 'CONTENT', 'EXPORT'],
          },
          {
            name: 'userConsumptions',
            path: '/revenueManagement/userConsumptions',
            component: './UserConsumptions/UserConsumptions',
            power: ['MENU', 'CONTENT', 'EXPORT'],
          },
          { 
            name: 'staffTuningLogs',
            path: '/revenueManagement/staffTuningLogs',
            component: './StaffTuningLogs/StaffTuningLogs',
            power: ['MENU', 'CONTENT', 'EXPORT'],
          },
          { 
            name: 'userAccount',
            path: '/revenueManagement/userAccount',
            component: './UserAccount/UserAccount',
            power: ['MENU', 'CONTENT', 'EXPORT', 'GIVE_COIN'],
          },
        ]
      },
    ],
  },
];

比较关键是准备这几个数据:(聪明的你肯定知道 _lodash)

/**
 * 过滤原始的 router 数据,返回有 power 属性的 item
 * @param {Array} data router.config.js 中关于 app 部分的配置,即:RouterConfig[1].routes,注意,不要直接把 RouterConfig[1].routes 传递进来,这里会改变原来的数据,所以需要深复制后才传进来
 * @returns {Array} 格式化后的 RouterConfig[1].routes,过滤掉没有 power 属性的 item
 */
function filterRouter(data) {
  return data.filter((item) => {
    if (item.routes) {
      item.routes = filterRouter(item.routes);
    }

    return item.power;
  })
}

/**
 * 将 filterRouter且memoizeOneFormatter 出来后的数据的 power 属性改成 [{label: "查看菜单", value: "MENU"}] 的形式,用于在展示是可以出现中文
 * @param {Array} data RouterConfig[1].routes执行 filterRouter且memoizeOneFormatter 函数后的数据,同样,该参数需要深复制后才传递进来
 * @returns {Array} 修改 power 属性后的数据
 */
function setPowerText(data) {
  return data.map((item) => {
    if (item.children) {
      item.children = setPowerText(item.children);
    }

    item.power = item.power.map((powerItem) => {
      return {
        label: powerName[powerItem],
        value: powerItem,
      }
    });

    return item;
  });
}

/**
 * path 为 key,power 为 value,将 filterRouter且memoizeOneFormatter 后的数据,转成这种 key-value 的对象
 * @param {Array} data RouterConfig[1].routes执行 filterRouter且memoizeOneFormatter 函数后的数据,同样,该参数需要深复制后才传递进来
 * @returns {Object} 
 * 例如:
    {
      '/list': ['MENU'],
      '/list/basic-list': ['MENU', 'CONTENT', 'ADD', 'UPDATE', 'DELETE'],
      '/exception': ['MENU'],
    }
 */
function getAllPowerKeyValue(data) {
  let result = {};

  const recursion = (data) => {
    data.forEach((item) => {
      result[item.path] = item.power;

      if (item.children) {
        recursion(item.children);
      }
    });
  }

  recursion(data);

  return result;
}

const powerOriginData = filterRouter(_.cloneDeep(RouterConfig[1].routes)); // 过滤没有 power 属性的项
const localePowerOriginData = memoizeOneFormatter(powerOriginData, undefined); // 将name 改成相应语言,注意,经过这个函数之后,原本的 routes 就改成 children 了
const powerTextData = setPowerText(_.cloneDeep(localePowerOriginData));
const allPowerKeyValueData = getAllPowerKeyValue(_.cloneDeep(localePowerOriginData));

powerOriginData 是过滤掉没有 power (power 是自己定义的一个属性,用来标明该模块中拥有哪些动作) 属性的项,减少接下来计算中的次数。

powerTextData 纯粹是为了展示用的,把动作的标识换成中文给用户选择时看

allPowerKeyValueData 主要是为了方便接下来的计算,把 router.config.js 中多余的字段都清掉,留下 key(以模块的 path 为 key)和对应的 power。

准备好这些展示数据,后面的交互逻辑和发送给后台就简单了,不啰嗦了~

使用

  1. 定义角色
  2. 授权角色拥有的权限
  3. 给用户指定角色

完成以上三步后,下一个模块就是直接使用了,这里分成两个部分:

  1. 登录时获取该用户的权限并初始化侧边栏
  2. 给各模块中的动作上锁

登录时拦截

  1. 登录后,结合当前用户信息,再向后台的接口请求数据,获取当前用户的所有权限
    • 比如,如果后台返回的数据如下(第 2 步下面)
    • 获得后台返回的数据后,将以上数据格式化成: {/authority: ["MENU"], /authority/role: ["MENU", "CONTENT", "ADD", "UPDATE", "TRIGGER", "RESOURCE_AUTHORIZE"]},标志每个路由(页面)里面匹配当前用户的角色分别有哪些权限,然后存在local storage中,字段命名为:curStaffAuthorized
  2. 进入主页面后,加载 src/layouts/BasicLayout.js 组件时会构造侧边栏,在 src/models/menu.jsgetMenuData 将以上缓存中 curStaffAuthorized 的数据转换成侧边栏的数据,过程如下:(具体可以查看:v2.0 权限控制
    • getMenuDatapayload 参数中有一个 routesconfig/router.config.js 中的所有路由
    • 结合缓存中 curStaffAuthorized 的数据就能知道当前用户哪些路由是有权限的,哪些路由没有权限,直接把没有权限的路由从要渲染到侧边栏的数据中删掉
后台返回的格式:("/authority"--这个 key 是路由,代表该路由或该页面有哪些权限)
{
  "/authority":[{permission_id: 1, action: "MENU", name: "角色权限管理-角色管理-MENU", description: ""}],
  "/authority/role":[
    {permission_id: 2, action: "MENU", name: "角色权限管理-角色管理-MENU", description: ""},
    {permission_id: 3, action: "CONTENT", name: "角色权限管理-角色管理-CONTENT", description: ""},
  ]
}

给各模块中的动作上锁

这一步就比较简单了(不过很麻烦,在想有没有更好的办法)

在 pages 中,根据 pathcurStaffAuthorized检查是否有该权限,然后根据标识控制对应功能的显示与否,比如:

let path = props.match.path;
this.contentPower = checkPower(CONTENT, path);
this.addPower = checkPower(ADD, path);
this.updatePower = checkPower(UPDATE, path);
this.deletePower = checkPower(DELETE, path);
this.triggerPower = checkPower(TRIGGER, path);

{this.addPower && <Button icon="plus" type="primary" onClick={this.handleAddClick}>新建</Button>}

吴勤发

芦苇科技web前端开发工程师、COO

擅长网站建设、公众号开发、微信小程序开发、小游戏、公众号开发,专注于前端框架、服务端渲染、SEO技术、交互设计、图像绘制、数据分析等研究,有兴趣的小伙伴来撩撩我们~ web@talkmoney.cn

访问 https://www.luweitech.cn/ 了解更多

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,968评论 6 482
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,601评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 153,220评论 0 344
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,416评论 1 279
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,425评论 5 374
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,144评论 1 285
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,432评论 3 401
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,088评论 0 261
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,586评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,028评论 2 325
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,137评论 1 334
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,783评论 4 324
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,343评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,333评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,559评论 1 262
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,595评论 2 355
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,901评论 2 345

推荐阅读更多精彩内容