以下笔记来源:编程导航
需求
- 能够灵活配置每个页面所需要的用户权限,由全局权限管理系统自动校验和拦截路由,而不需要在每个页面中编写权限校验代码,提高开发效率。(路由权限)
- 还要能够根据权限控制导航菜单的显隐,只有具有权限的菜单,才对用户可见。(菜单显隐)
实现思路
- 在路由配置文件, 定义某个路由的访问权限。由于 Next.js 项目是约定式路由,只有我们自定义的菜单配置文件,可以在菜单配置文件中定义权限。
- 每次访问页面时,根据用户要访问页面的路由权限信息,判断用户是否有对应的访问权限,并进行相应的拦截处理。这是一个全局逻辑,可以在项目根布局
app/layout.tsx中添加。 - 导航栏展示菜单时,可以过滤掉登录用户没有权限的菜单项,从而实现根据权限控制导航菜单的显隐。
具体实现:
1. 新增 forbidden 页面
src/app/forbidden.tsx
import { Button, Result } from "antd";
/**
* 无权限页面
* @constructor
*/
const Forbidden = () => {
return (
<Result
title="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 (
<Result
title="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);