Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V6服务器获取菜单并配置权限,刷新后出现403🐛 [BUG] #10827

Closed
africa1207 opened this issue Jul 4, 2023 · 6 comments
Closed
Labels

Comments

@africa1207
Copy link

🐛 bug 描述

菜单通过服务端获取,本地routes.ts中配置了所有菜单,并结合access.ts配置了对应菜单的权限,第一次user登录后菜单和权限都正常,通过浏览器输入没权限路由提示403,bug来袭

  1. 当此时进入有权限页面时,刷新页面,此时有权限页面也提示403
  2. 当此时进入有权限页面时,退出账号,再次登录user会redirect到有权限页面,但是提示403

🏞 期望结果 | Expected results

刷新页面或者退出后重登账号重定向到页面不提示403

💻 复现代码 | Recurrence code

线上代码:https://codesandbox.io/p/github/africa1207/ant-design-pro-v6/dev

核心代码内容如下

  1. mock/route.ts,返回menu菜单,并结合access.ts中canAccess来进行权限校验
export default {
  'GEt /api/getMenuList': async (req: Request, res: Response) => {
    res.send({
      success: true,
      data: [
        {
          path: '/welcome',
          name: '欢迎页',
          icon: 'smile',
        },
        {
          name: '表格',
          icon: 'table',
          path: '/list',
        },
        {
          path: '/charts',
          name: '图表',
          icon: 'plus',
          routes: [
            {
              path: '/charts/line-charts',
              name: '线性图表',
            },
          ],
        },
      ],
    });
  },
};
  1. app.tsx,getInitialState中返回空的userRoutes,menu的request中获取到菜单后再setInitialState,userRoutes用于结合access.ts中对匹配路由进行权限校验
export async function getInitialState(): Promise<{
  settings?: Partial<LayoutSettings>;
  currentUser?: API.CurrentUser;
  loading?: boolean;
  fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
  userRoutes?: API.MenuRouteItem[];
}> {
  const fetchUserInfo = async () => {
    try {
      const msg = await queryCurrentUser({
        skipErrorHandler: true,
      });
      return msg.data;
    } catch (error) {
      history.push(loginPath);
    }
    return undefined;
  };

  // 如果不是登录页面,执行
  const { location } = history;
  if (location.pathname !== loginPath) {
    const currentUser = await fetchUserInfo();
    const userRoutes: any[] = [];
    return {
      fetchUserInfo,
      userRoutes,
      currentUser,
      settings: defaultSettings as Partial<LayoutSettings>,
    };
  }

  return {
    fetchUserInfo,
    settings: defaultSettings as Partial<LayoutSettings>,
  };
}
export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
 return {
  // ...其他代码
  menu: {
      // 每当 initialState?.currentUser?.userid 发生修改时重新执行 request
      params: {
        userid: initialState?.currentUser?.userid,
      },
      request: async (params, defaultMenuData) => {
        if (params.userid) {
          const { data } = await getMenuList();
          // eslint-disable-next-line @typescript-eslint/no-use-before-define
          const menuData = processRoutes(data) || [];
          const fixMenuData = FixMenultemlcon(menuData);
          setInitialState({ ...initialState, userRoutes: fixMenuData });
          return fixMenuData;
        }
        return [];
      },
    },
 }
}
// 处理服务端返回菜单,如果菜单项包含routes但是为空,则隐藏该菜单
function processRoutes(routes: API.MenuRouteItem[]) {
  if (!routes) {
    return;
  }
  routes.forEach((route) => {
    if (route.routes) {
      processRoutes(route.routes);
      if (!route.routes.length) {
        route.hideInMenu = true;
      }
    }
  });
  return routes;
}
  1. config/routes.ts,本地写入所有页面,对需要和菜单比对的页面加上canAccess字段
export default [
  {
    path: '/user',
    layout: false,
    routes: [
      {
        name: 'login',
        path: '/user/login',
        component: './User/Login',
      },
    ],
  },
  {
    path: '/welcome',
    name: '欢迎页',
    icon: 'smile',
    component: './Welcome',
  },
  {
    path: '/admin',
    name: '管理台',
    icon: 'crown',
    access: 'canAccess',
    routes: [
      {
        path: '/admin',
        component: './RoutePage',
        access: 'show',
      },
      {
        path: '/admin/sub-page',
        name: '子页面',
        component: './Admin',
        access: 'canAccess',
      },
    ],
  },
  {
    name: '表格',
    icon: 'table',
    path: '/list',
    component: './TableList',
    access: 'canAccess',
  },
  {
    path: '/charts',
    name: '图表',
    icon: 'plus',
    access: 'canAccess',
    routes: [
      {
        path: '/charts',
        component: './RoutePage',
        access: 'show',
      },
      {
        path: '/charts/line-charts',
        name: '线性图表',
        component: './Charts/LineCharts/index',
        access: 'canAccess',
      },
      {
        path: '/charts/bar-charts',
        name: '柱状图表',
        component: './Charts/BarCharts/index',
        access: 'canAccess',
      },
    ],
  },
  {
    path: '/',
    redirect: '/welcome',
  },
  {
    path: '*',
    layout: false,
    component: './404',
  },
];
  1. src/access.ts
export default function access(
  initialState: { currentUser?: API.CurrentUser; userRoutes?: API.MenuRouteItem[] } | undefined,
) {
  const { currentUser, userRoutes } = initialState ?? {};

  // canAccess参数route是routes.ts中需要校验的route项
  function canAccess(route: API.MenuRouteItem) {
    /// 管理员角色直接返回 true
    if (currentUser?.roles?.includes('admin')) {
      return true;
    }
    // 检查是否需要特定角色才能访问
    if (route.roles && !route.roles.some((role) => currentUser?.roles?.includes(role))) {
      return false;
    }
    // 检查是否在用户菜单中
    if (!userRoutes) {
      return false;
    }
    // 遍历用户菜单,递归检查子路由
    for (const userRoute of userRoutes) {
      if (userRoute.path === route.path) {
        return true;
      }
      if (userRoute.routes) {
        const hasAccess = canAccess(userRoute);
        if (hasAccess) {
          return true;
        }
      }
    }

    return false;
  }
  return {
    canAdmin: currentUser && currentUser.roles?.includes('admin'),
    canAccess,
    show: true,
  };
}

© 版本信息

  • Ant Design Pro 版本:
  • 当前仓库最新版本(2023.5.28) https:github.com/ant-design/ant-design-pro/commit/753945ec3d561a81851c3d6861b365f0a837c711
  • Umi 版本
  • 4.0.69
  • 浏览器环境
  • chrome 116.0.5845.14 dev
  • 开发环境 [e.g. mac OS]
  • Windows11

🚑 其他信息

@africa1207 africa1207 changed the title 服务器获取菜单并配置权限,刷新后出现403🐛 [BUG] V6服务器获取菜单并配置权限,刷新后出现403🐛 [BUG] Jul 4, 2023
@tanqin
Copy link

tanqin commented Sep 9, 2023

我也遇到了同样的问题,进入有权限的页面,刷新页面,发现不会重新触发 request 去获取菜单信息导致 403 了

@chenshuai2144
Copy link
Collaborator

TAutomatically replying with ChatGPT can be attempted, but it cannot be guaranteed to be completely accurate and may not fully address all issues. Please feel free to reply if you have any further questions or concerns.
此回复基于 ChatGPT 自动生成,可以尝试下方案,官方人员会在一定时间后继续继续处理。

问题描述:

在Ant Design Pro v6中,使用V6服务器从服务端获取菜单并配置权限。第一次登录后,菜单和权限都正常,但是在刷新页面或退出登录后重新登录时,重定向到有权限的页面时出现403错误。

解决方案:

问题的根本原因是刷新页面或重新登录时,没有重新触发request去获取菜单信息。可以通过以下方式解决:

  1. src/access.ts文件中,添加一个fetchMenu的异步函数,用于获取菜单数据。
import { getMenuList } from './services/menu'; // 假设菜单服务的请求方法为getMenuList

async function fetchMenu() {
  try {
    const response = await getMenuList();
    const { data } = response;
    return data;
  } catch (error) {
    console.error('Failed to fetch menu data:', error);
    throw error;
  }
}

// 将fetchMenu导出
export { fetchMenu };
  1. src/app.tsx文件中,在getInitialState函数中添加一个fetchMenu的参数,并在setInitialState时传递给menu.request
import { fetchMenu } from './access'; // 引入fetchMenu函数

export async function getInitialState(fetchMenu) {
  // ...

  // 在if (location.pathname !== loginPath) {}代码块中获取菜单数据
  if (location.pathname !== loginPath) {
    const currentUser = await fetchUserInfo();
    const userRoutes: any[] = [];
    
    // 获取菜单数据
    const menuData = await fetchMenu();    
    
    return {
      fetchUserInfo,
      userRoutes,
      currentUser,
      settings: defaultSettings as Partial<LayoutSettings>,
      menuData, // 将菜单数据添加到initialState中
    };
  }

  return {
    fetchUserInfo,
    settings: defaultSettings as Partial<LayoutSettings>,
  };
}

export const layout: RunTimeLayoutConfig = ({ initialState, setInitialState }) => {
  // ...
  menu: {
    params: {
      userid: initialState?.currentUser?.userid,
    },
    request: async (params, defaultMenuData) => {
      if (params.userid) {
        // 使用initialState中的菜单数据
        const menuData = initialState.menuData || [];
        const fixMenuData = FixMenultemlcon(menuData);
        setInitialState({ ...initialState, userRoutes: fixMenuData });
        return fixMenuData;
      }
      return [];
    },
  },
  // ...
}

通过上述修改,每次刷新页面或重新登录时,都会重新触发menu.request,从而获取最新的菜单数据,解决了403错误的问题。

这里假设getMenuList是一个用于从服务端获取菜单数据的请求方法,你需要根据自己的实际情况来修改。同样,你可能还需要根据自己的菜单数据结构和权限配置进行相应的调整。

相关文档:

@chenshuai2144
Copy link
Collaborator

TAutomatically replying with ChatGPT can be attempted, but it cannot be guaranteed to be completely accurate and may not fully address all issues. Please feel free to reply if you have any further questions or concerns.
此回复基于 ChatGPT 自动生成,可以尝试下方案,官方人员会在一定时间后继续继续处理。

对于这个问题,可能是因为刷新页面或重新登录时,用户权限未正确更新导致的。你可以尝试以下解决方案:

  1. 确保每次刷新页面或重新登录后,都重新获取用户的权限信息并更新用户菜单。

app.tsx文件的getInitialState函数中,根据params.userid获取用户权限时,将userRoutes设置为空数组。这会将用户的权限信息设置为空,导致刷新页面或重新登录后的重定向页面没有权限。

你可以通过在fetchUserInfo中重新获取用户权限,并将其设置到userRoutes中来解决这个问题。修改如下:

export async function getInitialState(): Promise<{
  settings?: Partial<LayoutSettings>;
  currentUser?: API.CurrentUser;
  loading?: boolean;
  fetchUserInfo?: () => Promise<API.CurrentUser | undefined>;
  userRoutes?: API.MenuRouteItem[];
}> {
  const fetchUserInfo = async () => {
    try {
      const msg = await queryCurrentUser({
        skipErrorHandler: true,
      });
      const currentUser = msg.data;
      // 根据当前用户获取权限
      const { data: userRoutes } = await getPermission(currentUser.userid);
      
      return { currentUser, userRoutes };
    } catch (error) {
      history.push(loginPath);
    }
    return undefined;
  };

  // 如果不是登录页面,执行
  const { location } = history;
  if (location.pathname !== loginPath) {
    const { currentUser, userRoutes } = await fetchUserInfo();
    return {
      fetchUserInfo,
      currentUser,
      userRoutes,
      settings: defaultSettings as Partial<LayoutSettings>,
    };
  }

  return {
    fetchUserInfo,
    settings: defaultSettings as Partial<LayoutSettings>,
  };
}

这样,每次刷新页面或重新登录后,都会重新获取用户权限信息并更新userRoutes。确保用户菜单的权限正确。

  1. 确保权限校验正确

access.ts文件中,canAccess函数用于校验用户是否有访问特定页面的权限。你需要确保校验逻辑正确,并匹配用户的菜单信息。

请检查以下几点:

  • 确保currentUser.rolesroute.roles都是数组类型。
  • 确保用户的菜单信息userRoutes是正确的,并且包含了所有需要校验的菜单项。可以在获取菜单数据后进行打印,确认菜单信息是否正常获取并设置到了userRoutes中。
  • 确保userRoute的遍历逻辑正确,递归调用canAccess函数时传入的参数应该是子菜单项。

你可以在canAccess函数中添加一些console.log语句,打印相关变量,以便检查校验的逻辑是否正确。

另外,可以根据具体的需求,调整权限校验逻辑。例如,对于管理员角色(admin),直接返回true,或者对于不需要权限校验的页面,也可以直接返回true,避免出现不必要的403错误。

希望以上解决方案对你有帮助。如果问题仍然存在,请提供更多信息以便我们进一步分析和解决。

@wuxushun
Copy link

遇到同样的问题,在一个有权限的页面F5刷新会先进暂无全线页。查看access逻辑,console输出在没有获取到menu的时候返回的是false,获取菜单以后返回了true,但是还是渲染403页。打开后进入菜单就都正常了

@chenshuai2144
Copy link
Collaborator

layout 上面加个 params={{ timesnap: Date.now()}}, 看样子是 request 被缓存了

@ft20082
Copy link

ft20082 commented Jan 25, 2024

for (const userRoute of userRoutes) {
if (userRoute.path === route.path) {
return true;
}
if (userRoute.routes) {
const hasAccess = canAccess(userRoute);
if (hasAccess) {
return true;
}
}
}

你的这个逻辑有问题,递归不能这么写,canAccess 初始传进去的 route 是某个菜单,即 MenuDataItem,你需要递归你服务端返回的内容和传进去的 route 菜单比较才对

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

5 participants