React后台管理之权限验证

权限验证分为菜单权限,路由权限,按钮权限,布局模块权限。

菜单权限

以上实现主要是前端层面的设计方案, 我们都知道前端的安全措施永远是不可靠的, 所以我们为了保证系统的安全性, 一般我们会把菜单数据存到后端, 通过接口动态请求权限菜单.。
可以提前和后端做好约定, 让后端根据不同用户返回不同的权限菜单 schema 即可.

递归法

掘金文章介绍
这个参考了 umi 的路由方法,将路由包裹起来
路由包裹掘金介绍
GitHub 源码展示

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
54
55
56
57
58
59
60
61
import { IRoute, routes } from "@/route";
import React, { Suspense, useMemo } from "react";
import { Redirect, Route, Switch, HashRouter } from "react-router-dom";
import styles from "./index.module.less";

export default function App(): React.ReactElement {
const getChildrenComponent = (
route: IRoute,
key: number,
pPath: string = ""
) => {
const path = pPath + route.path;
return route.redirect ? (
<Redirect key={key} to={route.redirect} from={route.path}></Redirect>
) : (
(route.component || route.routes?.length > 0) && (
<Route key={key} path={path} exact={route.exact}>
{route.wrappers?.length > 0
? route.wrappers.reduceRight(
(element: any, wrapper: any) =>
React.createElement(wrapper, {}, element),
React.createElement(
route.component || React.Fragment,
{},
<Switch>
{route?.routes?.map((croute, rindex) =>
getChildrenComponent(croute, rindex, path)
)}
</Switch>
)
)
: React.createElement(
route.component || React.Fragment,
{},
<Switch>
{route?.routes?.map((croute, rindex) =>
getChildrenComponent(croute, rindex, path)
)}
</Switch>
)}
</Route>
)
);
};

const Routes = useMemo(() => {
const _Routes = routes.map((route, rindex) =>
getChildrenComponent(route, rindex)
);
console.log(_Routes, "_Routes");
return _Routes;
}, []);

return (
<Suspense fallback={<div>加载中</div>}>
<HashRouter basename="/">
<Switch>{Routes}</Switch>
</HashRouter>
</Suspense>
);
}

promise 封装法

GitHub 代码展示

Router v6 版本动态路由鉴权

掘金参考文章

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
// router/index.ts
import { lazy } from "react";
import { Navigate } from "react-router-dom";

// React 组件懒加载

// 快速导入工具函数
const lazyLoad = (moduleName: string) => {
const Module = lazy(() => import(`views/${moduleName}`));
return <Module />;
};
// 路由鉴权组件
const Appraisal = ({ children }: any) => {
const token = localStorage.getItem("token");
return token ? children : <Navigate to="/login" />;
};

interface Router {
name?: string;
path: string;
children?: Array<Router>;
element: any;
}

const routes: Array<Router> = [
{
path: "/login",
element: lazyLoad("login"),
},
{
path: "/",
element: <Appraisal>{lazyLoad("sand-box")}</Appraisal>,
children: [
{
path: "",
element: <Navigate to="home" />,
},
{
path: "*",
element: lazyLoad("sand-box/nopermission"),
},
],
},
{
path: "*",
element: lazyLoad("not-found"),
},
];

export default routes;

每次导航列表更新时,再触发路由更新 action
handelFilterRouter 就是根据导航菜单列表 和权限列表 得出路由表的

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
54
55
56
57
58
//redux login/action.ts
import { INITSIDEMENUS, UPDATUSERS, LOGINOUT, UPDATROUTES } from "./contant";
import { getSideMenus } from "services/home";
import { loginUser } from "services/login";
import { patchRights } from "services/right-list";
import { handleSideMenu } from "@/utils/devUtils";
import { handelFilterRouter } from "@/utils/routersFilter";
import { message } from "antd";

// 获取导航菜单列表
export const getSideMenusAction = (): any => {
return (dispatch: any, state: any) => {
getSideMenus().then((res: any) => {
const rights = state().login.users.role.rights;
const newMenus = handleSideMenu(res, rights);
dispatch({ type: INITSIDEMENUS, menus: newMenus });
dispatch(updateRoutesAction()); //import!
});
};
};

// 退出登录
export const loginOutAction = (): any => ({ type: LOGINOUT });

// 更新导航菜单
export const updateMenusAction = (item: any): any => {
return (dispatch: any) => {
patchRights(item).then((res: any) => {
dispatch(getSideMenusAction());
});
};
};

// 路由更新 //import!
export const updateRoutesAction = (): any => {
return (dispatch: any, state: any) => {
const rights = state().login.users.role.rights;
const menus = state().login.menus;
const routes = handelFilterRouter(rights, menus); //import!
dispatch({ type: UPDATROUTES, routes });
};
};

// 登录
export const loginUserAction = (item: any, navigate: any): any => {
return (dispatch: any) => {
loginUser(item).then((res: any) => {
if (res.length === 0) {
message.error("用户名或密码错误");
} else {
localStorage.setItem("token", res[0].username);
dispatch({ type: UPDATUSERS, users: res[0] });
dispatch(getSideMenusAction());
navigate("/home");
}
});
};
};

utils 工具函数处理

说一说我这里为什么要映射 element 成对应组件这步操作,原因是我使用了 redux-persist(redux 持久化),
不熟悉这个插件的可以看看我这篇文章:redux-persist
若是直接转换后存入本地再取出来渲染是会有问题的,所以需要先将 element 保存成映射路径,然后渲染前再进行一次路径映射出对应组件。
每个后台的数据返回格式都不一样,需要自己去转换,我这里的转换仅供参考。
ps:defaulyRoutes 和默认 router/index.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
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
import { lazy } from "react";
import { Navigate } from "react-router-dom";
// 快速导入工具函数
const lazyLoad = (moduleName: string) => {
const Module = lazy(() => import(`views/${moduleName}`));
return <Module />;
};
const Appraisal = ({ children }: any) => {
const token = localStorage.getItem("token");
return token ? children : <Navigate to="/login" />;
};

const defaulyRoutes: any = [
{
path: "/login",
element: lazyLoad("login"),
},
{
path: "/",
element: <Appraisal>{lazyLoad("sand-box")}</Appraisal>,
children: [
{
path: "",
element: <Navigate to="home" />,
},
{
path: "*",
element: lazyLoad("sand-box/nopermission"),
},
],
},
{
path: "*",
element: lazyLoad("not-found"),
},
];

// 权限列表 和 导航菜单 得出路由表 element暂用字符串表示 后面渲染前再映射
export const handelFilterRouter = (
rights: any,
menus: any,
routes: any = []
) => {
for (const menu of menus) {
if (menu.pagepermisson) {
let index = rights.findIndex((item: any) => item === menu.key) + 1;
if (!menu.children) {
if (index) {
const obj = {
path: menu.key,
element: `sand-box${menu.key}`,
};
routes.push(obj);
}
} else {
handelFilterRouter(rights, menu.children, routes);
}
}
}
return routes;
};

// 返回最终路由表
export const handelEnd = (routes: any) => {
defaulyRoutes[1].children = [...routes, ...defaulyRoutes[1].children];
return defaulyRoutes;
};

// 映射element 成对应组件
export const handelFilterElement = (routes: any) => {
return routes.map((route: any) => {
route.element = lazyLoad(route.element);
return route;
});
};

主文件

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
import routes from "./router";
import { useRoutes } from "react-router-dom";
import { shallowEqual, useSelector } from "react-redux";
import { useState, useEffect } from "react";
import { handelFilterElement, handelEnd } from "@/utils/routersFilter";
import { deepCopy } from "@/utils/devUtils";

function App() {
console.log("first");
const [rout, setrout] = useState(routes);
const { routs } = useSelector(
(state: any) => ({ routs: state.login.routes }),
shallowEqual
);

const element = useRoutes(rout);
// 监听路由表改变重新渲染
useEffect(() => {
// deepCopy 深拷贝state数据 不能影响到store里的数据!
// handelFilterElement 映射对应组件
// handelEnd 将路由表嵌入默认路由表得到完整路由表
const end = handelEnd(handelFilterElement(deepCopy(routs)));
setrout(end);
}, [routs]);

return <div className="height-all">{element}</div>;
}

export default App;