页面级别权限控制实操方法(前端动态路由与守卫)

页面级别的权限控制,在企业级 AI 网关项目里推荐用”后端返回路由 + 前端动态挂载”的方案:后端按用户角色返回可访问的菜单与路由,前端拿到后通过 addRoute 动态注册,配合全局路由守卫统一校验,未授权路径一律踢到 403 或登录页。静态写死路由的方案只适合内部小工具,一旦角色膨胀到几十个、菜单要按客户定制,改一次权限就要发一次版,根本扛不住。下面把我们在这套网关里落地的完整做法展开讲。

一、权限控制的分层模型

页面级权限只是权限体系的一层。网关项目里权限按粒度分成三层,每层有各自的控制点:

层级 控制对象 实现方式
页面级 路由地址 动态路由 + 路由守卫
菜单级 侧边栏菜单项 后端返回菜单树,前端渲染
按钮级 页面内操作 自定义指令 v-permission

页面级控制解决”能不能进这个页面”,按钮级控制解决”进了页面能不能点这个按钮”。两者配合才是一套完整权限。

二、为什么选动态路由而非静态路由

静态路由把全部路由一次性注册,在 meta 里标记 roles,跳转时校验。它的缺陷在网关项目里暴露得很明显:普通用户也能在浏览器里看到所有路由定义,通过地址栏直插越权页,只是前端不渲染而已;菜单藏在代码里,客户要定制菜单必须重新编译。

对比项 静态路由 动态路由
路由来源 前端代码写死 后端按角色返回
越权访问 需守卫拦截,遗漏即漏 未注册即不存在,天然不可达
菜单定制 重新发版 后端配置即可
首次加载 全量打包 常驻路由 + 按需挂载
适用场景 小型内部系统 企业级多角色系统

动态路由下,无权限的路径根本没被注册,访问时直接落到 404,安全性更高。

三、动态路由的实现步骤

3.1 后端返回路由与权限码

登录后网关接口返回角色、权限码列表和路由表。路由表结构大致如下,前端只负责把 component 字符串映射成真实组件:

// 后端返回的示例结构
const asyncRoutes = [
  {
    path: '/gateway/upstream',
    name: 'UpstreamManage',
    component: 'gateway/UpstreamManage', // 字符串,前端映射组件
    meta: { title: '上游管理', icon: 'server' },
  },
  {
    path: '/gateway/models',
    name: 'ModelConfig',
    component: 'gateway/ModelConfig',
    meta: { title: '模型配置', perm: 'gateway:model:edit' },
  },
]

3.2 权限 store 生成可挂载路由

前端维护一份”路由表到组件的映射”,把字符串 component 替换成 import 进来的组件,再按 meta.perm 过滤当前用户无权访问的路由:

// stores/permission.ts
import { defineStore } from 'pinia'

const componentMap = {
  'gateway/UpstreamManage': () => import('@/views/gateway/UpstreamManage.vue'),
  'gateway/ModelConfig': () => import('@/views/gateway/ModelConfig.vue'),
}

export const usePermissionStore = defineStore('permission', {
  state: () => ({ routes: [] as any[] }),
  actions: {
    async generateRoutes(userPerms: string[]) {
      const { asyncRoutes } = await getUserRoutes()
      const accessible = asyncRoutes.filter(
        (r) => !r.meta?.perm || userPerms.includes(r.meta.perm),
      )
      this.routes = accessible.map((r) => ({
        ...r,
        component: componentMap[r.component],
      }))
      return this.routes
    },
  },
})

3.3 路由守卫统一校验

全局前置守卫 beforeEach 承担三道判断:是否登录、是否需要拉权限、目标路由是否在权限内。注意动态路由挂载后要 next({ ...to, replace: true }) 重新触发一次导航,否则首次跳转会 404:

import router from '@/router'

const whiteList = ['/login', '/403']

router.beforeEach(async (to) => {
  const auth = useAuthStore()
  const perm = usePermissionStore()

  if (!auth.isLoggedIn) {
    return whiteList.includes(to.path) ? true : { path: '/login', query: { redirect: to.fullPath } }
  }
  if (to.path === '/login') return { path: '/' }

  if (perm.routes.length === 0) {
    try {
      const userPerms = await fetchUserPermissions()
      const routes = await perm.generateRoutes(userPerms)
      routes.forEach((r) => router.addRoute(r))
      return { ...to, replace: true } // 重新导航,保证动态路由生效
    } catch {
      auth.logout()
      return { path: '/login' }
    }
  }
  return true
})

路由挂载完,侧边栏菜单直接遍历 perm.routes 渲染,菜单与路由同源,不会出现”菜单有但路由没有”的错位。

四、按钮级权限用指令收敛

页面级挡住了路由,但”模型配置”页里的”删除模型”按钮未必所有人都有权点。按钮级控制我们封装成 v-permission 指令,绑定的权限码在用户权限列表中才保留元素:

import type { Directive } from 'vue'

export const permission: Directive = {
  mounted(el, binding) {
    const required = binding.value as string
    const auth = useAuthStore()
    if (required && !auth.permissions.includes(required)) {
      el.parentNode?.removeChild(el)
    }
  },
}

在模板里这样用:

// 模板中:删除按钮加 v-permission="'gateway:model:delete'"
// 无该权限码时,元素挂载后被直接移除,不触发任何点击事件

页面上几十个按钮都走这一条指令,权限判断逻辑只写一次。

五、白名单、403 与 404 的兜底

动态路由方案下”无权限”有两种表现:已登录但访问了不在自己路由表里的路径,落到 404;明确的越权操作,跳 403 页。我们约定 404 对应”路由不存在或无权访问”,403 对应”已知资源但无权限”,路由守卫和接口响应两处都做兜底。白名单只放 login、403、404 三个路径,其余全部要求登录态,避免任何绕过口子。

六、登出与权限变更时的路由回收

权限是动态的,不能只做”挂载”不做”回收”。用户登出时,把动态挂载的路由逐个 removeRoute,再清空权限 store,防止下一账号登录时残留上一账号的路由与菜单。管理员在后台调整角色权限后,前端收到提示即触发重新拉取,把旧路由移除、按新权限重新 addRoute。这套流程在网关项目里对接的是”模型可见范围””上游管理权”这类高频变更场景,回收做得干净,才不会出现权限被降级后页面仍可访问的问题。

// stores/permission.ts
actions: {
  resetRoutes() {
    this.routes.forEach((r) => router.removeRoute(r.name))
    this.routes = []
  },
}

路由守卫里已经保证”动态路由为空就重新生成”,所以回收后下一次导航会自动重建,不需要额外状态标记。

常见问题(FAQ)

Q1:动态路由刷新页面后为什么会 404?

刷新后路由被清空,守卫重新拉取并 addRoute,期间首次导航未命中,需返回重定向。

Q2:菜单和路由不一致怎么办?

让菜单直接遍历已挂载路由生成,保持单一数据源,从结构上杜绝错位。

Q3:按钮权限一定要做吗?

页面级只能挡路由,按钮级挡操作,两者缺一个都会出现可见但不可用的界面。

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 qiqicto@qq.com 举报,一经查实,本站将立刻删除。
赞 (0)
赵其鑫的头像赵其鑫管理团队

相关推荐

返回顶部