以下笔记来源:编程导航
需求
- 能够灵活配置每个页面所需要的用户权限,由全局权限管理系统自动校验和拦截路由,而不需要在每个页面中编写权限校验代码,提高开发效率。(路由权限)
- 还要能够根据权限控制导航菜单的显隐,只有具有权限的菜单,才对用户可见。(菜单显隐)
实现思路
- 在路由配置文件, 定义某个路由的访问权限。由于 Next.js 项目是约定式路由,只有我们自定义的菜单配置文件,可以在菜单配置文件中定义权限。
- 每次访问页面时,根据用户要访问页面的路由权限信息,判断用户是否有对应的访问权限,并进行相应的拦截处理。这是一个全局逻辑,可以在项目根布局
app/layout.tsx
中添加。 - 导航栏展示菜单时,可以过滤掉登录用户没有权限的菜单项,从而实现根据权限控制导航菜单的显隐。
具体实现:
1. 新增 forbidden 页面
src/app/forbidden.tsx
import { Button, Result } from "antd";/*** 无权限页面* @constructor*/
const Forbidden = () => {return (<Resulttitle="403"status="403"subTitle="对不起,你无权访问该页面"extra={<Button type="primary" href={"/"}>返回首页</Button>}/>);
};export default Forbidden;
2. 定义权限常量和默认用户
src/access/accessEnum.ts
/*** 权限枚举*/
const Access_Enum = {NOT_LOGIN: "notLogin",USER: "user",ADMIN: "admin"
};
export default Access_Enum;
src/ constants/user.ts
// 默认用户
import Access_Enum from "@/access/accessEnum";const DEFAULT_USER: API.LoginUserVO = {userName: "未登录",userProfile: "暂无简介",userAvatar: "/assets/notLoginUser.png",userRole: Access_Enum.NOT_LOGIN,
};export default DEFAULT_USER;
3. 菜单配置和筛选
为需要权限的菜单增加配置项,并根据路径查找菜单。
config/menu.tsx
import { MenuDataItem } from "@ant-design/pro-layout";
import { CrownOutlined } from "@ant-design/icons";
import Access_Enum from "@/access/accessEnum";// 菜单列表
export const menus = [{path: "/",name: "主页",},{path: "/banks",name: "题库",},{path: "/questions",name: "题目",},{path: "/admin",name: "管理",icon: <CrownOutlined />,access: Access_Enum.ADMIN,children: [{path: "/admin/user",name: "用户管理",access: Access_Enum.ADMIN,},{path: "/admin/bank",name: "题库管理",access: Access_Enum.ADMIN,},{path: "/admin/question",name: "题目管理",access: Access_Enum.ADMIN,},],},
] as MenuDataItem[];/*** 根据路径查找菜单*/
export const findMenuItemByPath = (menus: MenuDataItem[],path: string,
): MenuDataItem | null => {for (let menu of menus) {if (menu.path === path) {return menu;}if (menu.children) {const matchedMenuItem = findMenuItemByPath(menu.children, path);if (matchedMenuItem) {return matchedMenuItem;}}}return null;
};/*** 根据路径查找所有菜单*/
export const findAllMenuItemByPath = (path: string): MenuDataItem | null => {return findMenuItemByPath(menus, path);
};
4. 编写通用权限校验方法
因为菜单组件中要判断权限、权限拦截也要用到权限判断功能,所以抽离成公共模块。
src/access/checkAccess.ts
import Access_Enum from "@/access/accessEnum";/*** 检查权限(判断当前登录用户是否具有某个权限)* @param loginUser 当前登录用户* @param needAccess 需要检查的权限* @return boolean 有无权限*/
const checkAccess = (loginUser: API.LoginUserVO,needAccess = Access_Enum.NOT_LOGIN,
) => {// 获取当前用户具有的权限 如果没有登录 默认没有权限const loginUserAccess = loginUser?.userRole ?? Access_Enum.NOT_LOGIN;// 如果当前不需要权限if (needAccess === Access_Enum.NOT_LOGIN) {return true;}// 如果需要权限为普通用户if (needAccess === Access_Enum.USER) {if (loginUserAccess === Access_Enum.NOT_LOGIN) {return false;}}// 如果需要权限为管理员if (needAccess === Access_Enum.ADMIN) {if (loginUserAccess !== Access_Enum.ADMIN) {return false;}}return true;
};export default checkAccess;
5. 新增权限校验布局
src/access/AccessLayout.tsx
import React from "react";
import {useSelector} from "react-redux";
import {RootState} from "@/stores";
import {usePathname} from "next/navigation";
import {findAllMenuItemByPath} from "../../config/menu";
import Access_Enum from "@/access/accessEnum";
import checkAccess from "@/access/checkAccess";
import Forbidden from "@/app/forbidden";/*** 统一权限校验拦截器* @param children* @constructor*/
const AccessLayout: React.FC<Readonly<{ children: React.ReactNode }>> = ({children}) => {const pathname = usePathname()// 当前登录用户const loginUser = useSelector((state: RootState) => state.loginUser);// 根据路径获取当前菜单const menu = findAllMenuItemByPath(pathname) || {};// 需要的权限const needAccess = menu?.access ?? Access_Enum.NOT_LOGIN;// 判断是否拥有权限const canAccess = checkAccess(loginUser, needAccess);if (!canAccess) {return <Forbidden />}return <>{children}</>
};export default AccessLayout;
6. 包裹(增强)基础布局
src/app/layout.tsx
export default function RootLayout({children,
}: Readonly<{children: React.ReactNode;
}>) {return (<html lang="zh"><body><AntdRegistry><Provider store={store}><InitLayout><BasicLayout><AccessLayout>{children}</AccessLayout></BasicLayout></InitLayout></Provider></AntdRegistry></body></html>);
}
7. 控制菜单显隐
src/access/menuAccess.ts
import { menus } from "../../config/menu";
import checkAccess from "@/access/checkAccess";const getAccessibleMenus = (loginUser: API.LoginUserVO, menuItems = menus) => {return menuItems.filter((item) => {if (!checkAccess(loginUser, item.access)) {return false;}if (item.children) {item.children = getAccessibleMenus(loginUser, item.children);}return true;});
};export default getAccessibleMenus;
然后就可以在基础布局页面的菜单渲染使用上进行使用:
src/layouts/BasicLayout/index.tsx
// 定义菜单
menuDataRender={() => {return getAccessibleMenus(loginUser, menus);
}}
8. 404 页面
未和路由菜单匹配的路径,404 未找到。
src/app/not-found.tsx (约定式)
import {Button, Result} from "antd";/*** 未找到页面* @constructor*/
const NotFound = () => {return (<Resulttitle="404"status="404"subTitle="抱歉,你访问的页面不存在"extra={<Button type="primary" href={"/"}>返回首页</Button>}/>);
};export default NotFound;
其他(高阶组件权限校验)
还有其他实现权限校验的方法,比如使用高阶组件(HOC)在客户端进行权限校验,这种方法会更灵活。如下:
// components/withAuth.js
import { useRouter } from 'next/router';
import { useEffect } from 'react';
import { useSelector } from 'react-redux'; // 或者使用其他全局状态管理库export default function withAuth(Component) {return function AuthenticatedComponent(props) {const router = useRouter();const isAuthenticated = useSelector((state) => state.auth.isAuthenticated); // 获取用户登录状态useEffect(() => {if (!isAuthenticated) {// 如果未登录,重定向到登录页面router.push('/login');}}, [isAuthenticated]);// 如果未登录,不渲染组件if (!isAuthenticated) {return null;}// 如果已登录,渲染组件return <Component {...props} />;};
}
使用这个 HOC 包裹需要进行权限校验的页面:
// pages/protected.js
import withAuth from '@/components/withAuth';function ProtectedPage() {return <div>This is a protected page.</div>;
}export default withAuth(ProtectedPage);