返回笔记首页

权限系统设计 - 深度剖析

主题配置

简历项目经验描述

版本1 - 适合初中级

plain
负责权限系统前端开发,实现细粒度权限控制
- 基于 RBAC 模型实现角色权限管理,支持菜单、按钮、数据三级权限控制
- 开发路由权限守卫,根据用户角色动态生成菜单和路由,提升系统安全性
- 实现按钮级权限指令,通过 v-permission 控制元素显隐,代码简洁易维护

版本2 - 适合高级

plain
设计并实现企业级权限系统,支撑多租户业务场景
- 创新性采用权限树+位运算算法,权限校验从 O(n) 优化到 O(1),响应速度提升 80%
- 实现动态路由加载机制,根据权限实时生成路由表,减少 60% 的无效路由注册
- 开发数据权限过滤引擎,支持部门、区域、自定义等 8 种数据范围,满足复杂业务需求
- 建立权限配置可视化界面,支持拖拽配置权限树,配置效率提升 5 倍

版本3 - 适合架构方向

plain
主导权限架构设计,建立统一的权限管理体系
- 抽象 RBAC 权限模型,支持用户-角色-权限-资源四层映射,扩展性强
- 设计权限缓存策略,采用 LRU + 过期时间机制,缓存命中率 95%+
- 建立权限变更实时通知机制,通过 WebSocket 推送,权限生效时间从分钟级降至秒级
- 制定权限审计规范,记录所有权限操作日志,支撑安全审计和问题排查

面试标准回答话术

Q1: RBAC 权限模型是什么?你们是怎么实现的?

标准回答

"RBAC 是 Role-Based Access Control 的缩写,基于角色的访问控制。核心思想是用户不直接和权限关联,而是通过角色关联。

基本模型
  • 用户(User) - 系统的使用者
  • 角色(Role) - 一组权限的集合
  • 权限(Permission) - 对资源的操作权限
  • 资源(Resource) - 系统中的菜单、页面、按钮、数据等
关系
plain
用户 ←→ 角色 ←→ 权限 ←→ 资源
 N      N      N      1

一个用户可以有多个角色,一个角色可以有多个权限,一个权限对应一个资源。

我们的实现方案
1. 数据结构设计
javascript
// 用户数据
const user = {
  id: 1,
  username: 'zhangsan',
  roles: ['admin', 'editor'] // 用户的角色列表
}

// 角色数据
const roles = {
  admin: {
    id: 'admin',
    name: '管理员',
    permissions: ['user:add', 'user:edit', 'user:delete', 'user:view']
  },
  editor: {
    id: 'editor',
    name: '编辑',
    permissions: ['article:add', 'article:edit', 'article:view']
  }
}

// 权限数据
const permissions = {
  'user:add': {
    id: 'user:add',
    name: '添加用户',
    resource: '/user/add',
    type: 'button'
  },
  'user:edit': {
    id: 'user:edit',
    name: '编辑用户',
    resource: '/user/edit',
    type: 'button'
  }
}
2. 权限存储

用户登录后,从后端获取权限信息,存到 Vuex:

javascript
// store/modules/permission.js
import { defineStore } from 'pinia'

export const usePermissionStore = defineStore('permission', {
  state: () => ({
    roles: [],           // 用户的角色列表
    permissions: [],     // 用户的权限列表
    routes: [],          // 动态路由
  }),

  getters: {
    // 获取所有权限码
    permissionCodes: (state) => {
      return state.permissions.map(p => p.code)
    },

    // 获取所有角色码
    roleCodes: (state) => {
      return state.roles.map(r => r.code)
    }
  },

  actions: {
    // 设置权限信息
    setPermissions(data) {
      this.roles = data.roles || []
      this.permissions = data.permissions || []
    },

    // 检查是否有某个权限
    hasPermission(permission) {
      // 管理员拥有所有权限
      if (this.roleCodes.includes('admin')) {
        return true
      }

      // 检查权限列表
      return this.permissionCodes.includes(permission)
    },

    // 检查是否有某个角色
    hasRole(role) {
      return this.roleCodes.includes(role)
    },

    // 检查是否有任一权限
    hasAnyPermission(permissions) {
      return permissions.some(p => this.hasPermission(p))
    },

    // 检查是否有所有权限
    hasAllPermissions(permissions) {
      return permissions.every(p => this.hasPermission(p))
    }
  }
})
3. 获取权限
javascript
// 登录后获取用户权限
async function getUserPermissions() {
  const response = await api.getUserInfo()
  const { roles, permissions } = response.data

  const permissionStore = usePermissionStore()
  permissionStore.setPermissions({ roles, permissions })

  return { roles, permissions }
}

这套模型在我们项目里运行很稳定,支持了 20+ 个角色,200+ 个权限点,权限判断性能很好。"

Q2: 路由权限控制是怎么实现的?

标准回答

"路由权限控制有两种方案:静态路由过滤和动态路由生成。我们用的是动态路由生成。

核心思路
  1. 定义所有可能的路由(包含权限标识)
  2. 用户登录后获取权限
  3. 根据权限过滤路由
  4. 动态添加到 Vue Router
完整实现
1. 定义路由(带权限标识)
javascript
// router/routes.js
export const asyncRoutes = [
  {
    path: '/user',
    component: Layout,
    meta: {
      title: '用户管理',
      permission: 'user:view' // 需要的权限
    },
    children: [
      {
        path: 'list',
        component: () => import('@/views/user/list'),
        meta: {
          title: '用户列表',
          permission: 'user:view'
        }
      },
      {
        path: 'add',
        component: () => import('@/views/user/add'),
        meta: {
          title: '添加用户',
          permission: 'user:add'
        }
      }
    ]
  },
  {
    path: '/article',
    component: Layout,
    meta: {
      title: '文章管理',
      permission: 'article:view'
    },
    children: [
      {
        path: 'list',
        component: () => import('@/views/article/list'),
        meta: {
          title: '文章列表',
          permission: 'article:view'
        }
      }
    ]
  }
]

// 不需要权限的路由
export const constantRoutes = [
  {
    path: '/login',
    component: () => import('@/views/login')
  },
  {
    path: '/404',
    component: () => import('@/views/404')
  }
]
2. 路由过滤和生成
javascript
// utils/permission.js
export function filterAsyncRoutes(routes, permissions) {
  const result = []

  routes.forEach(route => {
    const temp = { ...route }

    // 检查权限
    if (hasPermission(temp, permissions)) {
      // 递归处理子路由
      if (temp.children) {
        temp.children = filterAsyncRoutes(temp.children, permissions)
      }

      result.push(temp)
    }
  })

  return result
}

function hasPermission(route, permissions) {
  // 没有权限标识,任何人都能访问
  if (!route.meta || !route.meta.permission) {
    return true
  }

  // 检查是否有权限
  const permission = route.meta.permission

  // 支持字符串或数组
  if (Array.isArray(permission)) {
    return permission.some(p => permissions.includes(p))
  }

  return permissions.includes(permission)
}
3. 路由守卫
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/store/user'
import { usePermissionStore } from '@/store/permission'
import { constantRoutes, asyncRoutes } from './routes'

const router = createRouter({
  history: createWebHistory(),
  routes: constantRoutes
})

// 白名单
const whiteList = ['/login', '/404']

router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  const permissionStore = usePermissionStore()

  // 有 token
  if (userStore.token) {
    if (to.path === '/login') {
      // 已登录,重定向到首页
      next({ path: '/' })
    } else {
      // 检查是否已获取用户信息
      if (permissionStore.permissions.length === 0) {
        try {
          // 获取用户权限
          await userStore.getUserInfo()

          // 生成动态路由
          const accessRoutes = filterAsyncRoutes(
            asyncRoutes,
            permissionStore.permissionCodes
          )

          // 添加路由
          accessRoutes.forEach(route => {
            router.addRoute(route)
          })

          // 添加 404 路由(必须在最后)
          router.addRoute({
            path: '/:pathMatch(.*)*',
            redirect: '/404'
          })

          // 保存路由
          permissionStore.routes = accessRoutes

          // 重新进入路由
          next({ ...to, replace: true })
        } catch (error) {
          // 获取权限失败,退出登录
          await userStore.logout()
          next(`/login?redirect=${to.path}`)
        }
      } else {
        next()
      }
    }
  } else {
    // 没有 token
    if (whiteList.includes(to.path)) {
      next()
    } else {
      next(`/login?redirect=${to.path}`)
    }
  }
})

export default router
4. 生成侧边栏菜单
javascript
// 根据路由生成菜单
function generateMenus(routes) {
  const menus = []

  routes.forEach(route => {
    // 跳过隐藏的路由
    if (route.meta && route.meta.hidden) {
      return
    }

    const menu = {
      path: route.path,
      title: route.meta?.title || '',
      icon: route.meta?.icon || ''
    }

    // 处理子菜单
    if (route.children && route.children.length > 0) {
      menu.children = generateMenus(route.children)
    }

    menus.push(menu)
  })

  return menus
}
优点
  • 用户只能访问有权限的路由
  • 减少路由注册数量,性能更好
  • 动态菜单,自动根据权限显示
注意事项
  • 404 路由必须最后添加
  • 刷新页面会重新生成路由
  • 要处理权限变更的情况

我们项目里有 50+ 个路由,权限变化时能实时刷新菜单,用户体验很好。"

Q3: 按钮级权限怎么控制?

标准回答

"按钮级权限就是控制页面上某些按钮、链接等元素的显隐。我们用自定义指令实现。

实现方案
1. 自定义指令
javascript
// directives/permission.js
import { usePermissionStore } from '@/store/permission'

export default {
  // Vue 3 的指令钩子
  mounted(el, binding) {
    const { value } = binding

    if (!value) {
      console.warn('v-permission 需要传入权限码')
      return
    }

    const permissionStore = usePermissionStore()

    // 支持字符串或数组
    let hasPermission = false

    if (Array.isArray(value)) {
      // 数组,满足任一即可
      hasPermission = permissionStore.hasAnyPermission(value)
    } else {
      // 字符串
      hasPermission = permissionStore.hasPermission(value)
    }

    // 没有权限,移除元素
    if (!hasPermission) {
      el.parentNode && el.parentNode.removeChild(el)
    }
  }
}

// 注册指令
export function setupPermissionDirective(app) {
  app.directive('permission', permission)
}
2. 全局注册
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { setupPermissionDirective } from '@/directives/permission'

const app = createApp(App)
setupPermissionDirective(app)
app.mount('#app')
3. 使用示例
vue
<template>
  <div class="user-page">
    <!-- 单个权限 -->
    <a-button
      v-permission="'user:add'"
      type="primary"
      @click="handleAdd"
    >
      添加用户
    </a-button>

    <!-- 多个权限(任一满足) -->
    <a-button
      v-permission="['user:edit', 'user:delete']"
      @click="handleBatchEdit"
    >
      批量操作
    </a-button>

    <!-- 结合 v-if 使用(不推荐,会留下注释节点) -->
    <a-button
      v-if="hasPermission('user:export')"
      @click="handleExport"
    >
      导出
    </a-button>
  </div>
</template>

<script setup>
import { usePermissionStore } from '@/store/permission'

const permissionStore = usePermissionStore()

// 或者用组合式函数
function hasPermission(permission) {
  return permissionStore.hasPermission(permission)
}
</script>
4. 权限工具函数(可复用)
javascript
// composables/usePermission.js
import { usePermissionStore } from '@/store/permission'

export function usePermission() {
  const permissionStore = usePermissionStore()

  // 检查单个权限
  function hasPermission(permission) {
    return permissionStore.hasPermission(permission)
  }

  // 检查多个权限(任一)
  function hasAnyPermission(permissions) {
    return permissionStore.hasAnyPermission(permissions)
  }

  // 检查多个权限(全部)
  function hasAllPermissions(permissions) {
    return permissionStore.hasAllPermissions(permissions)
  }

  // 检查角色
  function hasRole(role) {
    return permissionStore.hasRole(role)
  }

  return {
    hasPermission,
    hasAnyPermission,
    hasAllPermissions,
    hasRole
  }
}
5. 在组件中使用
vue
<script setup>
import { usePermission } from '@/composables/usePermission'

const { hasPermission, hasRole } = usePermission()

// 根据权限显示不同内容
const showAdvancedFeatures = hasPermission('advanced:view')

// 根据角色执行不同逻辑
if (hasRole('admin')) {
  // 管理员逻辑
} else {
  // 普通用户逻辑
}
</script>
进阶:禁用而不是隐藏

有时候我们希望按钮禁用而不是隐藏,提醒用户没有权限:

javascript
// directives/permission-disabled.js
export default {
  mounted(el, binding) {
    const { value } = binding
    const permissionStore = usePermissionStore()

    const hasPermission = Array.isArray(value)
      ? permissionStore.hasAnyPermission(value)
      : permissionStore.hasPermission(value)

    if (!hasPermission) {
      el.disabled = true
      el.classList.add('is-disabled')
      el.title = '暂无权限'
    }
  }
}

使用:

vue
<a-button v-permission-disabled="'user:delete'">
  删除
</a-button>

这套方案在我们项目里很好用,代码简洁,性能也好,一个页面几十个按钮也不会有问题。"

Q4: 数据权限过滤是怎么做的?

标准回答

"数据权限就是控制用户能看到哪些数据。比如普通员工只能看自己的数据,部门经理能看部门的,总经理能看全部。

数据权限的类型
  1. 全部数据 - 所有数据
  2. 本部门及下级部门 - 树形结构
  3. 本部门 - 平级
  4. 仅本人 - 个人
  5. 自定义 - 指定范围
实现方案
1. 后端返回数据权限范围
javascript
// 用户的数据权限
const dataPermission = {
  type: 'department', // all, department, department_and_child, self, custom
  departmentIds: [1, 2, 3], // 可访问的部门 ID
  userIds: [10, 20] // 可访问的用户 ID(自定义时用)
}
2. 前端请求时带上过滤条件
javascript
// api/user.js
export function getUserList(params) {
  const permissionStore = usePermissionStore()
  const dataPermission = permissionStore.dataPermission

  // 添加数据权限过滤
  const filters = {
    ...params,
    dataPermission: {
      type: dataPermission.type,
      departmentIds: dataPermission.departmentIds,
      userIds: dataPermission.userIds
    }
  }

  return request({
    url: '/user/list',
    method: 'get',
    params: filters
  })
}
3. 统一的数据权限拦截器
javascript
// utils/request.js
import axios from 'axios'
import { usePermissionStore } from '@/store/permission'

const service = axios.create({
  baseURL: '/api',
  timeout: 10000
})

// 请求拦截器
service.interceptors.request.use(
  config => {
    const permissionStore = usePermissionStore()

    // 自动添加数据权限过滤
    if (config.method === 'get' && config.params) {
      config.params._dataPermission = {
        type: permissionStore.dataPermission.type,
        scope: permissionStore.dataPermission.scope
      }
    }

    return config
  },
  error => {
    return Promise.reject(error)
  }
)

export default service
4. 前端二次过滤(可选)

有时候后端返回的数据需要前端再过滤一次:

javascript
// composables/useDataPermission.js
import { usePermissionStore } from '@/store/permission'
import { useUserStore } from '@/store/user'

export function useDataPermission() {
  const permissionStore = usePermissionStore()
  const userStore = useUserStore()

  // 过滤列表数据
  function filterData(list, config = {}) {
    const { type } = permissionStore.dataPermission

    if (type === 'all') {
      return list
    }

    if (type === 'self') {
      return list.filter(item => {
        return item.createBy === userStore.userId ||
               item.userId === userStore.userId
      })
    }

    if (type === 'department') {
      const deptIds = permissionStore.dataPermission.departmentIds
      return list.filter(item => {
        return deptIds.includes(item.departmentId)
      })
    }

    return list
  }

  // 检查是否能操作某条数据
  function canOperate(data) {
    const { type, userIds, departmentIds } = permissionStore.dataPermission

    if (type === 'all') return true

    if (type === 'self') {
      return data.createBy === userStore.userId
    }

    if (type === 'department') {
      return departmentIds.includes(data.departmentId)
    }

    if (type === 'custom') {
      return userIds.includes(data.userId)
    }

    return false
  }

  return {
    filterData,
    canOperate
  }
}
5. 在组件中使用
vue
<template>
  <div>
    <a-table :dataSource="filteredData" :columns="columns">
      <template #action="{ record }">
        <a-button
          v-if="canOperate(record)"
          @click="handleEdit(record)"
        >
          编辑
        </a-button>
      </template>
    </a-table>
  </div>
</template>

<script setup>
import { ref, computed } from 'vue'
import { useDataPermission } from '@/composables/useDataPermission'

const { filterData, canOperate } = useDataPermission()

const tableData = ref([])

// 过滤后的数据
const filteredData = computed(() => {
  return filterData(tableData.value)
})

// 获取数据
async function fetchData() {
  const response = await getUserList()
  tableData.value = response.data
}
</script>
复杂场景:树形数据权限

部门是树形结构,要过滤出有权限的部门树:

javascript
function filterTreeData(tree, allowedIds) {
  return tree
    .filter(node => {
      // 检查当前节点
      if (allowedIds.includes(node.id)) {
        return true
      }

      // 检查是否有子节点有权限
      if (node.children) {
        node.children = filterTreeData(node.children, allowedIds)
        return node.children.length > 0
      }

      return false
    })
    .map(node => ({ ...node }))
}

我们项目的数据权限支持 5 种范围,满足了各种业务需求,后端只需要根据前端传的参数过滤数据就行了。"

Q5: 权限配置如何可视化?

标准回答

"权限配置可视化就是用图形界面配置权限,不需要写代码。我们做了一个权限配置后台。

功能
  1. 角色管理 - 创建、编辑、删除角色
  2. 权限树配置 - 勾选角色的权限
  3. 用户分配角色 - 给用户分配角色
  4. 权限预览 - 预览某个角色能访问的菜单
核心实现
1. 权限树组件
vue
<template>
  <div class="permission-tree">
    <a-tree
      v-model:checkedKeys="checkedKeys"
      checkable
      :tree-data="treeData"
      :field-names="{ title: 'name', key: 'id', children: 'children' }"
      @check="handleCheck"
    >
      <template #title="{ title, description }">
        <div class="tree-node">
          <span class="node-title">{{ title }}</span>
          <span class="node-desc">{{ description }}</span>
        </div>
      </template>
    </a-tree>
  </div>
</template>

<script setup>
import { ref, watch } from 'vue'

const props = defineProps({
  permissions: {
    type: Array,
    default: () => []
  },
  value: {
    type: Array,
    default: () => []
  }
})

const emit = defineEmits(['update:value', 'change'])

// 选中的权限
const checkedKeys = ref([...props.value])

// 权限树数据
const treeData = ref([])

// 构建树形数据
function buildTree() {
  const permissions = props.permissions

  // 构建 map
  const map = new Map()
  permissions.forEach(item => {
    map.set(item.id, { ...item, children: [] })
  })

  // 构建树
  const tree = []
  permissions.forEach(item => {
    if (item.parentId === null || item.parentId === 0) {
      tree.push(map.get(item.id))
    } else {
      const parent = map.get(item.parentId)
      if (parent) {
        parent.children.push(map.get(item.id))
      }
    }
  })

  treeData.value = tree
}

// 监听权限数据变化
watch(() => props.permissions, buildTree, { immediate: true })

// 监听外部值变化
watch(() => props.value, (newVal) => {
  checkedKeys.value = [...newVal]
})

// 选中变化
function handleCheck(checked, info) {
  emit('update:value', checked)
  emit('change', checked, info)
}
</script>

<style scoped>
.tree-node {
  display: flex;
  align-items: center;
  gap: 12px;
}

.node-desc {
  font-size: 12px;
  color: #999;
}
</style>
2. 角色权限配置页面
vue
<template>
  <div class="role-permission-config">
    <a-card title="角色权限配置">
      <!-- 角色选择 -->
      <div class="role-selector">
        <a-select
          v-model:value="selectedRole"
          style="width: 200px"
          placeholder="选择角色"
          @change="handleRoleChange"
        >
          <a-select-option
            v-for="role in roles"
            :key="role.id"
            :value="role.id"
          >
            {{ role.name }}
          </a-select-option>
        </a-select>

        <a-button type="primary" @click="showAddRole = true">
          新增角色
        </a-button>
      </div>

      <!-- 权限树 -->
      <div v-if="selectedRole" class="permission-section">
        <h3>权限配置</h3>

        <a-tabs v-model:activeKey="activeTab">
          <!-- 菜单权限 -->
          <a-tab-pane key="menu" tab="菜单权限">
            <PermissionTree
              :permissions="menuPermissions"
              v-model:value="menuChecked"
              @change="handleMenuChange"
            />
          </a-tab-pane>

          <!-- 按钮权限 -->
          <a-tab-pane key="button" tab="按钮权限">
            <PermissionTree
              :permissions="buttonPermissions"
              v-model:value="buttonChecked"
              @change="handleButtonChange"
            />
          </a-tab-pane>

          <!-- 数据权限 -->
          <a-tab-pane key="data" tab="数据权限">
            <a-form layout="vertical">
              <a-form-item label="数据范围">
                <a-radio-group v-model:value="dataScope">
                  <a-radio value="all">全部数据</a-radio>
                  <a-radio value="department_and_child">
                    本部门及下级部门
                  </a-radio>
                  <a-radio value="department">本部门</a-radio>
                  <a-radio value="self">仅本人</a-radio>
                  <a-radio value="custom">自定义</a-radio>
                </a-radio-group>
              </a-form-item>

              <a-form-item
                v-if="dataScope === 'custom'"
                label="选择部门"
              >
                <a-tree-select
                  v-model:value="customDepartments"
                  :tree-data="departmentTree"
                  tree-checkable
                  placeholder="请选择部门"
                  style="width: 100%"
                />
              </a-form-item>
            </a-form>
          </a-tab-pane>
        </a-tabs>

        <!-- 操作按钮 -->
        <div class="actions">
          <a-space>
            <a-button type="primary" @click="handleSave">
              保存配置
            </a-button>
            <a-button @click="handleReset">
              重置
            </a-button>
            <a-button @click="handlePreview">
              预览权限
            </a-button>
          </a-space>
        </div>
      </div>
    </a-card>

    <!-- 预览抽屉 -->
    <a-drawer
      v-model:open="previewVisible"
      title="权限预览"
      width="600"
    >
      <div class="preview-content">
        <h4>可访问菜单</h4>
        <a-tree
          :tree-data="previewMenus"
          :field-names="{ title: 'name', key: 'id' }"
        />

        <h4>可用按钮</h4>
        <a-tag
          v-for="btn in previewButtons"
          :key="btn.id"
          color="blue"
        >
          {{ btn.name }}
        </a-tag>

        <h4>数据范围</h4>
        <p>{{ dataScopeText }}</p>
      </div>
    </a-drawer>
  </div>
</template>

<script setup>
import { ref, computed, onMounted } from 'vue'
import { message } from 'ant-design-vue'
import PermissionTree from './PermissionTree.vue'
import { getRoles, getPermissions, saveRolePermission } from '@/api/permission'

const roles = ref([])
const selectedRole = ref(null)
const activeTab = ref('menu')

const menuPermissions = ref([])
const buttonPermissions = ref([])
const menuChecked = ref([])
const buttonChecked = ref([])

const dataScope = ref('all')
const customDepartments = ref([])
const departmentTree = ref([])

const previewVisible = ref(false)
const previewMenus = ref([])
const previewButtons = ref([])

const dataScopeText = computed(() => {
  const map = {
    all: '全部数据',
    department_and_child: '本部门及下级部门',
    department: '本部门',
    self: '仅本人',
    custom: '自定义部门'
  }
  return map[dataScope.value] || '-'
})

onMounted(async () => {
  await loadRoles()
  await loadPermissions()
})

async function loadRoles() {
  const response = await getRoles()
  roles.value = response.data
}

async function loadPermissions() {
  const response = await getPermissions()
  const permissions = response.data

  menuPermissions.value = permissions.filter(p => p.type === 'menu')
  buttonPermissions.value = permissions.filter(p => p.type === 'button')
}

async function handleRoleChange(roleId) {
  // 加载角色的权限配置
  const role = roles.value.find(r => r.id === roleId)

  menuChecked.value = role.menuPermissions || []
  buttonChecked.value = role.buttonPermissions || []
  dataScope.value = role.dataScope || 'all'
  customDepartments.value = role.customDepartments || []
}

function handleMenuChange(checked) {
  console.log('Menu permissions changed:', checked)
}

function handleButtonChange(checked) {
  console.log('Button permissions changed:', checked)
}

async function handleSave() {
  try {
    await saveRolePermission({
      roleId: selectedRole.value,
      menuPermissions: menuChecked.value,
      buttonPermissions: buttonChecked.value,
      dataScope: dataScope.value,
      customDepartments: customDepartments.value
    })

    message.success('保存成功')
  } catch (error) {
    message.error('保存失败: ' + error.message)
  }
}

function handleReset() {
  handleRoleChange(selectedRole.value)
}

function handlePreview() {
  // 生成预览数据
  previewMenus.value = menuPermissions.value
    .filter(p => menuChecked.value.includes(p.id))

  previewButtons.value = buttonPermissions.value
    .filter(p => buttonChecked.value.includes(p.id))

  previewVisible.value = true
}
</script>

<style scoped>
.role-permission-config {
  padding: 24px;
}

.role-selector {
  display: flex;
  gap: 16px;
  margin-bottom: 24px;
}

.permission-section {
  margin-top: 24px;
}

.actions {
  margin-top: 24px;
  padding-top: 24px;
  border-top: 1px solid #f0f0f0;
}

.preview-content h4 {
  margin-top: 24px;
  margin-bottom: 12px;
}
</style>

这个配置界面在我们系统里用得很多,管理员可以很方便地配置权限,不需要改代码。"


核心难点与解决方案

难点1: 权限变更的实时生效

问题描述: 用户权限被修改后,如果用户还在使用系统,权限不会立即生效,需要刷新页面,体验不好。

解决方案

"我设计了权限实时同步机制,通过 WebSocket 推送权限变更通知。

实现方案
1. WebSocket 连接
javascript
// utils/websocket.js
class WebSocketClient {
  constructor(url) {
    this.url = url
    this.ws = null
    this.reconnectTimer = null
    this.heartbeatTimer = null
    this.listeners = new Map()
  }

  connect() {
    this.ws = new WebSocket(this.url)

    this.ws.onopen = () => {
      console.log('WebSocket connected')
      this.startHeartbeat()
    }

    this.ws.onmessage = (event) => {
      const message = JSON.parse(event.data)
      this.handleMessage(message)
    }

    this.ws.onerror = (error) => {
      console.error('WebSocket error:', error)
    }

    this.ws.onclose = () => {
      console.log('WebSocket closed')
      this.stopHeartbeat()
      this.reconnect()
    }
  }

  handleMessage(message) {
    const { type, data } = message

    // 触发监听器
    const listeners = this.listeners.get(type) || []
    listeners.forEach(callback => callback(data))
  }

  // 监听消息
  on(type, callback) {
    if (!this.listeners.has(type)) {
      this.listeners.set(type, [])
    }
    this.listeners.get(type).push(callback)
  }

  // 发送消息
  send(type, data) {
    if (this.ws && this.ws.readyState === WebSocket.OPEN) {
      this.ws.send(JSON.stringify({ type, data }))
    }
  }

  // 心跳
  startHeartbeat() {
    this.heartbeatTimer = setInterval(() => {
      this.send('ping', {})
    }, 30000) // 30秒
  }

  stopHeartbeat() {
    if (this.heartbeatTimer) {
      clearInterval(this.heartbeatTimer)
      this.heartbeatTimer = null
    }
  }

  // 重连
  reconnect() {
    if (this.reconnectTimer) return

    this.reconnectTimer = setTimeout(() => {
      console.log('Reconnecting...')
      this.connect()
      this.reconnectTimer = null
    }, 5000)
  }

  close() {
    this.stopHeartbeat()
    if (this.reconnectTimer) {
      clearTimeout(this.reconnectTimer)
    }
    if (this.ws) {
      this.ws.close()
    }
  }
}

export const wsClient = new WebSocketClient('ws://localhost:3000/ws')
2. 监听权限变更
javascript
// store/permission.js
import { defineStore } from 'pinia'
import { wsClient } from '@/utils/websocket'
import router from '@/router'

export const usePermissionStore = defineStore('permission', {
  state: () => ({
    permissions: [],
    roles: [],
    routes: []
  }),

  actions: {
    // 初始化 WebSocket 监听
    initWebSocket() {
      // 监听权限变更
      wsClient.on('permission:changed', (data) => {
        this.handlePermissionChanged(data)
      })

      // 监听角色变更
      wsClient.on('role:changed', (data) => {
        this.handleRoleChanged(data)
      })
    },

    // 处理权限变更
    async handlePermissionChanged(data) {
      console.log('Permission changed:', data)

      // 重新获取权限
      await this.fetchPermissions()

      // 重新生成路由
      await this.generateRoutes()

      // 提示用户
      this.$message.info('您的权限已更新')

      // 如果当前页面没权限了,跳转到首页
      const currentRoute = router.currentRoute.value
      if (!this.hasPermission(currentRoute.meta?.permission)) {
        router.push('/')
      }
    },

    // 重新生成路由
    async generateRoutes() {
      // 清除旧路由
      this.routes.forEach(route => {
        router.removeRoute(route.name)
      })

      // 生成新路由
      const accessRoutes = filterAsyncRoutes(
        asyncRoutes,
        this.permissionCodes
      )

      // 添加路由
      accessRoutes.forEach(route => {
        router.addRoute(route)
      })

      this.routes = accessRoutes
    }
  }
})
3. 在应用启动时初始化
javascript
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import { createPinia } from 'pinia'
import { wsClient } from '@/utils/websocket'
import { usePermissionStore } from '@/store/permission'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)

// 连接 WebSocket
wsClient.connect()

// 初始化权限监听
const permissionStore = usePermissionStore()
permissionStore.initWebSocket()

app.mount('#app')
4. 权限缓存失效

除了 WebSocket 推送,还要处理缓存失效:

javascript
// utils/permission-cache.js
class PermissionCache {
  constructor() {
    this.cache = new Map()
    this.version = 0
  }

  // 设置权限(带版本号)
  set(userId, permissions, version) {
    this.cache.set(userId, {
      permissions,
      version,
      timestamp: Date.now()
    })
    this.version = version
  }

  // 获取权限
  get(userId) {
    const cached = this.cache.get(userId)

    if (!cached) return null

    // 检查是否过期(10分钟)
    const isExpired = Date.now() - cached.timestamp > 10 * 60 * 1000

    if (isExpired) {
      this.cache.delete(userId)
      return null
    }

    return cached.permissions
  }

  // 清除缓存
  clear(userId) {
    if (userId) {
      this.cache.delete(userId)
    } else {
      this.cache.clear()
    }
  }

  // 检查版本
  checkVersion(version) {
    return version === this.version
  }
}

export const permissionCache = new PermissionCache()
5. 请求拦截器处理版本
javascript
// utils/request.js
service.interceptors.request.use(
  config => {
    // 添加权限版本号
    config.headers['X-Permission-Version'] = permissionCache.version
    return config
  }
)

service.interceptors.response.use(
  response => {
    // 检查权限版本
    const serverVersion = response.headers['x-permission-version']

    if (serverVersion && !permissionCache.checkVersion(Number(serverVersion))) {
      // 版本不一致,重新加载权限
      const permissionStore = usePermissionStore()
      permissionStore.fetchPermissions()
    }

    return response
  }
)
效果
  • 权限变更后 < 3 秒生效
  • 用户无感知,不需要刷新页面
  • 自动跳转到有权限的页面"

难点2: 权限性能优化

问题描述: 权限点很多(200+),每次判断权限都要遍历数组,性能差。

解决方案

"我用了三种优化手段:

优化1: 权限码转 Set
javascript
export const usePermissionStore = defineStore('permission', {
  state: () => ({
    permissions: [],
    permissionSet: new Set() // 权限码 Set
  }),

  actions: {
    setPermissions(permissions) {
      this.permissions = permissions

      // 转换为 Set
      this.permissionSet = new Set(
        permissions.map(p => p.code)
      )
    },

    hasPermission(permission) {
      // O(1) 时间复杂度
      return this.permissionSet.has(permission)
    }
  }
})
优化2: 权限位运算

把权限编码为二进制位,用位运算判断:

javascript
// 权限编码(后端返回)
const permissions = {
  'user:view': 1,    // 0001
  'user:add': 2,     // 0010
  'user:edit': 4,    // 0100
  'user:delete': 8,  // 1000
}

// 用户的权限值(多个权限按位或)
const userPermission = 7 // 0111 = view | add | edit

// 判断权限
function hasPermission(permission) {
  return (userPermission & permissions[permission]) === permissions[permission]
}

hasPermission('user:view')   // true
hasPermission('user:delete') // false
优化3: 权限结果缓存
javascript
class PermissionChecker {
  constructor() {
    this.cache = new Map()
  }

  hasPermission(permission, userPermissions) {
    const cacheKey = `${permission}_${userPermissions.join(',')}`

    if (this.cache.has(cacheKey)) {
      return this.cache.get(cacheKey)
    }

    const result = userPermissions.includes(permission)
    this.cache.set(cacheKey, result)

    return result
  }

  clearCache() {
    this.cache.clear()
  }
}
性能对比
  • 数组遍历:200 个权限,判断耗时 ~0.1ms
  • Set 查找:200 个权限,判断耗时 ~0.001ms
  • 位运算:200 个权限,判断耗时 ~0.0001ms

我们选择了 Set 方案,性能提升 100 倍,而且代码简单。"

难点3: 复杂的权限继承和组合

问题描述: 有些权限有依赖关系,比如"编辑"依赖"查看","删除"依赖"编辑",如何处理权限继承?

解决方案

"我设计了权限依赖图:

1. 权限依赖定义
javascript
const permissionDependencies = {
  'user:delete': ['user:edit'],
  'user:edit': ['user:view'],
  'article:publish': ['article:edit'],
  'article:edit': ['article:view']
}
2. 权限展开(包含依赖)
javascript
function expandPermissions(permissions) {
  const expanded = new Set(permissions)

  // 递归添加依赖
  function addDependencies(permission) {
    const deps = permissionDependencies[permission] || []

    deps.forEach(dep => {
      if (!expanded.has(dep)) {
        expanded.add(dep)
        addDependencies(dep)
      }
    })
  }

  permissions.forEach(p => addDependencies(p))

  return Array.from(expanded)
}

// 使用
const userPermissions = ['user:delete']
const expandedPermissions = expandPermissions(userPermissions)
// ['user:delete', 'user:edit', 'user:view']
3. 权限校验(自动包含依赖)
javascript
export const usePermissionStore = defineStore('permission', {
  state: () => ({
    permissions: [],
    expandedPermissions: []
  }),

  actions: {
    setPermissions(permissions) {
      this.permissions = permissions
      this.expandedPermissions = expandPermissions(
        permissions.map(p => p.code)
      )
    },

    hasPermission(permission) {
      return this.expandedPermissions.includes(permission)
    }
  }
})
4. 权限配置时自动勾选依赖
vue
<script setup>
function handlePermissionCheck(checked, node) {
  // 勾选权限时,自动勾选依赖
  if (checked) {
    const deps = permissionDependencies[node.id] || []
    deps.forEach(dep => {
      if (!checkedKeys.value.includes(dep)) {
        checkedKeys.value.push(dep)
      }
    })
  }

  // 取消勾选时,自动取消被依赖的权限
  if (!checked) {
    const dependents = Object.entries(permissionDependencies)
      .filter(([_, deps]) => deps.includes(node.id))
      .map(([key]) => key)

    dependents.forEach(dependent => {
      const index = checkedKeys.value.indexOf(dependent)
      if (index > -1) {
        checkedKeys.value.splice(index, 1)
      }
    })
  }
}
</script>

这套方案让权限配置更合理,不会出现有"删除"权限但没有"查看"权限的情况。"


(由于篇幅限制,完整技术实现、面试追问、项目经验总结部分请查看输出文档)

完整技术实现

权限管理完整示例

vue
<!-- views/Permission/RoleManagement.vue -->
<template>
  <div class="role-management">
    <a-card title="角色管理">
      <!-- 角色列表 -->
      <a-table
        :dataSource="roles"
        :columns="columns"
        row-key="id"
      >
        <template #action="{ record }">
          <a-space>
            <a-button
              type="link"
              @click="handleEdit(record)"
            >
              编辑
            </a-button>
            <a-button
              type="link"
              @click="handleConfigPermission(record)"
            >
              配置权限
            </a-button>
            <a-popconfirm
              title="确定删除?"
              @confirm="handleDelete(record)"
            >
              <a-button type="link" danger>
                删除
              </a-button>
            </a-popconfirm>
          </a-space>
        </template>
      </a-table>
    </a-card>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { message } from 'ant-design-vue'

const roles = ref([])

const columns = [
  { title: '角色名称', dataIndex: 'name', key: 'name' },
  { title: '角色编码', dataIndex: 'code', key: 'code' },
  { title: '描述', dataIndex: 'description', key: 'description' },
  { title: '创建时间', dataIndex: 'createTime', key: 'createTime' },
  { title: '操作', key: 'action', slots: { customRender: 'action' } }
]

onMounted(() => {
  fetchRoles()
})

async function fetchRoles() {
  // 获取角色列表
}

function handleEdit(record) {
  // 编辑角色
}

function handleConfigPermission(record) {
  // 配置权限
}

async function handleDelete(record) {
  // 删除角色
}
</script>

面试常见追问

Q: 如何处理超级管理员?

"超级管理员拥有所有权限,不需要单独配置。我的做法是:

javascript
hasPermission(permission) {
  // 超级管理员
  if (this.roles.includes('superadmin')) {
    return true
  }

  return this.permissions.includes(permission)
}
```"

### Q: 权限如何做审计日志?

"每次权限操作都记录日志:

```javascript
async function logPermissionOperation(operation) {
  await api.createLog({
    module: 'permission',
    action: operation.action,
    target: operation.target,
    operator: userStore.userId,
    timestamp: Date.now()
  })
}
```"

---

## 项目经验总结

### 踩过的坑

1. **动态路由添加时机** - 太早会找不到路由,太晚会白屏
2. **权限判断死循环** - 权限依赖形成环,导致死循环
3. **按钮权限闪烁** - 初始化慢,按钮先显示后隐藏
4. **WebSocket 断线重连** - 网络不稳定,要处理重连

### 性能数据

- 权限判断:< 0.01ms (Set 查找)
- 路由生成:< 100ms (50个路由)
- 权限变更生效:< 3s (WebSocket 推送)
- 权限缓存命中率:95%+

### 可以吹的点

- RBAC 模型,扩展性强
- 三级权限控制(菜单/按钮/数据)
- 动态路由,按需加载
- 权限实时生效,无需刷新
- 可视化配置,简单易用
```text