前端权限管理实战:RBAC、路由权限与按钮权限

6 阅读3分钟

引言

在企业级应用中,权限管理是不可或缺的核心功能。一个完善的权限系统需要同时控制页面访问路由跳转功能按钮三个层面。本文将介绍基于 RBAC(Role-Based Access Control)模型的完整权限管理方案,并提供可直接落地的代码实现。

RBAC 模型基础

RBAC 的核心思想是:用户 → 角色 → 权限。用户不直接拥有权限,而是通过角色间接获取。

// 权限类型定义
type Permission = {
  id: string;
  code: string;      // 权限标识,如 'user:create'
  name: string;      // 权限名称,如 '创建用户'
  type: 'menu' | 'button' | 'api';
};

// 角色定义
type Role = {
  id: string;
  code: string;      // 角色标识,如 'admin'
  name: string;      // 角色名称,如 '管理员'
  permissions: Permission[];
};

// 用户定义
type User = {
  id: string;
  username: string;
  roles: Role[];
};

路由权限控制

路由权限是最外层的防护,确保用户无法访问未授权的页面。

1. 路由元信息配置

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router';

const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { 
      requiresAuth: true,
      permission: 'dashboard:view'
    }
  },
  {
    path: '/user',
    name: 'UserManagement',
    component: () => import('@/views/user/Index.vue'),
    meta: { 
      requiresAuth: true,
      permission: 'user:view'
    },
    children: [
      {
        path: 'create',
        component: () => import('@/views/user/Create.vue'),
        meta: { permission: 'user:create' }
      }
    ]
  }
];

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

export default router;

2. 路由守卫实现

// router/permission.ts
import router from './index';
import { useAuthStore } from '@/stores/auth';

router.beforeEach(async (to, from, next) => {
  const authStore = useAuthStore();
  
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    next({ 
      path: '/login', 
      query: { redirect: to.fullPath } 
    });
    return;
  }
  
  if (to.meta.permission) {
    const hasPermission = authStore.hasPermission(to.meta.permission as string);
    if (!hasPermission) {
      next('/403');
      return;
    }
  }
  
  if (to.path === '/dashboard') {
    authStore.generateMenuFromRoutes();
  }
  
  next();
});

3. 权限 Store 实现

// stores/auth.ts
import { defineStore } from 'pinia';

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as User | null,
    permissions: new Set<string>(),
    menuTree: [] as MenuNode[]
  }),
  
  getters: {
    isAuthenticated: (state) => !!state.user,
    
    hasPermission: (state) => (code: string) => {
      return state.permissions.has(code);
    },
    
    hasAnyPermission: (state) => (codes: string[]) => {
      return codes.some(code => state.permissions.has(code));
    },
    
    hasAllPermissions: (state) => (codes: string[]) => {
      return codes.every(code => state.permissions.has(code));
    }
  },
  
  actions: {
    extractPermissions(user: User) {
      const codes = new Set<string>();
      user.roles.forEach(role => {
        role.permissions.forEach(perm => {
          codes.add(perm.code);
        });
      });
      this.permissions = codes;
    },
    
    generateMenuFromRoutes() {
      const routes = router.getRoutes();
      this.menuTree = routes
        .filter(route => route.meta?.permission)
        .filter(route => this.hasPermission(route.meta.permission as string))
        .map(route => ({
          path: route.path,
          name: route.name,
          icon: route.meta.icon
        }));
    }
  }
});

按钮权限控制

按钮权限通过自定义指令和组件实现细粒度控制。

1. 自定义指令 v-permission

// directives/permission.ts
import { useAuthStore } from '@/stores/auth';
import type { Directive, DirectiveBinding } from 'vue';

export const permission: Directive = {
  mounted(el: HTMLElement, binding: DirectiveBinding) {
    const authStore = useAuthStore();
    const { value } = binding;
    
    if (value) {
      const hasPermission = Array.isArray(value)
        ? authStore.hasAnyPermission(value)
        : authStore.hasPermission(value);
      
      if (!hasPermission) {
        el.parentNode?.removeChild(el);
      }
    } else {
      throw new Error('v-permission 需要传入权限标识');
    }
  }
};

2. 权限组件

<!-- components/Permission.vue -->
<template>
  <slot v-if="hasPermission" />
</template>

<script setup lang="ts">
import { computed } from 'vue';
import { useAuthStore } from '@/stores/auth';

const props = defineProps<{
  code: string | string[];
  type?: 'any' | 'all';
}>();

const authStore = useAuthStore();

const hasPermission = computed(() => {
  const codes = Array.isArray(props.code) ? props.code : [props.code];
  
  if (props.type === 'all') {
    return authStore.hasAllPermissions(codes);
  }
  return authStore.hasAnyPermission(codes);
});
</script>

3. 使用示例

<template>
  <div class="user-management">
    <el-button v-permission="'user:create'">
      新增用户
    </el-button>
    
    <el-button v-permission="['user:edit', 'user:delete']">
      批量操作
    </el-button>
    
    <Permission code="user:delete">
      <el-button type="danger">删除</el-button>
    </Permission>
  </div>
</template>

API 权限校验

前端权限只是第一道防线,后端必须校验所有敏感操作。

// utils/request.ts
import axios from 'axios';

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

request.interceptors.request.use(config => {
  const authStore = useAuthStore();
  if (authStore.token) {
    config.headers.Authorization = `Bearer ${authStore.token}`;
  }
  return config;
});

request.interceptors.response.use(
  response => response,
  error => {
    if (error.response?.status === 403) {
      ElMessage.error('权限不足,无法执行此操作');
    }
    return Promise.reject(error);
  }
);

完整示例:用户管理页面

<!-- views/user/Index.vue -->
<template>
  <div class="user-page">
    <el-card>
      <template #header>
        <div class="header">
          <span>用户管理</span>
          <el-button 
            v-permission="'user:create'" 
            type="primary"
            @click="handleCreate"
          >
            新增用户
          </el-button>
        </div>
      </template>
      
      <el-table :data="userList">
        <el-table-column prop="username" label="用户名" />
        <el-table-column prop="email" label="邮箱" />
        <el-table-column prop="role" label="角色" />
        <el-table-column label="操作" width="200">
          <template #default="{ row }">
            <Permission code="user:edit">
              <el-button size="small" @click="handleEdit(row)">
                编辑
              </el-button>
            </Permission>
            <Permission code="user:delete">
              <el-button size="small" type="danger" @click="handleDelete(row)">
                删除
              </el-button>
            </Permission>
          </template>
        </el-table-column>
      </el-table>
    </el-card>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { ElMessage } from 'element-plus';
import { userApi } from '@/api/user';

const userList = ref([]);

const loadUsers = async () => {
  try {
    userList.value = await userApi.getList();
  } catch (error) {
    ElMessage.error('加载用户列表失败');
  }
};

const handleDelete = async (row) => {
  try {
    await userApi.delete(row.id);
    ElMessage.success('删除成功');
    loadUsers();
  } catch (error) {
    ElMessage.error('删除失败');
  }
};

onMounted(() => {
  loadUsers();
});
</script>

总结

完善的权限管理系统需要三个层面的配合:

  1. 路由权限:控制页面访问,防止未授权访问
  2. 按钮权限:控制功能操作,实现细粒度管控
  3. API 权限:后端最终校验,确保数据安全

实施建议:

  • 权限标识统一命名规范(模块:操作)
  • 前端权限仅用于 UX 优化,不可依赖
  • 定期审计权限分配,遵循最小权限原则
  • 敏感操作记录审计日志

通过这套方案,可以构建安全、灵活、易维护的企业级权限系统。