路由和菜单的权限控制

1,205 阅读4分钟

1.菜单能正常加载的话,加载的菜单都是用户有权访问的,没权访问的不会显示出来,但是有个明显的权限问题,就是用户如果知道某个路由的访问url地址的话,直接在地址栏中输入url地址,是会正常加载该url的。比如我们之前做的登录日志列表的页面访问地址是/list/loginfo,我们先成功登录后,然后在地址栏中输入/list/loginfo,,登录日志列表页面是会正常加载的,如下图所示 image.png

2.如果我们想控制在地址栏中直接输入/list/loginfo,提示403,该如何实现呢?我们先参照官网文档路由和菜单的权限控制,理下思路。

如果需要对路由还有菜单进行权限控制,可以直接在路由上原有基础配置上加上权限控制相关的属性,即可快速实现路由和菜单的权限控制。(前提需要使用最佳实践的 Layout 方案 - @alipay/umi-plugin-layout )。

在以上定义(src/access.ts, src/app.ts)完成的基础上,再在路由配置项上添加 access 属性即可完成路由和菜单的权限控制。access 属性的值为 src/access.ts 中返回的对象的 key。以下为实际例子:

假设权限定义文件 src/access.ts 内容如下:


// src/access.ts
export default function (initialState = {}) {
  const { isAdmin, hasRoutes = [] } = initialState;
  return {
    // ...
    adminRouteFilter: () => isAdmin, // 只有管理员可访问
    normalRouteFilter: (route) => hasRoutes.includes(route.name), // initialState 中包含了的路由才有权限访问
  };
}
通过以上示例可以看到,权限路由控制相关的函数,接收"当前处理的路由"作为第一个参数

那么只需要按以下方式在常规路由配置中加上 access 这一项即可:

// config/config.ts
import { defineConfig } from 'umi';

export default defineConfig({
  routes: [
    {
      path: '/foo',
      name: 'foo',
      // ...
      access: 'normalRouteFilter', // 会调用 src/access.ts 中返回的 normalRouteFilter 进行鉴权
    },
    {
      path: '/admin',
      name: 'admin',
      // ...
      access: 'adminRouteFilter', // 会调用 src/access.ts 中返回的 adminRouteFilter 进行鉴权
    },
  ],
  // ...
});
对应鉴权函数(比如 adminRouteFilter)在接收路由作为参数后返回值为 false,该条路由将会被禁用,并且从左侧 layout 菜单中移除,如果直接从 URL 访问对应路由,将看到一个 403 页面。

3.官网这段话比较难理解,本人经过实践操作,确定如果要实现直接从 URL 访问对应(无权)路由,将看到一个 403 页面的效果,操作制步骤如下,先在src/access.ts中定义权限标识,比如在access.js(js版是js)中我们定义一个key叫 authorize的函数,authorize: (route) =>,官网上这句话:权限路由控制相关的函数,接收"当前处理的路由"作为第一个参数,也就是authorize的参数route就是当前正在访问的路由,然后我们可以得到当前用户有权访问的所有菜单,遍历菜单跟route参数做对比,如果route不在有权访问的菜单数组中,我们可以返回false,返回false即无权访问。下一步还要改config/config.js下的路由配置,我们的登录日志列表是配置在这里的,原始路由配置如下:

   {
          name: 'loginfor',
          icon: 'smile',
          path: '/list/loginfo',
          component: './loginfo',         
    },

我们在要控制的路由其中加一行access:authorize即可实现效果,authorize是我们之前在access中配置的key authorize,即

 {
          name: 'loginfor',
          icon: 'smile',
          path: '/list/loginfo',
          component: './loginfo',
          access: 'authorize',
  },

4.这里有一个小问题就是如何得到当前用户有权访问的所有菜单,我们之前是可以通过函数获取的,当然也可以再调用一下函数来获取,如果频繁要调用的数据,我们可以考虑放入initialState,全局初始数据中 ​

5.原理我们理清后,我们来做操作,完整的操作步骤如下,修改app.tsx,拿到菜单数据后,将放入到全局初始数据中

export const layout = ({ initialState,setInitialState  }) => {

layout中要传入setInitialState方法

   menu: {
      // 每当 initialState?.currentUser?.userId 发生修改时重新执行 request
      params: {
        userId: initialState?.currentUser?.userId,
      },
      request: async (params, defaultMenuData) => {

        const tempMenuData = await getCurrentUserMenus();
        const menuData=fixMenuItemIcon(tempMenuData);
        setInitialState({
          ...initialState,
          menuData: menuData,
        });
        return menuData;
      },

setInitialState中放入拿到的菜单数据menuData ​

6.打开src/services/ant-design-pro/menu.js,增加判定当前访问路由是否在有权限访问的菜单数组中的方法 getMatchMenuItem,

export function getMatchMenuItem(path, menuData){
  if(!menuData)
    return [];
  let items= [];
  menuData.forEach((item) => {
    if (item.path) {
      if (item.path === path) {
        items.push(item);
      }
      if (path.length >= item.path?.length) {
        const exp = `${item.path}/*`;
        if (path.match(exp)) {
          if(item.children) {
            const subpath = path.substr(item.path.length+1);
            const subItem = getMatchMenuItem(subpath, item.children);
            items = items.concat(subItem);
          } else {
            const paths = path.split('/');
            if(paths.length >= 2 && paths[0] === item.path && paths[1] === 'index') {
              items.push(item);
            }
          }
        }
      }
    }
  });
  return items;
}

7.打开src/access.js,先引入getMatchMenuItem方法,按照之前说的逻辑,增加authorize: (route) => {...}函数,完整的access.js代码如下:

/**
 * @see https://umijs.org/zh-CN/plugins/plugin-access
 * */

 import { getMatchMenuItem } from "./services/ant-design-pro/menu";

export default function access(initialState) {
  const { currentUser, menuData } = initialState || {};
  return {  
    authorize: (route) => {
      if(menuData) {
        const items = getMatchMenuItem(route.path, menuData);
        if(!items || items.length === 0){
          return false;
        } else {
          return true;
        }
      }
      return true;
    }, // initialState 中包含了的路由才有权限访问
  };
}

access.js中的menuData是从initialState中拿的。 ​

8.修改config/config.js中登录日志的路由配置,代码如下:

  {
          name: 'loginfor',
          icon: 'smile',
          path: '/list/loginfo',
          component: './loginfo',
          access: 'authorize',
        },

9.编译后,重启前台程序,这时候我们再登录后,在浏览器中直接访问/list/loginfo,就会返现报403了,截图如下: image.png 最近做了个小API应用,希望大家关注支持下: www.yuque.com/docs/share/…