RBAC权限系统实战踩坑记:从设计到落地的完整方案

极客成娟 安全 阅读 928
赞 23 收藏
二维码
手机扫码查看
反馈

这次终于搞定了权限控制的那些破事

最近做后台管理系统,权限控制这块儿又把我折腾得够呛。之前做过几个项目,每次都是临时抱佛脚,这次想着彻底把RBAC这套东西摸透,免得以后还要花时间重新搞。

RBAC权限系统实战踩坑记:从设计到落地的完整方案

最开始想当然地设计了简单权限

刚开始我觉得用户角色权限嘛,不就是 user -> role -> permission 这样的关系吗?结果设计了一个超级简单的版本,每个用户对应一个角色,然后角色直接绑定菜单权限。看起来挺好的,直到需求变更:

  • 某个用户需要多个角色
  • 临时给某些用户开某个功能的权限
  • 权限粒度要细化到按钮级别

这里我踩了个坑,原本的设计完全扛不住这些变化,改起来各种重构,前后花了快一周才整理清楚整个权限体系。

折腾了半天才明白RBAC到底该怎么玩

后来查了不少资料,看了很多开源项目的权限设计,终于明白了标准的RBAC模型长什么样。说白了就是:

用户可以有多个角色,角色可以有多个权限,权限可以分配给角色也可以直接分配给用户。这样既保证了通用性,又有足够的灵活性来处理特殊情况。

核心表结构大概是这样的:

  • users 表:存储用户信息
  • roles 表:存储角色信息
  • permissions 表:存储权限信息
  • user_roles 表:用户和角色的关联(多对多)
  • role_permissions 表:角色和权限的关联(多对多)
  • user_permissions 表:用户和权限的直接关联(用来处理临时权限)

前端权限控制的核心代码

后端权限验证相对简单,主要是前端这边要处理用户登录后权限的初始化和动态渲染。这是我在vuex里写的权限模块:

// store/modules/permission.js
const state = {
  permissions: [],
  routes: [],
  addRoutes: []
}

const mutations = {
  SET_PERMISSIONS: (state, permissions) => {
    state.permissions = permissions
  },
  SET_ROUTES: (state, routes) => {
    state.routes = routes
    state.addRoutes = routes.filter(route => route.path !== '*')
  }
}

const actions = {
  async getPermissions({ commit }) {
    try {
      const { data } = await fetch('/api/user/permissions').then(res => res.json())
      commit('SET_PERMISSIONS', data)
      return data
    } catch (error) {
      console.error('获取权限失败:', error)
      return []
    }
  },

  generateRoutes({ commit }, permissions) {
    const accessedRoutes = filterAsyncRoutes(asyncRoutes, permissions)
    commit('SET_ROUTES', accessedRoutes)
    return accessedRoutes
  }
}

function filterAsyncRoutes(routes, permissions) {
  const res = []
  routes.forEach(route => {
    const tmp = { ...route }
    if (hasPermission(permissions, tmp)) {
      if (tmp.children) {
        tmp.children = filterAsyncRoutes(tmp.children, permissions)
      }
      res.push(tmp)
    }
  })
  return res
}

function hasPermission(permissions, route) {
  if (route.meta && route.meta.permission) {
    return permissions.some(permission => permission === route.meta.permission)
  } else {
    return true
  }
}

export default {
  namespaced: true,
  state,
  mutations,
  actions
}

路由守卫里的权限检查也很重要:

// router/index.js
import store from '@/store'

router.beforeEach(async (to, from, next) => {
  const token = getToken()
  
  if (token) {
    if (!store.getters.hasUserInfo) {
      try {
        // 获取用户信息和权限
        const permissions = await store.dispatch('permission/getPermissions')
        
        // 根据权限生成路由
        const accessRoutes = await store.dispatch('permission/generateRoutes', permissions)
        
        // 动态添加路由
        accessRoutes.forEach(route => {
          router.addRoute(route)
        })
        
        next({ ...to, replace: true })
      } catch (error) {
        console.error('路由生成失败:', error)
        next('/login')
      }
    } else {
      // 已登录且已获取权限,正常跳转
      next()
    }
  } else {
    // 未登录,跳转登录页
    if (to.path !== '/login') {
      next('/login')
    } else {
      next()
    }
  }
})

按钮级别的权限判断组件

按钮级别的权限控制用指令比较方便,写了个v-permission指令:

// directives/permission.js
export default {
  inserted(el, binding, vnode) {
    checkPermission(el, binding, vnode)
  },
  update(el, binding, vnode) {
    checkPermission(el, binding, vnode)
  }
}

function checkPermission(el, binding, vnode) {
  const { value } = binding
  const permissions = vnode.context.$store.getters.permissions || []
  
  if (!value) {
    console.warn('need permissions! Like v-permission="'user:add'"')
    return
  }
  
  const hasPermission = permissions.includes(value)
  if (!hasPermission) {
    el.parentNode && el.parentNode.removeChild(el)
  }
}

使用的时候就这样:

<template>
  <el-button v-permission="'user:add'" @click="handleAdd">新增用户</el-button>
  <el-button v-permission="'user:edit'" @click="handleEdit">编辑</el-button>
  <el-button v-permission="'user:delete'" @click="handleDelete">删除</el-button>
</template>

这里注意我踩过好几次坑

权限数据的同步问题特别容易出错。比如用户登录成功后获取权限列表,然后动态生成路由,但有时候页面加载比权限获取还快,导致某些路由访问不了。

后来我在main.js里加了一层等待机制:

// main.js
async function startApp() {
  try {
    // 先获取用户权限
    await store.dispatch('permission/getPermissions')
    
    // 生成路由
    const permissions = store.getters.permissions
    const accessRoutes = await store.dispatch('permission/generateRoutes', permissions)
    
    // 添加路由后启动应用
    new Vue({
      router,
      store,
      render: h => h(App),
    }).$mount('#app')
  } catch (error) {
    console.error('应用启动失败:', error)
  }
}

startApp()

还有个问题就是权限更新的实时性。有时候管理员给用户开了新权限,用户这边还得刷新页面才能看到新的功能。这个我暂时没做websocket推送,因为感觉太重了,目前是用户每次打开系统都重新请求一次权限列表。

另外就是权限数据缓存的问题。为了减少接口调用次数,我把权限列表存在localStorage里,但问题是如果权限变了,用户可能不会立即感知到。我现在设置的是每天凌晨1点强制清空权限缓存,让第二天登录时重新获取。

后端配合的权限验证

前端权限控制只是体验层面的,真正的安全还是得靠后端验证。后端我也按标准RBAC实现:

<?php
// 示例PHP代码,实际项目用框架
class PermissionService {
    
    public function checkUserPermission($userId, $permissionCode) {
        // 检查用户是否直接拥有该权限
        $directPermission = $this->checkDirectPermission($userId, $permissionCode);
        if ($directPermission) {
            return true;
        }
        
        // 检查用户所属角色是否有该权限
        $roles = $this->getUserRoles($userId);
        foreach ($roles as $roleId) {
            if ($this->checkRolePermission($roleId, $permissionCode)) {
                return true;
            }
        }
        
        return false;
    }
    
    private function checkDirectPermission($userId, $permissionCode) {
        $sql = "SELECT id FROM user_permissions 
                WHERE user_id = ? AND permission_code = ? AND status = 1";
        return $this->db->query($sql, [$userId, $permissionCode])->rowCount() > 0;
    }
    
    private function checkRolePermission($roleId, $permissionCode) {
        $sql = "SELECT id FROM role_permissions 
                WHERE role_id = ? AND permission_code = ? AND status = 1";
        return $this->db->query($sql, [$roleId, $permissionCode])->rowCount() > 0;
    }
}
?>

前端接口调用时携带token,后端验证权限后返回数据,这样双重保障。

目前还遗留的小问题

虽然整体架构跑起来了,但还有几个小问题没完美解决:

  • 权限缓存策略还需要优化,现在的时间控制太粗暴
  • 当用户权限发生变化时,前端路由不会自动更新,需要刷新页面
  • 权限错误提示不够友好,有时候就是空白页面

不过这些问题都不影响主要功能,后续慢慢优化吧。这个RBAC权限系统算是基本能用了,至少能满足大部分业务场景的需求。

总结一下

这次把权限控制重新梳理了一遍,感觉比以前那种临时凑合的做法靠谱多了。标准的RBAC模型确实有它的道理,虽然一开始觉得复杂,但真正用起来才发现扩展性很好。

以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论