likes
comments
collection
share

从0到1搭建React从路由权限到按钮权限权限系统对于一个后台来说至关重要,权限管理是确保系统安全性、可靠性和稳定性的重

作者站长头像
站长
· 阅读数 54

前言

权限系统对于一个后台来说至关重要,权限管理是确保系统安全性、可靠性和稳定性的重要手段。现在开源的后台框架一般都是自带权限系统,自己用过的有两个OPSLIAnt Design Pro

一直以来都是用别人东西(也是很香),我认为权限管理可以大致分两个部分

  1. 路由权限,路由权限一般来说是基于角色的访问控制(Role-Based Access Control, RBAC),是普通用户还是管理员还是超级管理员。
  2. 模块权限,大到Section,小到Button,都是可以进行权限控制。这个可以按基于角色的访问控制(Role-Based Access Control, RBAC)基于属性的访问控制(Attribute-Based Access Control, ABAC)都是可以的。基于属性的访问控制(ABAC)提供了一种灵活而细粒度的权限管理方式。

基于角色的访问控制这个好理解,基于属性的访问控制那这个是什么。简单来说就是两个管理员账号,一个有删除用户的权限,一个没有删除用户的权限,这个可以归于用户属性,常见的还有环境属性等。

接下来将介绍如何从0到1搭建一个包含路由权限和按钮权限的React应用

简单的看下成果

从0到1搭建React从路由权限到按钮权限权限系统对于一个后台来说至关重要,权限管理是确保系统安全性、可靠性和稳定性的重

文章后面有完整代码地址

正文

项目结构搭建

这里项目dome演示采用Monorepo的代码仓库结构,将前端项目和后端项目放在一个仓库里。前端使用React,后端使用Nest

  1. 新建文件夹auth
  2. 使用pnpm init初始化
  3. 创建packages文件夹
  4. 创建pnpm-workspace.yaml文件
packages:
  - 'packages/*'

4. 在packages文件夹下面分别使用命令创建后端和前端项目

nest new backend
pnpm create vite

5. 在根目录在package.json下面添加两个命令,分别用来启动前端和后端项目。

  "scripts": {
    "start:frontend": "pnpm --filter frontend run start:dev",
    "start:backend": "pnpm --filter backend run start:dev",
  },

在前后端项目中也必须要有start:dev这个命令。Nest默认就有这个命令,修改前端项目的package.json

  "scripts": {
    "start:dev": "vite",
    "build": "tsc -b && vite build",
    "lint": "eslint .",
    "preview": "vite preview"
  },

完成后结构如下

从0到1搭建React从路由权限到按钮权限权限系统对于一个后台来说至关重要,权限管理是确保系统安全性、可靠性和稳定性的重

后端环境搭建

这里就初略说一下干了什么事,重点不在这里。

  1. 创建3个模块,登录、用户和权限模块

登录模块:用户登录、对权限检查

用户:为登录模块提供服务,查询用户

权限模块:为登录模块提供服务,生成token

  1. 创建了一个全局拦截器,统一数据的返回格式
  2. 创建了一个守卫,用来检验token
  3. 创建了全局的权限过滤器,对token错误时进行处理。

详细代码可以到仓库里面看。

权限管理思路

使用Provider来做,在系统初始化时获取当前账号的角色权限属性权限,并保存。

AuthProvider如下,对账号的角色权限属性权限角色路由进行管理,这样在全局都可以对权限进行使用和修改。

import { createContext, useEffect, useState } from 'react';
import { RouteItem } from '../../routes';

interface AuthContextType {
  moduleAuth: string[];
  roleAuth: string;
  roleRouterList: RouteItem[];
  changeRoleAuth: (roleAuth: string) => void;
  changeModuleAuth: (authList: string[]) => void;
  changeRoleRouterList: (roleRouterList: RouteItem[]) => void;
}
export const AuthContext = createContext<AuthContextType>({
  moduleAuth: [],
  roleAuth: '',
  roleRouterList: [],
  changeRoleRouterList: (roleRouterList: RouteItem[]) => {},
  changeModuleAuth: (authList: string[]) => {},
  changeRoleAuth: (roleAuth: string) => {},
});

interface Props {
  children: React.ReactNode;
}

const AuthProvider = (props: Props) => {
  const { children } = props;
  /** 属性权限 */
  const [moduleAuth, setModuleAuth] = useState<string[]>([]);
  /** 角色权限 */
  const [roleAuth, setRoleAuth] = useState<string>('');
  /** 角色路由权限列表 */
  const [roleRouterList, setRoleRouterList] = useState<RouteItem[]>([]);

  return (
    <AuthContext.Provider
      value={{
        moduleAuth,
        roleAuth,
        roleRouterList,
        changeRoleRouterList: setRoleRouterList,
        changeRoleAuth: setRoleAuth,
        changeModuleAuth: setModuleAuth,
      }}
    >
      {children}
    </AuthContext.Provider>
  );
};

export default AuthProvider;

为了方便使用,我们写一个useAuth

import React, { useContext } from 'react';
import { AuthContext } from './../AuthProvider'

export const useAuth = () => {
  return useContext(AuthContext);
};

将根元素包裹

createRoot(document.getElementById('root')!).render(
  <StrictMode>
    <AuthProvider>
      <BrowserRouter>
        <App />
      </BrowserRouter>
    </AuthProvider>
  </StrictMode>
);

到这里基本的框架有了。

路由权限

对于路由数据的来源有两种

(1). 路由数据写在项目中,一个角色权限对应着一个路由表。也可以是一个总的路由表,通过meta中的字段控制路由权限。对于明确各个角色权限可以使用这种。

(2). 后端返回路由数据,会更加灵活(可以随时修改角色的路由权限、添加角色),这种相当于直接得到第一种处理后的数据。

本文讲的是第一种通过meta中的字段控制路由权限。

这是我们的路由总表

const router: RouteItem[] = [
  {
    path: 'login',
    element: <Login />,
  },
  {
    path: '404',
    element: <NotFound />,
  },
  {
    path: '/',
    element: <Navigation />,
    children: [
      {
        path: 'home',
        element: <Home />,
        meta: {
          title: '首页',
        },
      },
      {
        path: 'user',
        meta: {
          title: '用户管理',
        },
        children: [
          {
            index: true,
            element: <Navigate replace to="list" />,
          },
          {
            path: 'list',
            element: <UserList />,
            meta: {
              title: '用户列表',
              auth: ['admin'], // 该路由需要管理员权限
            },
          },
          {
            path: 'order',
            element: <UserOrder />,
            meta: {
              title: '用户订单',
            },
          },
          {
            path: 'edit/:id',
            element: <EditUser />,
            meta: {
              isHide: true, // 是否在菜单中隐藏
            },
          },
        ],
      },
      {
        path: 'meal',
        meta: {
          title: '餐品管理',
        },
        children: [
          {
            index: true,
            element: <Navigate replace to="list" />,
          },
          {
            path: 'list',
            element: <MealList />,
            meta: {
              title: '餐品列表',
            },
          },
          {
            path: 'order',
            element: <MealOrder />,
            meta: {
              title: '餐品订单',
            },
          },
        ],
      },
      {
        path: 'system',
        meta: {
          title: '系统设置',
          auth: ['admin'], // 该路由需要管理员权限
        },
        children: [
          {
            index: true,
            element: <Navigate replace to="auth" />,
          },
          {
            path: 'auth',
            element: <AuthSet />,
            meta: {
              title: '权限设置',
            },
          },
          {
            path: 'role',
            element: <RoleSet />,
            meta: {
              title: '角色设置',
            },
          },
        ],
      },
      {
        index: true,
        element: <Navigate to="home" />, // 重定向到 /home
      },
    ],
  },
  {
    path: '*',
    element: <Navigate replace to="/404" />,
  },
];

这里又分为两种情况

  1. 从登录页进入
  2. 已登录
从登录页进入

登录时对角色权限属性权限角色路由进行保存

  const { changeRoleAuth, changeRoleRouterList, changeModuleAuth } = useAuth();

  const onClick = () => {
    axios({
      method: 'post',
      url: '/login',
      data: {
        name: user,
        password: password,
      },
    })
      .then((res: any) => {
        if (res.code == 200) {
          //保存token
          localStorage.setItem('token', res.data.token);
          //调用getRoleRouterList,获取账号对应的角色路由
          let resultRouter = getRoleRouterList(user);
          //保存角色路由权限列表
          changeRoleRouterList(resultRouter);
          //保存角色权限
          changeRoleAuth(res.data.auth);
          //保存属性权限
          changeModuleAuth(res.data.authModule);
          navigate(`/home`, {
            replace: false,
          });
        } else {
          messageApi.error(res.data.message);
        }
      })
      .catch(err => {
        messageApi.error('系统错误');
      });
  };

getRoleRouterList方法的实现,获取账号对应的角色路由

从0到1搭建React从路由权限到按钮权限权限系统对于一个后台来说至关重要,权限管理是确保系统安全性、可靠性和稳定性的重

这一步就已经有账号的权限角色路由了,接下来只要渲染就行了

//App.tsx部分代码
  const { roleRouterList, changeModuleAuth, changeRoleAuth, changeRoleRouterList } = useAuth();
  const [routes, setRoutes] = useState<any>(router);

  // 路由权限变化时更新路由
  useEffect(() => {
    if (roleRouterList.length > 0) {
      setRoutes(roleRouterList);
    }
  }, [roleRouterList]);

  if (isLoading) {
    return <Loading />;
  }

  return (
    <>
      <Element router={routes} />
    </>
  );
const Element = (props: any) => {
  const element = useRoutes(props.router);
  return <>{element}</>;
};
已登录

对于已登录情况,只需要把token给到后端,然后返回账号的角色权限属性权限

//App.tsx部分代码
  const { roleRouterList, changeModuleAuth, changeRoleAuth, changeRoleRouterList } = useAuth();
  const [isLoading, setLoading] = useState(true);
  const [routes, setRoutes] = useState<any>(router);
  let navigate = useNavigate();

  const onInit = () => {
    let token = localStorage.getItem('token');

    //是否有token
    if (!!token) {
      //检验token是否有效
      axios({
        method: 'post',
        url: '/login/check',
      })
        .then((res: any) => {
          if (res.code === 200) {
            let resultRouter = getRoleRouterList(res.data.auth);
            setRoutes(resultRouter);
            //保存角色权限
            changeRoleAuth(res.data.auth);
            //保存角色路由权限列表
            changeRoleRouterList(resultRouter);
            //保存属性权限
            changeModuleAuth(res.data.authModule);
            setLoading(false);
          } else {
            localStorage.clear();
            navigate('/login');
            setLoading(false);
          }
        })
        .catch(err => {
          setLoading(false);
        });
    } else {
      //没有token,返回登录页面
      setLoading(false);
      navigate('/login');
      localStorage.clear();
    }
  };

  // 初始化
  useEffect(() => {
    onInit();
  }, []);
  
   if (isLoading) {
    return <Loading />;
  }
  
    return (
    <>
      <Element router={routes} />
    </>
  );
属性权限

因为我们使用Provider包裹根元素,所以在全局可以访问权限。我们只需要写一个组件,根据账号的属性权限来控制显示还是消失就好了

AuthModule组件

从0到1搭建React从路由权限到按钮权限权限系统对于一个后台来说至关重要,权限管理是确保系统安全性、可靠性和稳定性的重

AuthModule组件的使用

<AuthModule module={['userAction']}>
    <Button>删除用户</Button>
    <Button>添加用户</Button>
</AuthModule>

<AuthModule 
    module={['userAction']} 
    role={['admin']}
>
    <Button>删除用户</Button>
    <Button>添加用户</Button>
</AuthModule>
左侧导航栏渲染

我们现在已经有角色的权限路由了,我们只要对权限路由进行处理得到菜单列表数据。

从0到1搭建React从路由权限到按钮权限权限系统对于一个后台来说至关重要,权限管理是确保系统安全性、可靠性和稳定性的重

处理出来的数据,直接给antdMenu组件就可以了

 <Menu
    mode="inline"
    theme="dark"
    selectedKeys={selectMenu}
    defaultOpenKeys={defaultOpenKeys}
    inlineCollapsed={false}
    items={navigationList}
    onClick={clickMenu}
    style={{
       height: '100%',
    }}
/>

结果如下

从0到1搭建React从路由权限到按钮权限权限系统对于一个后台来说至关重要,权限管理是确保系统安全性、可靠性和稳定性的重

问题补充
token失效跳转登录

设置响应拦截,对token失效的情况进行处理

axios.interceptors.response.use(
  response => {
    return response.data;
  },
  error => {
    message.error(error.response.data.message);
    //token失效
    if (error.response.status === 401) {
      navigateTo('/login');
      localStorage.clear();
    }

    return Promise.reject(error);
  }
);
当前页刷新时,菜单选中和父级展开
  1. 菜单选中,我们在获取菜单列表数据时,对路径进行了拼接。并设置为menukey,所以很容易,只要取当前的pathname
  /** 选中的菜单项 */
  const selectMenu = useMemo(() => {
    let path = location.pathname;
    return [path];
  }, [location]);
  1. 父级展开,我们只要设置Menu组件上的defaultOpenKeys属性就行了,但是要注意defaultOpenKeys要先有值,而不是页面渲染后defaultOpenKeys再有值。所以使用isLoading控制Menu组件的显示就行了
  let [isLoading, setIsLoading] = useState(true);
  let [defaultOpenKeys, setDefaultOpenKeys] = useState<string[]>([]);
  
  /** 刷新页面时,展开当前页面所在的菜单项 */
  useEffect(() => {
    if (!isLoading) {
      return;
    }

    let path = location.pathname;
    const fn = (list: any[], keys: string[]) => {
      for (let i = 0; i < list.length; i++) {
        let tempKeys = [...keys];
        let item = list[i];
        if (item.key === path) {
          setDefaultOpenKeys(tempKeys);
          break;
        }

        if (item.children && item.children.length > 0) {
          tempKeys.push(item.key);
          fn(item.children, tempKeys);
        }
      }
    };

    fn(navigationList, []);
    setIsLoading(false);
  }, [navigationList]);
  

结语

感兴趣的可以去试试

仓库地址:auth: 权限管理 (gitee.com)

转载自:https://juejin.cn/post/7413335608464097319
评论
请登录