后台管理系统权限管理:前端实现详解

需求背景:
( 公司希望在现运行的后台系统上, 满足不同角色的划分,保护隐私数据 ,实现权限控制, 于是我基于诺依系统的权限管理功能, 借鉴了思路)

  • 数据隔离:按角色划分数据与功能可见范围,如普通员工无权查看管理层报表。
  • 操作限制:基于角色职责管控按钮级操作,如禁止实习生执行删除、审批等高危操作。
  • 体验优化:动态隐藏无权限的菜单、按钮,减少无效操作,降低学习成本。
  • 路由安全:拦截通过手动修改 URL 访问无权限页面的行为,避免前端报错和后端无效请求。

项目实现部分截图

一、权限数据的初始化流程

前端权限系统的运转始于权限数据的加载,这一过程集中在用户登录后的初始化阶段。诺依前端采用 Vue 生态,通过 Vuex 管理全局状态,权限数据的流转路径清晰可控。

  1. 登录成功后的权限加载触发​

当用户登录请求得到后端响应(包含 token)后,前端会立即调用权限初始化方法:

javascript 复制代码
// src/store/modules/user.js
actions: {
  // 登录动作
  login({ commit }, userInfo) {
    return new Promise((resolve, reject) => {
      login(userInfo).then(response => {
        const { token } = response.data;
        // 存储token
        setToken(token);
        commit('SET_TOKEN', token);
        resolve();
      }).catch(error => {
        reject(error);
      });
    });
  },
  
  // 加载用户权限信息
  getInfo({ commit, state }) {
    return new Promise((resolve, reject) => {
      getInfo().then(response => {
        const { user, roles, permissions } = response.data;
        // 存储用户信息、角色、权限标识
        commit('SET_USER', user);
        commit('SET_ROLES', roles);
        commit('SET_PERMISSIONS', permissions);
        resolve(response.data);
      }).catch(error => {
        reject(error);
      });
    });
  }
}

这里的getInfo接口会返回三组关键数据:用户基本信息、角色列表(如["admin", "user"])、权限标识集合(如["system:user:add", "system:user:edit"])。

  1. 动态路由的生成与注入

权限标识加载完成后,下一步是生成可访问的路由配置。诺依将路由分为 "常量路由"(如登录页、404 页)和 "动态路由"(需权限控制的业务页面):

javascript 复制代码
// src/store/modules/permission.js
actions: {
  generateRoutes({ commit, state }) {
    return new Promise(resolve => {
      // 调用后端接口获取菜单树
      getRouters().then(response => {
        const asyncRoutes = response.data;
        // 将后端返回的菜单数据转换为前端路由配置
        const accessedRoutes = filterAsyncRoutes(asyncRoutes);
        commit('SET_ROUTES', accessedRoutes);
        resolve(accessedRoutes);
      });
    });
  }
}

// 菜单转路由的核心函数
function filterAsyncRoutes(routes) {
  const res = [];
  routes.forEach(route => {
    const tmp = { ...route };
    // 组件路径转换(诺依约定组件路径格式为"module/component")
    if (tmp.component) {
      // 目录
      if (tmp.component === 'Layout') {
        tmp.component = () => import("@/views/index");; 
      // 父级菜单
      } else if (tmp.component === "ParentView") {
        tmp.component = ParentView;
      // 子菜单
      }else {
        tmp.component = loadView(tmp.component); // 懒加载业务组件
      }
    }
    // 递归处理子菜单
    if (tmp.children && tmp.children.length > 0) {
      tmp.children = filterAsyncRoutes(tmp.children);
    }
    res.push(tmp);
  });
  return res;
}

// 组件懒加载函数
export const loadView = (view) => {
  return () => import(`@/views/${view}`);
};

转换完成的路由会通过router.addRoutes方法注入 Vue Router,此时用户才能通过路由访问对应页面。

二、路由守卫:权限校验

即使动态路由已注入,仍需防止用户通过手动修改 URL 访问无权限页面。诺依通过全局路由守卫实现实时权限校验:

javascript 复制代码
// src/permission.js
router.beforeEach(async(to, from, next) => {
  // 从本地存储获取token
  const hasToken = getToken();
  
  if (hasToken) {
    if (to.path === '/login') {
      // 已登录跳转首页
      next({ path: '/' });
    } else {
      // 判断是否已加载权限信息
      const hasRoles = store.getters.roles && store.getters.roles.length > 0;
      if (hasRoles) {
        next();
      } else {
        try {
          // 加载权限信息
          const { roles } = await store.dispatch('user/getInfo');
          // 生成动态路由
          const accessRoutes = await store.dispatch('permission/generateRoutes');
          // 注入路由
          router.addRoutes(accessRoutes);
          // 重定向至目标路由(确保路由已生效)
          next({ ...to, replace: true });
        } catch (error) {
          // 权限加载失败,清除token并跳转登录页
          await store.dispatch('user/resetToken');
          Message.error(error || '权限加载失败');
          next(`/login?redirect=${to.path}`);
        }
      }
    }
  } else {
    // 未登录状态处理
    if (whiteList.indexOf(to.path) !== -1) {
      // 白名单路由直接放行
      next();
    } else {
      // 非白名单路由跳转登录页
      next(`/login?redirect=${to.path}`);
    }
  }
});

这段代码构成了权限校验的核心逻辑:未登录用户强制跳转登录页,已登录用户需完成权限加载与路由注入后才能访问页面。

三、动态菜单渲染:从数据到 UI 的转换

菜单是权限最直观的体现,诺依通过递归组件将权限菜单数据转换为可视化导航:​

  1. 菜单组件的递归渲染
javascript 复制代码
<!-- src/layout/components/Sidebar/SidebarMenu.vue -->
<template>
  <div class="menu-container">
    <el-menu
      :default-active="activeMenu"
      :collapse="isCollapse"
      mode="vertical"
    >
      <template v-for="(route, index) in routes">
        <!-- 有子菜单的项 -->
        <el-submenu
          v-if="route.children && route.children.length > 0"
          :index="route.path"
          :key="index"
        >
          <template #title>
            <svg-icon :icon-class="route.icon" />
            <span>{{ route.meta.title }}</span>
          </template>
          <!-- 递归渲染子菜单 -->
          <sidebar-menu
            :routes="route.children"
            :base-path="resolvePath(route.path)"
            :is-collapsed="isCollapse"
          />
        </el-submenu>
        
        <!-- 无子菜单的项 -->
        <el-menu-item
          v-else
          :index="route.path"
          :key="index"
          @click="handleClick(route)"
        >
          <svg-icon :icon-class="route.icon" />
          <span>{{ route.meta.title }}</span>
        </el-menu-item>
      </template>
    </el-menu>
  </div>
</template>

<script>
export default {
  name: 'SidebarMenu',
  components: { SidebarMenu }, // 递归组件注册
  props: {
    routes: { type: Array, required: true },
    basePath: { type: String, default: '' },
    isCollapsed: { type: Boolean, default: false }
  },
  methods: {
    resolvePath(path) {
      return this.basePath ? `${this.basePath}/${path}` : path;
    },
    handleClick(route) {
      // 菜单点击事件处理
      this.$router.push(route.path);
    }
  },
  computed: {
    activeMenu() {
      // 计算当前激活的菜单
      const route = this.$route;
      return route.path;
    }
  }
};
</script>

通过递归调用sidebar-menu组件,无论菜单层级有多深,都能被正确渲染为嵌套的下拉菜单。

四、按钮级权限控制:指令化实现方案

诺依通过自定义 Vue 指令v-permission实现按钮级别的权限控制,核心逻辑是 "权限校验 + DOM 操作":​

  1. 权限指令的实现
javascript 复制代码
// src/directive/permission/index.js
import Vue from 'vue';
import { hasPermission } from '@/utils/permission';

// 注册v-permission指令
Vue.directive('permission', {
  inserted(el, binding, vnode) {
    const { value } = binding;
    // 权限标识可以是字符串或数组
    if (value && value instanceof Array && value.length > 0) {
      const hasAuth = hasPermission(value);
      if (!hasAuth) {
        // 无权限则移除元素
        el.parentNode && el.parentNode.removeChild(el);
      }
    } else {
      throw new Error('v-permission指令需要传入权限标识数组,如v-permission="[\'system:user:add\']"');
    }
  }
});

// 权限校验工具函数
// src/utils/permission.js
export function hasPermission(permissions) {
  // 从Vuex获取用户拥有的权限集合
  const userPermissions = store.getters && store.getters.permissions;
  // 判断用户是否拥有目标权限(支持多权限"或"关系)
  return permissions.some(permission => userPermissions.includes(permission));
}
  1. 指令在页面中的使用​

在业务组件中,只需为按钮添加v-permission指令并传入所需权限标识,即可实现权限控制:

javascript 复制代码
<!-- src/views/system/user/index.vue -->
<template>
  <el-button 
    type="primary" 
    @click="handleAdd" 
    v-permission="['system:user:add']"
  >
    <i class="el-icon-plus"></i> 新增用户
  </el-button>
  
  <el-table-column label="操作" width="200">
    <template #default="scope">
      <el-button 
        size="mini" 
        @click="handleEdit(scope.row)" 
        v-permission="['system:user:edit']"
      >
        编辑
      </el-button>
      <el-button 
        size="mini" 
        type="danger" 
        @click="handleDelete(scope.row)" 
        v-permission="['system:user:delete']"
      >
        删除
      </el-button>
    </template>
  </el-table-column>
</template>

当用户无对应权限时,按钮会在组件初始化阶段被从 DOM 中移除,实现 "无权限则不可见" 的效果。

五、前端权限优化:缓存与动态更新

在实际应用中,权限可能会在用户使用过程中发生变更(如管理员修改了角色权限)。诺依通过以下方式优化权限动态性:​

  1. 权限缓存策略​
  • 基础权限数据(角色、权限标识)存储在 Vuex 中,同时备份到sessionStorage,避免页面刷新后权限丢失;
  • 动态路由信息仅存储在 Vue Router 实例中,刷新页面后会重新调用generateRoutes接口生成。
  1. 权限动态更新​

当权限发生变更时,前端可通过调用reloadPermission方法强制刷新权限:

javascript 复制代码
// 权限刷新方法
export function reloadPermission() {
  // 清除Vuex中的权限数据
  store.dispatch('user/resetInfo');
  // 重新加载权限信息
  store.dispatch('user/getInfo').then(() => {
    // 重新生成路由
    store.dispatch('permission/generateRoutes').then(routes => {
      // 重置路由并注入新路由
      router.matcher = new VueRouter({ mode: 'history' }).matcher;
      router.addRoutes(routes);
      // 刷新当前页面
      router.go(0);
    });
  });
}

这种方式虽然会导致页面刷新,但能确保权限变更立即生效,适合权限调整不频繁的场景。​

总结:前端权限的核心设计思想​

诺依前端权限管理的实现,本质是 **"数据驱动的 UI 适配"**:通过后端返回的权限数据,前端动态调整路由配置、菜单结构和按钮显隐,同时通过路由守卫和后端校验构建双重安全机制。​

其设计亮点在于:​

  • 指令化控制:将权限判断逻辑封装为指令,降低业务代码与权限逻辑的耦合;
  • 递归组件:通过递归实现任意层级菜单的渲染,适应复杂的权限结构;
  • 懒加载策略:路由组件和权限数据按需加载,平衡性能与安全性。

对于开发者而言,理解这些实现细节不仅能快速定位权限相关问题,更能在自定义权限系统时,借鉴其 "前端做展示控制,后端做安全校验" 的设计原则,构建既灵活又安全的权限体系。

相关推荐
周航宇JoeZhou2 小时前
JP3-3-MyClub后台后端(二)
java·mysql·vue·ssm·springboot·项目·myclub
yuanmenglxb20043 小时前
前端工程化包管理器:从npm基础到nvm多版本管理实战
前端·前端工程化
新手小新3 小时前
C++游戏开发(2)
开发语言·前端·c++
我不吃饼干4 小时前
【TypeScript】三分钟让 Trae、Cursor 用上你自己的 MCP
前端·typescript·trae
小杨同学yx5 小时前
前端三剑客之Css---day3
前端·css
编程社区管理员6 小时前
Vue项目使用ssh2-sftp-client实现打包自动上传到服务器(完整教程)
运维·服务器·vue
Mintopia7 小时前
🧱 用三维点亮前端宇宙:构建你自己的 Three.js 组件库
前端·javascript·three.js
故事与九7 小时前
vue3使用vue-pdf-embed实现前端PDF在线预览
前端·vue.js·pdf
Mintopia8 小时前
🚀 顶点-面碰撞检测之诗:用牛顿法追寻命运的交点
前端·javascript·计算机图形学