基于Umi 框架(Ant Design Pro 底层框架)的动态路由权限控制实现方案

在中后台前端开发中,动态路由权限控制是核心能力之一 ------ 系统需要根据当前登录用户的角色 / 权限,动态展示可访问的菜单并拦截无权限的路由访问。本文将基于 Umi 框架(Ant Design Pro 底层框架),详解如何实现精细化的动态路由权限判断。

一、核心场景与技术背景

1. 业务场景

  • 不同角色(如管理员、普通运营、访客)看到不同的菜单列表
  • 禁止用户通过手动输入 URL 访问无权限的路由
  • 兼容后端返回的多格式菜单数据(如 name/title 字段混用)

2. 技术栈基础

  • 框架:Umi 4(Ant Design Pro v6)
  • 核心能力:Umi 的 access 权限控制体系
  • 存储:sessionStorage 存储用户菜单数据
  • 数据结构:TypeScript 接口约束菜单 / 路由数据

二、完整实现代码与解析

1. 类型定义(核心接口)

首先通过 TypeScript 接口约束菜单、路由、用户数据结构,保证类型安全:

typescript

运行

复制代码
// src/access.d.ts
// 菜单单项接口(兼容后端多字段返回)
interface MenuItem {
  path?: string;           // 菜单路径
  name?: string;           // 菜单名称(优先)
  title?: string;          // 菜单标题(兼容字段)
  children?: MenuItem[];   // 子菜单
  [key: string]: any;      // 兼容其他自定义字段
}

// 本地存储的用户菜单数据结构
interface UserMenuData {
  menu?: MenuItem[];       // 用户拥有的菜单列表
}

// 路由匹配参数接口(Umi 路由对象子集)
interface Route {
  path?: string;           // 路由路径
  name?: string;           // 路由名称
}

// 扩展 Umi 的 Access 类型(可选)
declare module 'umi' {
  interface AccessState {
    currentUser?: API.CurrentUser;
  }
}

2. Access 权限控制核心逻辑

Umi 的 access 函数是权限控制的核心,返回的权限方法可在全局使用:

typescript

运行

复制代码
// src/access.ts
export default function access(initialState: { currentUser?: API.CurrentUser } | undefined) {
  // 从初始状态解构当前用户(Umi 初始化时传入)
  const { currentUser } = initialState ?? {};
  // 存储用户菜单数据的变量
  let menuData: MenuItem[] = [];

  return {
    // 核心方法:判断路由是否有权限访问
    hasRoute: (route: Route): boolean => {
      try {
        // 1. 从 sessionStorage 读取用户菜单数据
        const userDataStr = sessionStorage.getItem('userinfo');
        if (userDataStr) {
          const userData: UserMenuData = JSON.parse(userDataStr);
          menuData = userData.menu || [];
        }
      } catch (error) {
        // 异常处理:解析失败时重置菜单数据
        console.error('解析用户菜单数据失败:', error);
        menuData = [];
      }

      // 2. 根路径特殊处理:放行重定向逻辑
      if (route.path === '/') {
        return false; // 让路由配置中的 redirect 生效
      }

      // 3. 递归遍历菜单树,匹配路由
      const matchMenu = (menuList: MenuItem[]): boolean => {
        return menuList.some(item => {
          // 优先使用 name,兼容 title 字段
          const menuName = item.name || item.title;
          const routeName = route.name;

          // 路径匹配逻辑(含特殊场景兼容)
          let matchPath = false;
          if (item.path && route.path) {
            // 兼容:菜单根路径 "/" 匹配路由 "/index"
            if (item.path === '/' && route.path === '/index') {
              matchPath = true;
            } else {
              // 常规路径精确匹配
              matchPath = item.path === route.path;
            }
          }

          // 名称匹配逻辑
          const matchName = menuName === routeName;

          // 路径或名称匹配则返回有权限
          if (matchPath || matchName) {
            return true;
          }

          // 递归检查子菜单
          if (item.children && item.children.length > 0) {
            return matchMenu(item.children);
          }

          // 无匹配且无子菜单
          return false;
        });
      };

      // 4. 执行匹配并返回结果
      return matchMenu(menuData);
    },
  };
}

3. 关键逻辑解析

(1)菜单数据加载
  • sessionStorage.getItem('userinfo') 读取用户菜单数据(登录后由接口写入)
  • 异常捕获:防止 JSON 解析失败导致权限逻辑崩溃
  • 兜底处理:解析失败时重置 menuData 为空数组
(2)根路径特殊处理
  • 路由配置中根路径 '/' 通常配置为重定向(如跳转到 /dashboard
  • 返回 false 让 Umi 的路由重定向逻辑生效,避免权限拦截影响默认跳转
(3)递归菜单匹配
  • 双重匹配规则:路径(path)匹配 + 名称(name/title)匹配,兼容不同后端返回格式
  • 递归遍历 :支持多级嵌套菜单(如 /system/user/system/role
  • 短路逻辑Array.some() 找到匹配项立即返回,提升性能

三、实际应用场景

1. 路由守卫(菜单过滤)

config/routes.ts 中使用 access 控制路由是否显示:

typescript

运行

复制代码
// config/routes.ts
export default [
  {
    path: '/dashboard',
    name: 'dashboard',
    component: './Dashboard',
    access: 'hasRoute', // 使用 access 中的 hasRoute 方法判断权限
  },
  {
    path: '/system/user',
    name: 'systemUser',
    component: './System/User',
    access: 'hasRoute',
  },
];

2. 菜单渲染控制

在菜单组件中使用 useAccess 钩子过滤无权限菜单:

typescript

运行

复制代码
// src/components/Menu/index.tsx
import { useAccess } from 'umi';

const CustomMenu = () => {
  const access = useAccess();
  const menuData = useSelector(state => state.menu.data);

  // 过滤无权限的菜单
  const filteredMenu = menuData.filter(item => {
    return access.hasRoute({ path: item.path, name: item.name });
  });

  return <Menu data={filteredMenu} />;
};

3. 手动跳转拦截

在页面跳转前校验权限,防止无权限访问:

typescript

运行

复制代码
// src/utils/permission.ts
import { useAccess, history } from 'umi';

export const checkRouteAccess = (path: string, name?: string) => {
  const access = useAccess();
  const hasPermission = access.hasRoute({ path, name });
  if (!hasPermission) {
    history.push('/403'); // 跳转到无权限页面
    return false;
  }
  return true;
};

四、扩展优化方案

1. 性能优化

  • 缓存匹配结果 :对已匹配的路由结果做缓存,避免重复递归遍历

    typescript

    运行

    复制代码
    // 新增缓存对象
    const routeCache = new Map<string, boolean>();
    
    const matchMenu = (menuList: MenuItem[]): boolean => {
      const cacheKey = `${route.path}-${route.name}`;
      if (routeCache.has(cacheKey)) {
        return routeCache.get(cacheKey)!;
      }
      // 原有匹配逻辑...
      const result = menuList.some(/** 匹配逻辑 **/);
      routeCache.set(cacheKey, result);
      return result;
    };

2. 支持模糊匹配

适配动态路由(如 /detail/:id),扩展路径匹配逻辑:

typescript

运行

复制代码
// 模糊路径匹配(适配动态路由)
const matchDynamicPath = (menuPath: string, routePath: string): boolean => {
  // 移除动态参数(如 /detail/:id → /detail)
  const menuPathPure = menuPath.replace(/:\w+/g, '');
  const routePathPure = routePath.replace(/:\w+/g, '');
  return menuPathPure === routePathPure;
};

3. 权限刷新机制

用户切换角色 / 权限后,清空缓存并重新加载菜单:

typescript

运行

复制代码
// 新增刷新权限方法
refreshAccess: () => {
  menuData = [];
  routeCache.clear(); // 清空缓存
  const userDataStr = sessionStorage.getItem('userinfo');
  if (userDataStr) {
    const userData: UserMenuData = JSON.parse(userDataStr);
    menuData = userData.menu || [];
  }
}

五、总结

本文基于 Umi 框架的 access 权限体系,实现了一套通用的动态路由权限控制方案,核心亮点:

  1. 类型安全:通过 TypeScript 接口约束菜单 / 路由数据结构
  2. 兼容灵活:支持 name/title 字段混用、根路径特殊匹配
  3. 性能可靠:递归遍历 + 异常捕获 + 可选缓存优化
  4. 全局可用:权限方法可在路由配置、组件、工具函数中复用

该方案可直接应用于 Ant Design Pro 项目,也可适配其他基于 Umi 的中后台系统,只需根据实际业务调整菜单匹配规则(如模糊匹配、权限码匹配等)即可。

相关推荐
石像鬼₧魂石2 小时前
Fail2ban + Nginx/Apache 防 Web 暴力破解配置清单
前端·nginx·apache
weixin_464307633 小时前
设置程序自启动
前端
小满zs3 小时前
Next.js第十七章(Script脚本)
前端·next.js
小满zs4 小时前
Next.js第十六章(font字体)
前端·next.js
喝拿铁写前端9 小时前
别再让 AI 直接写页面了:一种更稳的中后台开发方式
前端·人工智能
A向前奔跑10 小时前
前端实现实现视频播放的方案和面试问题
前端·音视频
十一.36610 小时前
131-133 定时器的应用
前端·javascript·html
xhxxx11 小时前
你的 AI 为什么总答非所问?缺的不是智商,是“记忆系统”
前端·langchain·llm