前端权限控制
作者:唐亚峰 | battcn
字数统计:1.5k 字
学习目标
掌握 Wemirr Platform 前端权限控制的实现方式
权限模型
前端权限控制分为三个层级:
┌─────────────────────────────────────────┐
│ 路由权限 │
│ 控制用户能访问哪些页面 │
├─────────────────────────────────────────┤
│ 按钮权限 │
│ 控制用户能操作哪些按钮 │
├─────────────────────────────────────────┤
│ 数据权限 │
│ 控制用户能看到哪些数据(后端实现) │
└─────────────────────────────────────────┘路由权限
权限获取流程
用户登录 → 获取用户信息 → 获取权限列表 → 动态生成路由 → 渲染菜单路由配置
路由权限通过后端返回的菜单数据动态生成:
typescript
// 后端返回的菜单数据结构
interface MenuItem {
id: number;
parentId: number;
name: string; // 路由名称
path: string; // 路由路径
component: string; // 组件路径
redirect?: string; // 重定向
meta: {
title: string; // 菜单标题
icon?: string; // 菜单图标
hidden?: boolean; // 是否隐藏
keepAlive?: boolean; // 是否缓存
permissions?: string[]; // 权限标识
};
children?: MenuItem[];
}路由守卫
typescript
// router/guard.ts
router.beforeEach(async (to, from, next) => {
const userStore = useUserStore();
const token = userStore.getToken;
// 白名单路由直接放行
if (whiteList.includes(to.path)) {
next();
return;
}
// 未登录跳转登录页
if (!token) {
next({ path: '/login', query: { redirect: to.fullPath } });
return;
}
// 已登录但未获取用户信息
if (!userStore.getUserInfo) {
try {
// 获取用户信息和权限
await userStore.getUserInfoAction();
// 动态添加路由
const routes = await permissionStore.buildRoutesAction();
routes.forEach((route) => router.addRoute(route));
// 重新进入当前路由
next({ ...to, replace: true });
} catch (error) {
await userStore.logout();
next({ path: '/login' });
}
return;
}
next();
});按钮权限
权限指令
使用 v-auth 指令控制按钮显示:
vue
<template>
<!-- 单个权限 -->
<a-button v-auth="'user:add'">新增用户</a-button>
<!-- 多个权限(满足任一即可) -->
<a-button v-auth="['user:edit', 'user:update']">编辑</a-button>
<!-- 必须满足所有权限 -->
<a-button v-auth.all="['user:edit', 'user:audit']">审核编辑</a-button>
</template>指令实现
typescript
// directives/auth.ts
import type { Directive, DirectiveBinding } from 'vue';
import { usePermission } from '@/hooks/web/usePermission';
const auth: Directive = {
mounted(el: Element, binding: DirectiveBinding) {
const { hasPermission } = usePermission();
const { value, modifiers } = binding;
if (!value) return;
const permissions = Array.isArray(value) ? value : [value];
const hasAuth = modifiers.all
? permissions.every((p) => hasPermission(p))
: permissions.some((p) => hasPermission(p));
if (!hasAuth) {
el.parentNode?.removeChild(el);
}
},
};
export default auth;函数式判断
在 JS/TS 中判断权限:
typescript
import { usePermission } from '@/hooks/web/usePermission';
const { hasPermission } = usePermission();
// 单个权限判断
if (hasPermission('user:add')) {
// 有权限
}
// 多个权限判断(任一满足)
if (hasPermission(['user:edit', 'user:update'])) {
// 有权限
}
// 多个权限判断(全部满足)
if (hasPermission(['user:edit', 'user:audit'], true)) {
// 有权限
}usePermission Hook
typescript
// hooks/web/usePermission.ts
import { useUserStore } from '@/store/modules/user';
export function usePermission() {
const userStore = useUserStore();
/**
* 判断是否有权限
* @param permission 权限标识,可以是字符串或数组
* @param all 是否需要全部满足,默认 false(满足任一即可)
*/
function hasPermission(
permission: string | string[],
all = false
): boolean {
const permissions = userStore.getPermissions;
if (!permission) return true;
if (!permissions || permissions.length === 0) return false;
// 超级管理员拥有所有权限
if (permissions.includes('*:*:*')) return true;
const permissionList = Array.isArray(permission) ? permission : [permission];
return all
? permissionList.every((p) => permissions.includes(p))
: permissionList.some((p) => permissions.includes(p));
}
/**
* 判断是否有角色
*/
function hasRole(role: string | string[], all = false): boolean {
const roles = userStore.getRoles;
if (!role) return true;
if (!roles || roles.length === 0) return false;
const roleList = Array.isArray(role) ? role : [role];
return all
? roleList.every((r) => roles.includes(r))
: roleList.some((r) => roles.includes(r));
}
return {
hasPermission,
hasRole,
};
}菜单配置
后台菜单配置
在「系统管理」-「菜单管理」中配置菜单:
| 字段 | 说明 | 示例 |
|---|---|---|
| 菜单名称 | 显示的标题 | 用户管理 |
| 路由地址 | 前端路由路径 | /system/user |
| 组件路径 | Vue 组件路径 | wemirr/system/user/index |
| 权限标识 | 权限编码 | system:user:list |
| 图标 | 菜单图标 | ant-design:user-outlined |
按钮权限配置
按钮作为菜单的子节点配置:
| 按钮名称 | 权限标识 |
|---|---|
| 新增 | system:user:add |
| 编辑 | system:user:edit |
| 删除 | system:user:delete |
| 导出 | system:user:export |
权限标识规范
推荐使用三级命名:模块:资源:操作
system:user:list # 系统模块-用户-列表
system:user:add # 系统模块-用户-新增
system:user:edit # 系统模块-用户-编辑
system:user:delete # 系统模块-用户-删除
system:user:export # 系统模块-用户-导出
system:role:list # 系统模块-角色-列表实战示例
完整页面示例
vue
<script setup lang="ts">
import { ref } from 'vue';
import { usePermission } from '@/hooks/web/usePermission';
import { getUserList, deleteUser } from './api';
const { hasPermission } = usePermission();
// 根据权限控制列表列显示
const columns = computed(() => {
const baseColumns = [
{ title: '用户名', dataIndex: 'username' },
{ title: '邮箱', dataIndex: 'email' },
];
// 有查看薪资权限才显示薪资列
if (hasPermission('user:salary:view')) {
baseColumns.push({ title: '薪资', dataIndex: 'salary' });
}
return baseColumns;
});
// 根据权限控制操作按钮
const actionColumn = computed(() => ({
title: '操作',
key: 'action',
width: 200,
customRender: ({ record }) => (
<a-space>
{hasPermission('user:view') && (
<a-button type="link" onClick={() => handleView(record)}>
查看
</a-button>
)}
{hasPermission('user:edit') && (
<a-button type="link" onClick={() => handleEdit(record)}>
编辑
</a-button>
)}
{hasPermission('user:delete') && (
<a-popconfirm title="确定删除?" onConfirm={() => handleDelete(record)}>
<a-button type="link" danger>删除</a-button>
</a-popconfirm>
)}
</a-space>
),
}));
</script>
<template>
<div class="user-list">
<!-- 工具栏 -->
<div class="toolbar">
<a-button v-auth="'user:add'" type="primary" @click="handleAdd">
新增用户
</a-button>
<a-button v-auth="'user:export'" @click="handleExport">
导出
</a-button>
<a-button v-auth="'user:import'" @click="handleImport">
导入
</a-button>
</div>
<!-- 数据表格 -->
<a-table :columns="[...columns, actionColumn]" :data-source="dataSource" />
</div>
</template>动态表单控制
vue
<script setup lang="ts">
import { usePermission } from '@/hooks/web/usePermission';
const { hasPermission } = usePermission();
// 根据权限动态生成表单项
const formSchema = computed(() => {
const schema = [
{ field: 'username', label: '用户名', component: 'Input' },
{ field: 'email', label: '邮箱', component: 'Input' },
];
// 有管理员权限才能分配角色
if (hasPermission('user:assignRole')) {
schema.push({
field: 'roleIds',
label: '角色',
component: 'Select',
componentProps: { mode: 'multiple' },
});
}
// 有设置薪资权限才显示薪资字段
if (hasPermission('user:salary:edit')) {
schema.push({
field: 'salary',
label: '薪资',
component: 'InputNumber',
});
}
return schema;
});
</script>最佳实践
1. 权限粒度
- 粗粒度:菜单级别,控制页面访问
- 细粒度:按钮级别,控制具体操作
- 数据粒度:行级别,由后端控制
2. 权限缓存
typescript
// 登录后缓存权限,避免重复请求
const userStore = useUserStore();
// 权限存储在 Pinia 中
userStore.setPermissions(permissions);
// 需要时从 store 获取
const permissions = userStore.getPermissions;3. 权限刷新
用户权限变更后需要刷新:
typescript
// 方式一:重新登录
await userStore.logout();
router.push('/login');
// 方式二:重新获取权限
await userStore.getUserInfoAction();
// 刷新页面以重新生成路由
window.location.reload();4. 超级管理员
typescript
// 超级管理员拥有所有权限
if (permissions.includes('*:*:*')) {
return true;
}