React-Router(V6)的權(quán)限控制實(shí)現(xiàn)示例
在一個(gè)后臺(tái)管理系統(tǒng)中,安全是很重要的。不光后端需要做權(quán)限校驗(yàn),前端也需要做權(quán)限控制。 我們可以大致將權(quán)限分為3種: 接口權(quán)限、頁(yè)面權(quán)限、按鈕權(quán)限。
在這當(dāng)中,前端主要關(guān)注點(diǎn)則是頁(yè)面權(quán)限,按鈕權(quán)限,而前端做這些的主要目的則是:
- 禁止用戶訪問(wèn)一些無(wú)權(quán)限訪問(wèn)的頁(yè)面
- 過(guò)濾不必要的請(qǐng)求,減少服務(wù)器壓力
下面主要是思路的整理,以及一些核心實(shí)現(xiàn)
接口權(quán)限
接口權(quán)限一般是用戶登錄后,后端根據(jù)賬號(hào)密碼來(lái)認(rèn)證和授權(quán),并頒發(fā)token或者session等來(lái)保存用戶登錄狀態(tài)。
后續(xù)客戶端請(qǐng)求一般是在header中攜帶token,后端通過(guò)對(duì)token進(jìn)行鑒權(quán)是否合法來(lái)控制是否可以訪問(wèn)接口。
一般后臺(tái)會(huì)通過(guò)用戶的角色等來(lái)做對(duì)應(yīng)的接口權(quán)限控制。
而需要我們前端做的是在請(qǐng)求中攜帶好登錄后回傳的token,我們以axios為例
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過(guò)期或者錯(cuò)誤
window.location.replace("/login");
}
return response.data.data;
},
(err) => {
Promise.reject(err.response);
}
);頁(yè)面權(quán)限
首先,我們先完成路由配置
src/routes/routes.tsx
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 />,
},
];然后是基于路由配置來(lái)生成對(duì)應(yīng)的路由組件
src/routes/root.tsx
const Root = () => {
// 創(chuàng)建一個(gè)有子節(jié)點(diǎn)的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>
);
};
// 創(chuàng)建一個(gè)沒(méi)有子節(jié)點(diǎn)的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組件即可
ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
<Provider store={store}>
<Root />
</Provider>
);上面只是完成了基本的配置,下面才是權(quán)限相關(guān)
路由權(quán)限主要分為兩個(gè)方向:
1. 菜單權(quán)限
一般來(lái)說(shuō),后臺(tái)通過(guò)維護(hù)user、role、menu、user_role、menu_role這幾張表來(lái)做相應(yīng)的權(quán)限設(shè)計(jì)。
所以,在登錄接口中,一般后臺(tái)會(huì)返回用戶對(duì)應(yīng)的角色、菜單等信息。我們通過(guò)redux-toolkit保存登錄數(shù)據(jù)。大致信息如下(未真正請(qǐng)求接口,只寫了初始數(shù)據(jù)):
src/pages/login/Login.slice.ts
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: "首頁(yè)",
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表示當(dāng)前用戶的角色,menuLists為用戶可訪問(wèn)的菜單
然后在首頁(yè)中生成菜單列表
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;但是,只根據(jù)權(quán)限列表來(lái)動(dòng)態(tài)生成菜單并不能完全實(shí)現(xiàn)權(quán)限相關(guān)的目的。用戶還可以通過(guò)在地址欄輸入url的方式來(lái)訪問(wèn)沒(méi)有在菜單中顯示的頁(yè)面。
2. 路由權(quán)限
我們可以通過(guò)實(shí)現(xiàn)一個(gè)AuthRoute來(lái)解決上述的問(wèn)題。
通過(guò)AuthRoute來(lái)攔截頁(yè)面的訪問(wèn)操作。
src/routes/AuthRoute.tsx
// 無(wú)需權(quán)限認(rèn)證的白名單
// 一般是前端的一些報(bào)錯(cuò)頁(yè)
const DONT_NEED_AUTHORIZED_PAGE = ["/unauthorized", "/*"];
const AuthRoute = ({ children, path }: any) => {
// 該flag用于控制 受保護(hù)頁(yè)面的渲染時(shí)機(jī),需要等待useEffect中所有的權(quán)限驗(yàn)證條件完成后才表示可以渲染
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") || "";
// 在白名單中的無(wú)需驗(yàn)證,直接跳轉(zhuǎn)
if (DONT_NEED_AUTHORIZED_PAGE.includes(path)) {
return children;
}
useEffect(() => {
// 用戶未登錄
if (token === "") {
message.error("token 過(guò)期,請(qǐng)重新登錄!");
navigate("/login");
}
// 已登錄
if (token) {
// 已登錄需要通過(guò)logout來(lái)控制退出登錄或者是token過(guò)期返回登錄界面
if (location.pathname == "/login") {
navigate("/");
}
// 已登錄,根據(jù)后臺(tái)傳的權(quán)限列表做判斷
if (!menuUrls.includes(location.pathname)) {
navigate("/unauthorized", { replace: true });
}
}
// 當(dāng)上面的權(quán)限控制通過(guò)后,再渲染受保護(hù)的頁(yè)面
setRenderFlag(true);
}, [token, location.pathname]);
if (!canRender) return null;
return children;
};
export default AuthRoute;然后,在我們生成Route的時(shí)候在element屬性中使用AuthRoute,這一步,我們已經(jīng)在上面src/routes/root.tsx這個(gè)文件中寫進(jìn)去了。
到這里,我們就通過(guò)實(shí)現(xiàn)AuthRoute來(lái)攔截頁(yè)面訪問(wèn),做權(quán)限相關(guān)處理。
然后我們可以運(yùn)行該倉(cāng)庫(kù) 代碼來(lái)看效果。
目前沒(méi)有實(shí)現(xiàn)登錄相關(guān)功能,所以需要手動(dòng)在localStorage中添加access_token來(lái)模擬登錄。
- 如果沒(méi)有登錄(沒(méi)有access_token)或者登錄已過(guò)期,訪問(wèn)任何路由都會(huì)被路由到
/login。 - 如果已經(jīng)登錄,但是再訪問(wèn)登錄頁(yè)面,會(huì)被路由到
/首頁(yè) - 如果已經(jīng)登錄,但是訪問(wèn)了一個(gè)你無(wú)訪問(wèn)的頁(yè)面,如
/protected,則會(huì)被路由到/unauthorized頁(yè)面
按鈕權(quán)限
按鈕級(jí)別的權(quán)限,根據(jù)當(dāng)前用戶角色的不同,可以看到的按鈕和操作不同。這里我只簡(jiǎn)單實(shí)現(xiàn)了一個(gè)AuthButton
src/coponents/auth-button/index.tsx
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;使用方法如下,新增了一個(gè)roles屬性,表示哪些角色可以看見(jiàn)該按鈕
src/pages/foo/auth-button.tsx
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;我們可以手動(dòng)的修改Login.slice.ts中的role來(lái)查看不同的情況。
這種實(shí)現(xiàn)方式比較簡(jiǎn)單,大伙可以根據(jù)自己的具體場(chǎng)景選擇更好的方案
參考
到此這篇關(guān)于React-Router(V6)的權(quán)限控制實(shí)現(xiàn)示例的文章就介紹到這了,更多相關(guān)React-Router權(quán)限控制內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React-RouterV6+AntdV4實(shí)現(xiàn)Menu菜單路由跳轉(zhuǎn)的方法
這篇文章主要介紹了React-RouterV6+AntdV4實(shí)現(xiàn)Menu菜單路由跳轉(zhuǎn),主要有兩種跳轉(zhuǎn)方式一種是編程式跳轉(zhuǎn)另一種是NavLink鏈接式跳轉(zhuǎn),每種方式通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),需要的朋友可以參考下2022-08-08
React實(shí)現(xiàn)動(dòng)效彈窗組件
最近在使用react開(kāi)發(fā)項(xiàng)目,遇到這樣一個(gè)需求實(shí)現(xiàn)一個(gè)帶有動(dòng)效的 React 彈窗組件,如果不考慮動(dòng)效,很容易實(shí)現(xiàn),接下來(lái)小編通過(guò)本文給大家介紹React實(shí)現(xiàn)動(dòng)效彈窗組件的實(shí)現(xiàn)代碼,一起看看吧2021-06-06
再次談?wù)揜eact.js實(shí)現(xiàn)原生js拖拽效果引起的一系列問(wèn)題
React 起源于 Facebook 的內(nèi)部項(xiàng)目,因?yàn)樵摴緦?duì)市場(chǎng)上所有 JavaScript MVC 框架,都不滿意,就決定自己寫一套,用來(lái)架設(shè) Instagram 的網(wǎng)站.本文給大家介紹React.js實(shí)現(xiàn)原生js拖拽效果,需要的朋友一起學(xué)習(xí)吧2016-04-04
React使用UI(Ant?Design)框架的詳細(xì)過(guò)程
Ant?Design主要用于中后臺(tái)系統(tǒng)的使用,它提供了豐富的組件和工具,可以幫助開(kāi)發(fā)人員快速構(gòu)建出美觀、易用的界面,同時(shí),Ant?Design還提供了詳細(xì)的文檔和示例,方便開(kāi)發(fā)者學(xué)習(xí)和使用,這篇文章主要介紹了React使用UI(Ant?Design)框架,需要的朋友可以參考下2023-12-12
用React實(shí)現(xiàn)一個(gè)完整的TodoList的示例代碼
本篇文章主要介紹了用React實(shí)現(xiàn)一個(gè)完整的TodoList的示例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10

