V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
V2EX 提问指南
csdoker
V2EX  ›  问与答

后台管理系统的权限管理,前端你们用的哪种方案?

  •  
  •   csdoker · 2021-07-05 10:05:52 +08:00 · 2230 次点击
    这是一个创建于 1270 天前的主题,其中的信息可能已经有所发展或是发生改变。

    (以下两种权限管理的方法均基于 RBAC 模型)

    一、权限列表法

    1. 后端在每个角色下挂载其具有权限的路由(菜单)和按钮数据,然后根据当前用户角色,返回给前端对应的角色数据(菜单、按钮权限等)
    2. 前端拿到菜单数据 menus 后,将其处理为树状结构,然后生成路由、侧边栏菜单(也可以直接由后端返回树状的 menus 数据)
    3. 页面元素可以通过 checkPermission 方法,判断当前元素是否有权限
    // 用户数据
    const users = [
      {
        userId: 1,
        username: 'admin',
        password: '123456',
        phone: '13600000000',
        email: '[email protected]',
        desc: '超级管理员',
        roles: [1]
      },
      {
        userId: 2,
        username: 'user',
        password: '123456',
        phone: '13600000001',
        email: '[email protected]',
        desc: '运维人员',
        roles: [2]
      }
    ]
    
    // 角色数据
    const roles = [
      {
        roleId: 1,
        title: '超级管理员',
        desc: '超级管理员',
        menus: [
          {
            menuId: 1,
            title: '首页',
            icon: 'icon-home',
            url: '/home',
            desc: '首页',
            parentId: null,
            children: null
          },
          {
            menuId: 2,
            title: '系统管理',
            icon: 'icon-setting',
            url: '/system',
            desc: '系统管理目录分支',
            parentId: null,
            children: [
              {
                menuId: 3,
                title: '用户管理',
                icon: 'icon-user',
                url: '/system/useradmin',
                desc: '系统管理 /用户管理',
                parent: 2,
                children: null
              },
              {
                menuId: 4,
                title: '角色管理',
                icon: 'icon-team',
                url: '/system/roleadmin',
                desc: '系统管理 /角色管理',
                parent: 2,
                children: null
              },
              {
                menuId: 5,
                title: '权限管理',
                icon: 'icon-safetycertificate',
                url: '/system/poweradmin',
                desc: '系统管理 /权限管理',
                parent: 2,
                children: null
              },
              {
                menuId: 6,
                title: '菜单管理',
                icon: 'icon-appstore',
                url: '/system/menuadmin',
                desc: '系统管理 /菜单管理',
                parent: 2,
                children: null
              }
            ]
          },
        ],
        powers: [
          {
            powerId: 1,
            menuId: 3,
            title: '新增',
            code: 'user:add',
            desc: '用户管理 - 添加权限'
          },
          {
            powerId: 2,
            menuId: 3,
            title: '修改',
            code: 'user:up',
            desc: '用户管理 - 修改权限'
          },
          {
            powerId: 3,
            menuId: 3,
            title: '查看',
            code: 'user:query',
            desc: '用户管理 - 查看权限'
          },
          {
            powerId: 4,
            menuId: 3,
            title: '删除',
            code: 'user:del',
            desc: '用户管理 - 删除权限'
          },
          {
            powerId: 5,
            menuId: 3,
            title: '分配角色',
            code: 'user:role',
            desc: '用户管理 - 分配角色权限'
          },
          {
            powerId: 6,
            menuId: 4,
            title: '新增',
            code: 'role:add',
            desc: '角色管理 - 添加权限'
          },
          {
            powerId: 7,
            menuId: 4,
            title: '修改',
            code: 'role:up',
            desc: '角色管理 - 修改权限'
          },
          {
            powerId: 8,
            menuId: 4,
            title: '查看',
            code: 'role:query',
            desc: '角色管理 - 查看权限'
          },
          {
            powerId: 18,
            menuId: 4,
            title: '分配权限',
            code: 'role:power',
            desc: '角色管理 - 分配权限'
          },
          {
            powerId: 9,
            menuId: 4,
            title: '删除',
            code: 'role:del',
            desc: '角色管理 - 删除权限'
          },
          {
            powerId: 10,
            menuId: 5,
            title: '新增',
            code: 'power:add',
            desc: '权限管理 - 添加权限'
          },
          {
            powerId: 11,
            menuId: 5,
            title: '修改',
            code: 'power:up',
            desc: '权限管理 - 修改权限'
          },
          {
            powerId: 12,
            menuId: 5,
            title: '查看',
            code: 'power:query',
            desc: '权限管理 - 查看权限'
          },
          {
            powerId: 13,
            menuId: 5,
            title: '删除',
            code: 'power:del',
            desc: '权限管理 - 删除权限'
          },
          {
            powerId: 14,
            menuId: 6,
            title: '新增',
            code: 'menu:add',
            desc: '菜单管理 - 添加权限'
          },
          {
            powerId: 15,
            menuId: 6,
            title: '修改',
            code: 'menu:up',
            desc: '菜单管理 - 修改权限'
          },
          {
            powerId: 16,
            menuId: 6,
            title: '查看',
            code: 'menu:query',
            desc: '菜单管理 - 查看权限'
          },
          {
            powerId: 17,
            menuId: 6,
            title: '删除',
            code: 'menu:del',
            desc: '菜单管理 - 删除权限'
          }
        ]
      },
      {
        roleId: 2,
        title: '运维人员',
        desc: '运维人员',
        menus: [
          {
            menuId: 1,
            title: '首页',
            icon: 'icon-home',
            url: '/home',
            parent: null,
            desc: '首页'
          }
        ],
        powers: []
      }
    ]
    
    // 判断当前元素是否有权限(按钮级)
    const checkPermission = powerCode => {
      return powers.map(item => item.code).includes(powerCode)
    }
    
    checkPermission('menu:add') && <Button>新增</Button>
    

    还有两个小问题我认为需要注意下:

    路由和权限数据获取时机

    这里可以进行一些接口的拆分,比如把路由和权限数据单独拆成一个接口来获取(/getMenusAndPowers,其实也就是当前角色对应的数据)

    在每次路由渲染之前(也就是路由拦截逻辑中),判断当前 store 中有没有角色数据,如果没有就重新请求 /getMenusAndPowers 接口,获取最新的角色数据并存入 store 中;如果已经有数据,就什么都不做(避免重复请求)

    这样做我认为有个好处,就是当用户修改了权限或者菜单数据后,刷新页面后,会去重新拉取最新的数据; 有的系统的做法是登陆以后请求一次接口,然后把权限和菜单数据存在 storage 中,但这样每次修改数据后,用户必须重新登录系统才能看到最新的效果,体验不好

    路由数据拼接

    路由可以分为不需要根据角色来判断权限的( constantRoutes,如 login 、404 等页面路由)路由和需要权限的( asyncRoutes,后端返回的就是这部分),前端最终的路由数据应该由这两部分组装

    一般的前后端分离项目在路由拦截里还需要添加判断用户是否登录的逻辑

    角色类型法

    此方法参考项目: https://github.com/panjiachen/vue-element-admin

    1. 前端把角色类型写死,并把角色 ID 绑定到路由(菜单)数据中,然后过滤当前角色有权限访问的路由,由前端来维护这份路由数据
    2. 这种方法最大的缺点是当系统的角色类型有变化时,前端需要手动修改这份数据,不够灵活(个人观点
    const menus = [
      {
        title: '',
        path: '/login',
        roles: ['admin', 'test'],
        component: '@/page/user/Login'
      },
      {
        title: '',
        path: '/404',
        roles: ['test'],
        component: '@/page/404'
      },
      {
        title: '',
        path: '/home',
        roles: ['admin'],
        component: '@/page/home'
      }
    ]
    

    看了很多后台项目,基本都是这两种方案的思路,我个人觉得第一种方案要更靠谱点?大家项目都是用的哪种呢?

    10 条回复    2024-09-04 12:53:43 +08:00
    basefas
        1
    basefas  
       2021-07-05 10:17:09 +08:00
    全部交给后端做,前端不要判断权限
    Aliennnnnn
        2
    Aliennnnnn  
       2021-07-05 10:30:15 +08:00
    第一种
    csdoker
        3
    csdoker  
    OP
       2021-07-05 10:30:19 +08:00
    @basefas 就是第一种方案吧?
    basefas
        4
    basefas  
       2021-07-05 10:34:49 +08:00
    @csdoker #3 对的,前端只做数据展示
    hellwys1
        5
    hellwys1  
       2021-07-05 15:16:09 +08:00
    第一种。
    第二种不靠谱,但是小项目前端也这样做过。
    csdoker
        6
    csdoker  
    OP
       2021-07-05 15:54:28 +08:00
    @hellwys1 好的👌
    rsyjjsn
        7
    rsyjjsn  
       2021-07-06 13:23:13 +08:00
    理论第一种更优
    实际第二种更方便(毕竟代码要自己写,antd 的中台目前也是第二种)
    csdoker
        8
    csdoker  
    OP
       2021-07-06 14:14:26 +08:00
    @luoyelusheng 第二种就是前端的工作量要大点,需要根据角色去手动改一些配置
    rsyjjsn
        9
    rsyjjsn  
       2021-07-07 09:44:54 +08:00
    @csdoker 这就得看你的项目初期架构怎么样了,如果公司有成熟的技术栈,那么像这种配置基本上就是拿来随便改改就行了,反而是第一种,你无法保证后端返回权限符合你当前前端页面排版,毕竟后端重架构,前台重交互(这也是我经常和后端开架的理由)
    lizy0329
        10
    lizy0329  
       113 天前
    为什么要“前端把角色类型写死”? 都数据 fetch 下来 match 一下不就完了吗
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   1078 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 25ms · UTC 22:55 · PVG 06:55 · LAX 14:55 · JFK 17:55
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.