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

