Skip to content

权限控制

vue3-element-admin 提供了完善的权限控制系统,支持多种权限控制方式。

权限类型

1. 路由权限

通过动态路由实现页面级权限控制。用户登录后,根据其角色权限动态生成可访问的路由。

实现原理

typescript
// src/router/index.ts
// 根据用户权限过滤路由
const accessRoutes = await permissionStore.generateRoutes(roles)
accessRoutes.forEach((route) => {
  router.addRoute(route)
})

2. 按钮权限

通过指令或函数实现按钮级权限控制。

指令方式

vue
<template>
  <!-- 用户拥有指定权限才显示 -->
  <el-button v-hasPerm="['sys:user:add']">新增</el-button>

  <!-- 用户拥有指定角色才显示 -->
  <el-button v-hasRole="['ADMIN']">删除</el-button>
</template>

函数方式

vue
<script setup lang="ts">
import { hasPerm, hasRole } from '@/utils/permission'

// 在逻辑中判断权限
if (hasPerm(['sys:user:add'])) {
  // 执行操作
}
</script>

3. 角色权限

基于角色的访问控制(RBAC),用户通过角色获得权限。

配置示例

typescript
// 路由元信息中配置角色
{
  path: '/system/user',
  meta: {
    title: '用户管理',
    roles: ['ADMIN', 'USER'] // 允许访问的角色
  }
}

4. 数据权限

控制用户可以访问的数据范围。

实现方式

  • 全部数据权限:可以查看所有数据
  • 本部门数据权限:只能查看本部门数据
  • 本部门及下级部门数据权限:可以查看本部门和下级部门数据
  • 仅本人数据权限:只能查看自己的数据
  • 自定义数据权限:自定义可访问的部门

权限配置

路由配置

在路由的 meta 中配置权限:

typescript
// src/router/modules/system.ts
export default [
  {
    path: '/system',
    component: Layout,
    redirect: '/system/user',
    meta: {
      title: '系统管理',
      icon: 'setting',
      hidden: false,
      alwaysShow: true,
      roles: ['ADMIN'] // 只有 ADMIN 角色可访问
    },
    children: [
      {
        path: 'user',
        component: () => import('@/views/system/user/index.vue'),
        name: 'User',
        meta: {
          title: '用户管理',
          icon: 'user',
          keepAlive: true,
          perms: ['sys:user:list'] // 需要的权限标识
        }
      }
    ]
  }
]

meta 属性说明

属性类型说明
titlestring路由标题(菜单名称)
iconstring菜单图标
hiddenboolean是否隐藏菜单
alwaysShowboolean是否总是显示根菜单
rolesstring[]允许访问的角色
permsstring[]需要的权限标识
keepAliveboolean是否缓存页面
affixboolean是否固定在 tags-view

按钮权限配置

使用 v-hasPerm 指令:

vue
<template>
  <div>
    <!-- 单个权限 -->
    <el-button v-hasPerm="['sys:user:add']">新增</el-button>

    <!-- 多个权限(满足其一即可) -->
    <el-button v-hasPerm="['sys:user:edit', 'sys:user:update']">
      编辑
    </el-button>

    <!-- 需要同时满足多个权限 -->
    <el-button v-hasPerm="['sys:user:delete']">删除</el-button>
  </div>
</template>

使用 v-hasRole 指令:

vue
<template>
  <div>
    <!-- 单个角色 -->
    <el-button v-hasRole="['ADMIN']">管理员操作</el-button>

    <!-- 多个角色(满足其一即可) -->
    <el-button v-hasRole="['ADMIN', 'USER']">用户操作</el-button>
  </div>
</template>

权限指令实现

hasPerm 指令

typescript
// src/directives/permission/hasPerm.ts
import { useUserStore } from '@/store/modules/user'

export default {
  mounted(el: HTMLElement, binding: any) {
    const { value } = binding
    const perms = useUserStore().perms

    if (value && value instanceof Array && value.length > 0) {
      const requiredPerms = value
      const hasPermission = perms.some((perm: string) => {
        return requiredPerms.includes(perm)
      })

      if (!hasPermission) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error('需要传入权限标识数组')
    }
  }
}

hasRole 指令

typescript
// src/directives/permission/hasRole.ts
import { useUserStore } from '@/store/modules/user'

export default {
  mounted(el: HTMLElement, binding: any) {
    const { value } = binding
    const roles = useUserStore().roles

    if (value && value instanceof Array && value.length > 0) {
      const requiredRoles = value
      const hasRole = roles.some((role: string) => {
        return requiredRoles.includes(role)
      })

      if (!hasRole) {
        el.parentNode && el.parentNode.removeChild(el)
      }
    } else {
      throw new Error('需要传入角色标识数组')
    }
  }
}

权限工具函数

typescript
// src/utils/permission.ts
import { useUserStore } from '@/store/modules/user'

/**
 * 检查是否有权限
 * @param perms 权限标识数组
 * @returns 是否有权限
 */
export function hasPerm(perms: string[]): boolean {
  const userStore = useUserStore()
  const userPerms = userStore.perms

  return userPerms.some((perm: string) => {
    return perms.includes(perm)
  })
}

/**
 * 检查是否有角色
 * @param roles 角色标识数组
 * @returns 是否有角色
 */
export function hasRole(roles: string[]): boolean {
  const userStore = useUserStore()
  const userRoles = userStore.roles

  return userRoles.some((role: string) => {
    return roles.includes(role)
  })
}

/**
 * 检查是否有任一权限
 * @param perms 权限标识数组
 * @returns 是否有任一权限
 */
export function hasAnyPerm(perms: string[]): boolean {
  const userStore = useUserStore()
  const userPerms = userStore.perms

  return perms.some((perm: string) => {
    return userPerms.includes(perm)
  })
}

/**
 * 检查是否有所有权限
 * @param perms 权限标识数组
 * @returns 是否有所有权限
 */
export function hasAllPerms(perms: string[]): boolean {
  const userStore = useUserStore()
  const userPerms = userStore.perms

  return perms.every((perm: string) => {
    return userPerms.includes(perm)
  })
}

权限流程

登录流程

mermaid
graph LR
    A[用户登录] --> B[获取 Token]
    B --> C[存储 Token]
    C --> D[获取用户信息]
    D --> E[获取用户权限]
    E --> F[生成动态路由]
    F --> G[跳转首页]

路由守卫

typescript
// src/router/index.ts
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  const permissionStore = usePermissionStore()

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

          // 生成动态路由
          const accessRoutes = await permissionStore.generateRoutes(
            userStore.roles
          )

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

          // 确保添加路由完成
          next({ ...to, replace: true })
        } catch (error) {
          // 获取用户信息失败,重新登录
          await userStore.logout()
          next({ path: '/login' })
        }
      } else {
        next()
      }
    }
  } else {
    // 未登录
    if (whiteList.includes(to.path)) {
      // 白名单路径,直接访问
      next()
    } else {
      // 重定向到登录页
      next({ path: '/login', query: { redirect: to.path } })
    }
  }
})

动态路由生成

typescript
// src/store/modules/permission.ts
import { defineStore } from 'pinia'

export const usePermissionStore = defineStore('permission', () => {
  const routes = ref<RouteRecordRaw[]>([])

  /**
   * 生成动态路由
   * @param roles 用户角色
   */
  async function generateRoutes(roles: string[]) {
    // 获取后端返回的路由数据
    const res = await getRoutes()
    const asyncRoutes = res.data

    // 过滤有权限的路由
    const accessedRoutes = filterAsyncRoutes(asyncRoutes, roles)

    routes.value = constantRoutes.concat(accessedRoutes)

    return accessedRoutes
  }

  /**
   * 递归过滤路由
   */
  function filterAsyncRoutes(routes: any[], roles: string[]) {
    const res: RouteRecordRaw[] = []

    routes.forEach((route) => {
      const tmp = { ...route }

      if (hasPermission(roles, tmp)) {
        if (tmp.children) {
          tmp.children = filterAsyncRoutes(tmp.children, roles)
        }
        res.push(tmp)
      }
    })

    return res
  }

  /**
   * 判断是否有权限
   */
  function hasPermission(roles: string[], route: any) {
    if (route.meta && route.meta.roles) {
      return roles.some((role) => route.meta.roles.includes(role))
    }
    return true
  }

  return {
    routes,
    generateRoutes
  }
})

最佳实践

1. 权限标识命名规范

采用 模块:功能:操作 的格式:

sys:user:list    // 用户列表查询
sys:user:add     // 用户新增
sys:user:edit    // 用户编辑
sys:user:delete  // 用户删除
sys:user:export  // 用户导出

2. 角色命名规范

使用大写字母和下划线:

ADMIN           // 管理员
USER            // 普通用户
GUEST           // 访客

3. 前后端权限一致

  • 前端权限标识与后端保持一致
  • 前端校验 + 后端校验双重保障
  • 敏感操作务必在后端进行权限校验

4. 权限缓存

  • 权限信息缓存在 Pinia Store 中
  • 避免频繁请求后端获取权限
  • Token 过期后清除权限缓存

常见问题

1. 刷新页面后路由消失

原因:动态添加的路由没有持久化

解决:在路由守卫中重新生成路由

2. 权限更新不及时

原因:权限信息缓存未更新

解决:重新登录或清除缓存

3. 按钮权限不生效

原因

  1. 权限标识配置错误
  2. 用户权限未正确获取
  3. 指令使用不当

解决

  1. 检查权限标识是否正确
  2. 确认用户权限已正确获取
  3. 确认指令使用方式正确

相关链接

基于 MIT 许可发布