Skip to content

🛡️ 权限使用

ZsAdmin 前端采用基于角色的权限控制(RBAC)模型,实现了细粒度的权限管理。本章节将详细介绍 ZsAdmin 前端的权限管理机制和使用方法。

1. 📋 权限管理概述

1.1 RBAC 模型

RBAC(Role-Based Access Control)是一种基于角色的权限控制模型,它将权限与角色关联,用户通过分配角色获得相应的权限。ZsAdmin 前端实现了完整的 RBAC 模型,包括:

  • 用户:系统中的操作者
  • 角色:权限的集合
  • 权限:对资源的操作许可
  • 资源:系统中的各种资源,如菜单、按钮、API 等

1.2 权限类型

ZsAdmin 前端支持以下几种权限类型:

权限类型说明示例
菜单权限控制用户是否可以看到某个菜单仪表盘菜单
按钮权限控制用户是否可以看到某个按钮新增、编辑、删除按钮
API 权限控制用户是否可以访问某个 API/api/user/delete
数据权限控制用户可以访问的数据范围只能查看自己创建的数据

2. ⚙️ 权限配置

2.1 权限定义

在后端系统中,管理员可以定义各种权限,包括菜单权限、按钮权限和 API 权限。权限定义通常包括:

  • 权限名称
  • 权限编码
  • 权限类型
  • 资源路径
  • 描述

2.2 角色配置

管理员可以创建角色,并为角色分配相应的权限。角色配置通常包括:

  • 角色名称
  • 角色编码
  • 角色描述
  • 分配的权限列表

2.3 用户角色分配

管理员可以为用户分配一个或多个角色,用户将获得所有分配角色的权限。

3. 🔧 权限实现

3.1 权限获取

当用户登录成功后,前端会从后端获取用户的权限信息,包括:

  • 用户拥有的角色列表
  • 用户拥有的权限列表

这些权限信息会被存储在状态管理中,供前端应用使用。

3.2 权限存储

权限信息存储在 Pinia 状态管理中,使用 useUserStore 进行管理:

typescript
import { defineStore } from 'pinia'

interface User {
  age?: number
  avatar?: string
  email?: string
  ip?: string
  ipAddress?: string
  isAdmin?: boolean
  lastLoginTime?: string
  phone?: string
  realName?: string
  sex?: number
  status?: number
  sysDeptId?: number
  sysPostId?: number
  sysUserId?: number
  username?: string
}

interface UserState {
  user: User
  permissions: string[]
}

export const useUserStore = defineStore('user', {
  state: (): UserState => ({
    user: {},
    permissions: [],
  }),
  
  getters: {
    // 检查是否拥有指定权限
    hasPermission: (state) => (permission: string) => {
      return state.permissions.includes(permission)
    },
    
    // 检查是否拥有任一权限
    hasAnyPermission: (state) => (permissions: string[]) => {
      return permissions.some(perm => state.permissions.includes(perm))
    },
    
    // 检查是否拥有所有权限
    hasAllPermissions: (state) => (permissions: string[]) => {
      return permissions.every(perm => state.permissions.includes(perm))
    },
  },
  
  actions: {
    // 从后端获取用户信息和权限
    async info() {
      const response = await getUserInfo()
      this.user = response.data.user
      this.permissions = response.data.permissions || []
    },
    
    // 清除用户信息
    logout() {
      this.user = {}
      this.permissions = []
      removeToken()
    }
  }
})

3.3 权限校验

3.3.1 路由权限校验

在路由配置中,可以通过 meta.requireAuth 属性指定该路由是否需要权限验证:

typescript
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes: [
    {
      path: '/dashboard',
      name: 'Dashboard',
      component: () => import('@/views/dashboard/index.vue'),
      meta: {
        title: '仪表盘',
        icon: 'dashboard',
        requireAuth: true, // 需要权限验证
      }
    }
  ]
})

然后在路由守卫中进行权限校验:

typescript
router.beforeEach(async (to, from, next) => {
  NProgress.start();

  const isLoginPage = to.path === '/login';
  // 已登录的情况
  if (isLogin()) {
    if (isLoginPage) {
      next('/dashboard');
      NProgress.done();
      return;
    }
    const userStore = useUserStore();
    const appStore = useAppStore();
    if (!appStore.hasFetchedMenus) {
      try {
        await userStore.info();
        await appStore.fetchServerMenuConfig();
        const newRoutes = transformRoutes(appStore.serverMenu);

        addRoutesToRouter(router, newRoutes);
        addSpecialRoutes(router); // 加载完动态路由后添加特殊路由

        next({ ...to, replace: true });
        NProgress.done();
      } catch (error) {
        /* eslint-disable no-console */
        console.error('路由守卫加载菜单失败:', error);
        await userStore.logout();
        next('/login'); // 跳转到登录页
        NProgress.done();
      }
    } else {
      // 如果已经加载过动态路由,则直接放行
      next();
      NProgress.done();
    }
  } else {
    if (to.meta.ignoreAuth) {
      next();
    } else {
      next({
        path: '/login',
        query: { redirect: to.path, ...to.query } as LocationQueryRaw,
      });
    }
    NProgress.done();
  }
});

3.3.2 组件权限校验

在组件中,可以使用 v-permission 指令来控制按钮等元素的显示与隐藏:

vue
<template>
  <div>
    <el-button type="primary" v-permission="'sys:user:add'">新增用户</el-button>
    <el-button type="success" v-permission="['sys:user:update', 'sys:user:edit']">编辑用户</el-button>
    <el-button type="danger" v-permission="'sys:user:delete'">删除用户</el-button>
  </div>
</template>

v-permission 指令的实现:

typescript
import { DirectiveBinding, App } from 'vue';
import { useUserStore } from '@/store';

/**
 * 权限指令实现
 * 支持单个权限字符串或权限数组
 */
const permission = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    checkPermission(el, binding);
  },
  updated(el: HTMLElement, binding: DirectiveBinding) {
    checkPermission(el, binding);
  },
};

/**
 * 检查权限
 */
function checkPermission(el: HTMLElement, binding: DirectiveBinding) {
  const { value } = binding;
  if (!value) return;
  
  const userStore = useUserStore();
  const { permissions } = userStore;
  
  let hasPermission = false;
  
  if (typeof value === 'string') {
    // 单个权限字符串
    hasPermission = Array.isArray(permissions) && permissions.includes(value);
  } else if (Array.isArray(value)) {
    // 权限数组,只要拥有任一权限即可
    hasPermission = Array.isArray(permissions) && value.some(perm => permissions.includes(perm));
  }
  
  if (!hasPermission && el.parentNode) {
    el.parentNode.removeChild(el);
  }
}

export function setupDirectives(app: App) {
  app.directive('permission', permission)
}

4. 📖 权限管理示例

4.1 配置菜单权限

  1. 后端定义菜单权限

    • 权限编码:sys:user:view
    • 权限名称:查看用户管理
    • 权限类型:菜单权限
    • 资源路径:/system/user
  2. 前端路由配置

    typescript
    {
      path: '/system/user',
      name: 'User',
      component: () => import('@/views/system/user/index.vue'),
      meta: {
        title: '用户管理',
        icon: 'user',
        requireAuth: true,
        permission: 'sys:user:view'
      }
    }
  3. 管理员分配权限

    • 为角色分配 sys:user:view 权限
    • 为用户分配角色

4.2 配置按钮权限

  1. 后端定义按钮权限

    • 权限编码:sys:user:add
    • 权限名称:新增用户
    • 权限类型:按钮权限
    • 资源路径:/api/user/add
  2. 前端使用指令

    vue
    <el-button type="primary" v-permission="'sys:user:add'">新增用户</el-button>
  3. 管理员为用户分配权限

    • 为角色分配 sys:user:add 权限
    • 为用户分配角色

5. 💡 最佳实践

5.1 权限设计原则

  • 最小权限原则:只授予用户完成工作所需的最小权限
  • 权限分层:按功能模块和操作类型进行权限分层设计
  • 权限命名规范:使用 模块:功能:操作 的格式命名权限,如 sys:user:add
  • 定期权限审计:定期检查和清理用户权限,移除不必要的权限

5.2 前端实现建议

  • 避免硬编码权限:权限编码应从后端获取,避免在前端代码中硬编码
  • 权限缓存策略:合理缓存权限信息,减少后端请求
  • 错误处理:权限校验失败时给出友好提示
  • 调试模式:开发环境提供权限模拟功能,方便测试

5.3 常见问题与解决方案

问题解决方案
权限不生效检查权限编码是否正确,确保用户已被分配相应权限
动态路由加载失败检查路由配置和后端返回的菜单数据格式是否匹配
按钮不显示检查 v-permission 指令的使用是否正确,确保权限编码存在
页面刷新后权限丢失确保权限信息在页面刷新后能从缓存或后端重新获取

6. 📊 权限流程图

mermaid
sequenceDiagram
    participant User as 用户
    participant Frontend as 前端
    participant Backend as 后端
    participant DB as 数据库
    
    User->>Frontend: 登录
    Frontend->>Backend: 发送登录请求
    Backend->>DB: 查询用户信息和权限
    DB-->>Backend: 返回用户信息和权限
    Backend-->>Frontend: 返回登录结果和权限列表
    Frontend->>Frontend: 存储权限到 Pinia
    Frontend->>User: 登录成功,跳转首页
    
    Note over Frontend,Backend: 权限校验流程
    User->>Frontend: 访问受保护路由
    Frontend->>Frontend: 路由守卫检查权限
    Frontend->>User: 允许访问或跳转到无权限页面
    
    User->>Frontend: 点击按钮
    Frontend->>Frontend: v-permission 指令检查权限
    Frontend->>User: 显示或隐藏按钮