广告位联系
返回顶部
分享到

React-Router(V6)的权限控制实现

JavaScript 来源:互联网 作者:佚名 发布时间:2023-05-06 23:31:10 人浏览
摘要

在一个后台管理系统中,安全是很重要的。不光后端需要做权限校验,前端也需要做权限控制。 我们可以大致将权限分为3种:接口权限、页面权限、按钮权限。 在这当中,前端主要关

在一个后台管理系统中,安全是很重要的。不光后端需要做权限校验,前端也需要做权限控制。 我们可以大致将权限分为3种: 接口权限、页面权限、按钮权限。

在这当中,前端主要关注点则是页面权限,按钮权限,而前端做这些的主要目的则是:

  • 禁止用户访问一些无权限访问的页面
  • 过滤不必要的请求,减少服务器压力

下面主要是思路的整理,以及一些核心实现

接口权限

接口权限一般是用户登录后,后端根据账号密码来认证和授权,并颁发token或者session等来保存用户登录状态。

后续客户端请求一般是在header中携带token,后端通过对token进行鉴权是否合法来控制是否可以访问接口。

一般后台会通过用户的角色等来做对应的接口权限控制。

而需要我们前端做的是在请求中携带好登录后回传的token,我们以axios为例

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

const instance = axios.create(config);

instance.interceptors.request.use(

  (request: any) => {

    request.headers["access_token"] = localStorage.getItem("access_token");

    return request;

  },

  (err) => {

    Promise.reject(err.response);

  }

);

instance.interceptors.response.use(

  (response) => {

    if (response.status !== 200) return Promise.reject(response.data);

    if (response.data.code === 401) {

      //token过期或者错误

      window.location.replace("/login");

    }

    return response.data.data;

  },

  (err) => {

    Promise.reject(err.response);

  }

);

页面权限

首先,我们先完成路由配置

src/routes/routes.tsx

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

export type RoutesType = {

  path: string;

  element: ReactElement;

  children?: RoutesType[];

};

const routers: RoutesType[] = [

  {

    path: "/login",

    element: <Login />,

  },

  {

    path: "/",

    element: <Home />,

  },

  {

    path: "/foo",

    element: <Foo />,

    children: [

      {

        path: "/foo/auth-button",

        element: <MyAuthButtonPage />,

      },

    ],

  },

  {

    path: "/protected",

    element: <Protected />,

  },

  {

    path: "/unauthorized",

    element: <UnauthorizedPage />,

  },

  // 配置404,需要放在最后

  {

    path: "/*",

    element: <NotFound />,

  },

];

然后是基于路由配置来生成对应的路由组件

src/routes/root.tsx

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

const Root = () => {

  // 创建一个有子节点的Route

  const CreateHasChildrenRoute = (route: RoutesType) => {

    return (

      <Route path={route.path} key={route.path}>

        <Route

          index

          element={

            <AuthRoute key={route.path} path={route.path}>

              {route.element}

            </AuthRoute>

          }

        />

        {route?.children && RouteAuthFun(route.children)}

      </Route>

    );

  };

  // 创建一个没有子节点的Route

  const CreateNoChildrenRoute = (route: RoutesType) => {

    return (

      <Route

        key={route.path}

        path={route.path}

        element={

          <AuthRoute path={route.path} key={route.path}>

            {route.element}

          </AuthRoute>

        }

      />

    );

  };

  // 处理我们的routers

  const RouteAuthFun = (routeList: any) => {

    return routeList.map((route: RoutesType) => {

      let element: ReactElement | null = null;

      if (route.children && !!route.children.length) {

        element = CreateHasChildrenRoute(route);

      } else {

        element = CreateNoChildrenRoute(route);

      }

      return element;

    });

  };

  return (

    <BrowserRouter>

      <Routes>{RouteAuthFun(routers)}</Routes>

    </BrowserRouter>

  );

};

最后是只需要在入口中写入Root组件即可

1

2

3

4

5

ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(

  <Provider store={store}>

    <Root />

  </Provider>

);

上面只是完成了基本的配置,下面才是权限相关

路由权限主要分为两个方向:

1. 菜单权限

一般来说,后台通过维护user、role、menu、user_role、menu_role这几张表来做相应的权限设计。

所以,在登录接口中,一般后台会返回用户对应的角色、菜单等信息。我们通过redux-toolkit保存登录数据。大致信息如下(未真正请求接口,只写了初始数据):

src/pages/login/Login.slice.ts

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

interface LoginState {

  username: string;

  role: string;

  menuLists: any[];

}

// Define the initial state using that type

const initialState: LoginState = {

  username: "ryo",

  role: "admin",

  menuLists: [

    {

      id: "1",

      name: "首页",

      icon: "icon-home",

      url: "/",

      parent_id: "0",

    },

    {

      id: "2",

      name: "foo",

      icon: "icon-foo",

      url: "/foo",

      parent_id: "0",

    },

    {

      id: "2-1",

      name: "auth-button",

      icon: "icon-auth-button",

      url: "/foo/auth-button",

      parent_id: "2",

    },

  ],

};

这里的role表示当前用户的角色,menuLists为用户可访问的菜单

然后在首页中生成菜单列表

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

const getMenuItem = (menus: any): any => {

  return menus.map((menu: any) => {

    if (menu.children) {

      return (

        <div key={menu.url}>

          <Link to={menu.url}>{menu.name}</Link>

          {getMenuItem(menu.children)}

        </div>

      );

    }

    return (

      <div key={menu.url}>

        <Link to={menu.url}>{menu.name}</Link>

      </div>

    );

  });

};

function genMenu(array: any, parentId = "0") {

  const result = [];

  for (const item of array) {

    if (item.parent_id === parentId) {

      const menu = { ...item };

      menu.children = genMenu(array, menu.id);

      result.push(menu);

    }

  }

  return result;

}

function Home() {

  const menuLists = useAppSelector((state) => state.login.menuLists);

  const menuTree = genMenu(menuLists);

  return (

    <div>

      <h1>home page</h1>

      {getMenuItem(menuTree)}

    </div>

  );

}

export default Home;

但是,只根据权限列表来动态生成菜单并不能完全实现权限相关的目的。用户还可以通过在地址栏输入url的方式来访问没有在菜单中显示的页面。

2. 路由权限

我们可以通过实现一个AuthRoute来解决上述的问题。

通过AuthRoute来拦截页面的访问操作。

src/routes/AuthRoute.tsx

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

// 无需权限认证的白名单

// 一般是前端的一些报错页

const DONT_NEED_AUTHORIZED_PAGE = ["/unauthorized", "/*"];

const AuthRoute = ({ children, path }: any) => {

  // 该flag用于控制 受保护页面的渲染时机,需要等待useEffect中所有的权限验证条件完成后才表示可以渲染

  const [canRender, setRenderFlag] = useState(false);

  const navigate = useNavigate();

  const menuLists = useAppSelector((state) => state.login.menuLists);

  const menuUrls = menuLists.map((menu) => menu.url);

  const token = localStorage.getItem("access_token") || "";

  // 在白名单中的无需验证,直接跳转

  if (DONT_NEED_AUTHORIZED_PAGE.includes(path)) {

    return children;

  }

  useEffect(() => {

    // 用户未登录

    if (token === "") {

      message.error("token 过期,请重新登录!");

      navigate("/login");

    }

    // 已登录

    if (token) {

      // 已登录需要通过logout来控制退出登录或者是token过期返回登录界面

      if (location.pathname == "/login") {

        navigate("/");

      }

      // 已登录,根据后台传的权限列表做判断

      if (!menuUrls.includes(location.pathname)) {

        navigate("/unauthorized", { replace: true });

      }

    }

    // 当上面的权限控制通过后,再渲染受保护的页面

    setRenderFlag(true);

  }, [token, location.pathname]);

  if (!canRender) return null;

  return children;

};

export default AuthRoute;

然后,在我们生成Route的时候在element属性中使用AuthRoute,这一步,我们已经在上面src/routes/root.tsx这个文件中写进去了。

到这里,我们就通过实现AuthRoute来拦截页面访问,做权限相关处理。

然后我们可以运行该仓库 代码来看效果。

目前没有实现登录相关功能,所以需要手动在localStorage中添加access_token来模拟登录。

  • 如果没有登录(没有access_token)或者登录已过期,访问任何路由都会被路由到/login。
  • 如果已经登录,但是再访问登录页面,会被路由到/首页
  • 如果已经登录,但是访问了一个你无访问的页面,如/protected,则会被路由到/unauthorized页面

按钮权限

按钮级别的权限,根据当前用户角色的不同,可以看到的按钮和操作不同。这里我只简单实现了一个AuthButton

src/coponents/auth-button/index.tsx

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

import { Button } from "antd";

import type { ButtonProps } from "antd";

import React from "react";

import { useAppSelector } from "../../hooks/typedHooks";

interface AuthButtonProps extends ButtonProps {

  roles: string[];

}

const AuthButton: React.FC<AuthButtonProps> = ({ roles, children }) => {

  const role = useAppSelector((state) => state.login.role);

  if (roles.includes(role)) {

    return <Button>{children}</Button>;

  }

  return null;

};

export default AuthButton;

使用方法如下,新增了一个roles属性,表示哪些角色可以看见该按钮

src/pages/foo/auth-button.tsx

1

2

3

4

5

6

7

8

9

10

11

12

const ButtonPermission: React.FC = () => {

  const role = useAppSelector((state) => state.login.role);

  return (

    <div>

      <h1>Button Permission</h1>

      <AuthButton roles={["admin", "user"]}>添加</AuthButton>

      <AuthButton roles={["admin"]}>编辑</AuthButton>

      <AuthButton roles={["admin"]}>删除</AuthButton>

    </div>

  );

};

export default ButtonPermission;

我们可以手动的修改Login.slice.ts中的role来查看不同的情况。

这种实现方式比较简单,大伙可以根据自己的具体场景选择更好的方案


版权声明 : 本文内容来源于互联网或用户自行发布贡献,该文观点仅代表原作者本人。本站仅提供信息存储空间服务和不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权, 违法违规的内容, 请发送邮件至2530232025#qq.cn(#换@)举报,一经查实,本站将立刻删除。
原文链接 :
相关文章
  • JS滚动到顶部踩坑解决记录
    一般在比较长的页面中会有一个滚动到顶部的按钮,用户点击一下 300ms 内,会滚动到顶部,有动画效果。 一开始我想,这不是很简单,一行
  • React-Router(V6)的权限控制实现
    在一个后台管理系统中,安全是很重要的。不光后端需要做权限校验,前端也需要做权限控制。 我们可以大致将权限分为3种:接口权限、页
  • Vue3之getCurrentInstance与ts结合使用的方式
    getCurrentInstance与ts结合使用 vue3项目中,如果不用ts这样使用是没问题的 1 const { proxy } = getCurrentInstance() 在ts中使用会报错:报错:...类型Co
  • Vue3进阶主题Composition API使用介绍
    Composition API 是 Vue3 中引入的一种新的 API 风格,旨在提高代码的可读性、可维护性和可重用性。Composition API 不同于 Vue2 中的 Options API,它采
  • ChatGPT Notion AI 从注册到体验及免费使用过程

    ChatGPT Notion AI 从注册到体验及免费使用过程
    Notion AI 是一款基于人工智能的文本编辑工具,具备生成内容、修改内容的功能,可用于文稿写作、会议日程、新闻稿、销售文案等内容的辅
  • ChatGPT如何写好Prompt编程
    现在已经产生了一种新职业:Prompt Engineer(提示指令工程师),可见 Prompt 是多么重要,且编写不易。 ChatGPT的产出,一半决定于它的实力,
  • ChatGPT前端编程秀之别拿编程语言不当语言

    ChatGPT前端编程秀之别拿编程语言不当语言
    写完小工具,这一篇回来我们接着写我们的程序。再看一眼我们的程序运行视图: 带着TDD思路,我进入了 ejs_and_yaml_dsl_loader 这个模块,这块
  • Angular独立组件入门指南
    如果你正在学习Angular,那么你可能已经听说过独立组件(Component)。顾名思义,独立组件就是可以独立使用和管理的组件,它们能够被包含
  • 解决JS请求路径控制台报错 Failed to launch'xxx' bec
    控制台报错: Failed to launch xxx because the scheme does not have a registered handler. 这种错误是因为请求没有协议,应该把协议头加上 错误的例子 window.
  • 网页资源阻塞浏览器加载的原理

    网页资源阻塞浏览器加载的原理
    一个页面允许加载的外部资源有很多,常见的有脚本、样式、字体、图片和视频等,对于这些外部资源究竟是如何影响整个页面的加载和渲
  • 本站所有内容来源于互联网或用户自行发布,本站仅提供信息存储空间服务,不拥有版权,不承担法律责任。如有侵犯您的权益,请您联系站长处理!
  • Copyright © 2017-2022 F11.CN All Rights Reserved. F11站长开发者网 版权所有 | 苏ICP备2022031554号-1 | 51LA统计