简历项目经验描述
版本1 - 适合初中级
负责权限系统前端开发,实现细粒度权限控制
- 基于 RBAC 模型实现角色权限管理,支持菜单、按钮、数据三级权限控制
- 开发路由权限守卫,根据用户角色动态生成菜单和路由,提升系统安全性
- 实现按钮级权限指令,通过 v-permission 控制元素显隐,代码简洁易维护
版本2 - 适合高级
设计并实现企业级权限系统,支撑多租户业务场景
- 创新性采用权限树+位运算算法,权限校验从 O(n) 优化到 O(1),响应速度提升 80%
- 实现动态路由加载机制,根据权限实时生成路由表,减少 60% 的无效路由注册
- 开发数据权限过滤引擎,支持部门、区域、自定义等 8 种数据范围,满足复杂业务需求
- 建立权限配置可视化界面,支持拖拽配置权限树,配置效率提升 5 倍
版本3 - 适合架构方向
主导权限架构设计,建立统一的权限管理体系
- 抽象 RBAC 权限模型,支持用户-角色-权限-资源四层映射,扩展性强
- 设计权限缓存策略,采用 LRU + 过期时间机制,缓存命中率 95%+
- 建立权限变更实时通知机制,通过 WebSocket 推送,权限生效时间从分钟级降至秒级
- 制定权限审计规范,记录所有权限操作日志,支撑安全审计和问题排查
面试标准回答话术
Q1: RBAC 权限模型是什么?你们是怎么实现的?
标准回答
"RBAC 是 Role-Based Access Control 的缩写,基于角色的访问控制。核心思想是用户不直接和权限关联,而是通过角色关联。
基本模型
- 用户(User) - 系统的使用者
- 角色(Role) - 一组权限的集合
- 权限(Permission) - 对资源的操作权限
- 资源(Resource) - 系统中的菜单、页面、按钮、数据等
关系
用户 ←→ 角色 ←→ 权限 ←→ 资源
N N N 1
一个用户可以有多个角色,一个角色可以有多个权限,一个权限对应一个资源。
我们的实现方案
1. 数据结构设计
// 用户数据
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:
// 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. 获取权限
// 登录后获取用户权限
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: 路由权限控制是怎么实现的?
标准回答
"路由权限控制有两种方案:静态路由过滤和动态路由生成。我们用的是动态路由生成。
核心思路
- 定义所有可能的路由(包含权限标识)
- 用户登录后获取权限
- 根据权限过滤路由
- 动态添加到 Vue Router
完整实现
1. 定义路由(带权限标识)
// 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. 路由过滤和生成
// 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. 路由守卫
// 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. 生成侧边栏菜单
// 根据路由生成菜单
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. 自定义指令
// 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. 全局注册
// 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. 使用示例
<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. 权限工具函数(可复用)
// 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. 在组件中使用
<script setup>
import { usePermission } from '@/composables/usePermission'
const { hasPermission, hasRole } = usePermission()
// 根据权限显示不同内容
const showAdvancedFeatures = hasPermission('advanced:view')
// 根据角色执行不同逻辑
if (hasRole('admin')) {
// 管理员逻辑
} else {
// 普通用户逻辑
}
</script>
进阶:禁用而不是隐藏
有时候我们希望按钮禁用而不是隐藏,提醒用户没有权限:
// 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 = '暂无权限'
}
}
}
使用:
<a-button v-permission-disabled="'user:delete'">
删除
</a-button>
这套方案在我们项目里很好用,代码简洁,性能也好,一个页面几十个按钮也不会有问题。"
Q4: 数据权限过滤是怎么做的?
标准回答
"数据权限就是控制用户能看到哪些数据。比如普通员工只能看自己的数据,部门经理能看部门的,总经理能看全部。
数据权限的类型
- 全部数据 - 所有数据
- 本部门及下级部门 - 树形结构
- 本部门 - 平级
- 仅本人 - 个人
- 自定义 - 指定范围
实现方案
1. 后端返回数据权限范围
// 用户的数据权限
const dataPermission = {
type: 'department', // all, department, department_and_child, self, custom
departmentIds: [1, 2, 3], // 可访问的部门 ID
userIds: [10, 20] // 可访问的用户 ID(自定义时用)
}
2. 前端请求时带上过滤条件
// 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. 统一的数据权限拦截器
// 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. 前端二次过滤(可选)
有时候后端返回的数据需要前端再过滤一次:
// 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. 在组件中使用
<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>
复杂场景:树形数据权限
部门是树形结构,要过滤出有权限的部门树:
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. 权限树组件
<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. 角色权限配置页面
<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 连接
// 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. 监听权限变更
// 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. 在应用启动时初始化
// 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 推送,还要处理缓存失效:
// 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. 请求拦截器处理版本
// 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
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: 权限位运算
把权限编码为二进制位,用位运算判断:
// 权限编码(后端返回)
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: 权限结果缓存
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. 权限依赖定义
const permissionDependencies = {
'user:delete': ['user:edit'],
'user:edit': ['user:view'],
'article:publish': ['article:edit'],
'article:edit': ['article:view']
}
2. 权限展开(包含依赖)
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. 权限校验(自动包含依赖)
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. 权限配置时自动勾选依赖
<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>
这套方案让权限配置更合理,不会出现有"删除"权限但没有"查看"权限的情况。"
(由于篇幅限制,完整技术实现、面试追问、项目经验总结部分请查看输出文档)
完整技术实现
权限管理完整示例
<!-- 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: 如何处理超级管理员?
"超级管理员拥有所有权限,不需要单独配置。我的做法是:
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