加载中...
前端日常-基于React-Router(V6)的权限控制
发表于:2023-05-02 | 分类: 前端

在一个后台管理系统中,安全是很重要的。不光后端需要做权限校验,前端也需要做权限控制。
我们可以大致将权限分为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
24
25
26
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
39
40
41
42

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
50
51
52
53
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. 菜单权限

一般来说,后台通过维护userrolemenuuser_rolemenu_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
34
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
40
41
42
43
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
39
40
41
42
43
44
45
// 无需权限认证的白名单
// 一般是前端的一些报错页
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
16
17
18
19
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
13
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来查看不同的情况。

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

参考

上一篇:
移动端开发H5页面,各种问题介绍
下一篇:
cookie详解
本文目录
本文目录