React實現(xiàn)路由鑒權(quán)的實例詳解
前言
React應(yīng)用中的路由鑒權(quán)是確保用戶僅能訪問其授權(quán)頁面的方式,用于已登錄或具有訪問特定頁面所需的權(quán)限,由于React沒有實現(xiàn)類似于Vue的路由守衛(wèi)功能,所以只能由開發(fā)者自行實現(xiàn)。前端中的路由鑒權(quán)可以區(qū)分以下顆粒度:菜單權(quán)限控制、組件權(quán)限控制、路由權(quán)限控制。在后臺管理系統(tǒng)中三者是必不可少的。這篇文章就來記錄下React實現(xiàn)路由鑒權(quán)的流程。
確定權(quán)限樹
開始之前我們需要對權(quán)限樹的數(shù)據(jù)結(jié)構(gòu)進(jìn)行確定,一般來說我們需要拿到后端傳回的權(quán)限樹進(jìn)行存儲,在React中通常將權(quán)限樹存儲在Redux中,并且我們前端也需要自行維護(hù)一顆屬于前端的權(quán)限樹,這里需要兩個權(quán)限樹進(jìn)行對比從而實現(xiàn)路由鑒權(quán)。權(quán)限樹結(jié)構(gòu)如下:
export type LocalMenuType = { key: string; level: number; // 層級 menucode: string; // 權(quán)限碼 用于查詢是否有此權(quán)限 label?: React.ReactNode; // 菜單名稱 path?: string | string[]; // 路由 parentmenuid?: string; // 父級菜單id children?: LocalMenuType[]; // 子菜單 };
權(quán)限樹處理
在確定權(quán)限樹后,我們需要對權(quán)限樹結(jié)構(gòu)進(jìn)行打平。為此我們可以寫個utils來處理權(quán)限樹。我們使用遞歸來進(jìn)行打平權(quán)限樹:
export const getFlattenList = ( authList: LocalMenuType[], // 需要打平的權(quán)限樹 flattenAuthList: LocalMenuType[], // 存儲打平后的權(quán)限樹 level?: number // 打平層級 ) => { authList.forEach((item) => { // 如果查找層級超出則返回 if (level && item.level > level) return; flattenAuthList.push(Object.assign({}, item, { children: [] })); if (item.children && item.children.length > 0) { getFlattenList(item.children, flattenAuthList, level); } }); };
通過以上代碼我們將一個樹打平,打平后結(jié)構(gòu)如下:
菜單權(quán)限控制
在后臺系統(tǒng)中,每個角色有不同的菜單權(quán)限,我們需要根據(jù)后端返回的角色數(shù)據(jù)進(jìn)行菜單的顯示與隱藏,為了方便起見這里直接使用mock數(shù)據(jù)并將數(shù)據(jù)存儲在localStorage中:
export enum RoleType { MANAGER, ONLY_ORDER, ONLY_VIEW_LIST, } export const setCurrentRole = (type: RoleType) => { switch (type) { case RoleType.MANAGER: { window.localStorage.setItem("menu", JSON.stringify(menus)); break; } case RoleType.ONLY_ORDER: { window.localStorage.setItem("menu", JSON.stringify(onlyOrder)); break; } case RoleType.ONLY_VIEW_LIST: { window.localStorage.setItem("menu", JSON.stringify(onlyViewList)); break; } } }; export const getCurrentRole = () => JSON.parse(window.localStorage.getItem("menu")) as LocalMenuType[];
通過localStorage得到的角色信息進(jìn)行顯示菜單,這里我們只顯示1,2級菜單,所以需要對權(quán)限菜單進(jìn)行處理,只保留1,2的菜單數(shù)據(jù),也是需要用到遞歸來處理:
// 處理權(quán)限菜單 const handleAuthMenu = ( menuList: LocalMenuType[],// 全部菜單權(quán)限樹 authCodes: string[], // 角色所有權(quán)限 authMenuList: LocalMenuType[], // 角色最終擁有菜單列表 level?: number // 處理層級 ) => { menuList.forEach((menu) => { // 如果level 存在,則只處理小于level的情況 if (level && menu.level > level) return; // 如果有權(quán)限,則繼續(xù)遞歸遍歷菜單 if (authCodes.includes(menu.menucode)) { let newAuthMenu: LocalMenuType = { ...menu, children: undefined }; let newAuthMenuChildren: LocalMenuType[] = []; if (menu.children && menu.children.length > 0) { handleAuthMenu(menu.children, authCodes, newAuthMenuChildren, level); } // 添加子菜單 if (newAuthMenuChildren.length > 0) { newAuthMenu.children = newAuthMenuChildren; } authMenuList.push(newAuthMenu); } }); }; // 獲取角色權(quán)限菜單 export const getAuthMenu = (flattenAuth: LocalMenuType[], level?: number) => { // 獲取權(quán)限菜單的menucode const authCodes: string[] = flattenAuth.map((auth) => auth.menucode); let authMenu: LocalMenuType[] = []; handleAuthMenu(menus, authCodes, authMenu, level); return authMenu; };
在獲取完角色1,2級菜單后,我們需要對左側(cè)菜單欄進(jìn)行初始化,默認(rèn)為菜單列表中第一個path。獲取二級菜單的首位菜單路由之后通過useNavigate進(jìn)行跳轉(zhuǎn)。獲取菜單路由通過getRoutePath進(jìn)行獲取,由于一個頁面可能包含多個路由,所以需要對path信息進(jìn)行判斷:
// anthRoles.ts // 獲取菜單路由 export const getRoutePath = (localMenu: LocalMenuType) => { return localMenu.path ? typeof localMenu.path === "object" ? localMenu.path[0] : localMenu.path : null; }; // home.tsx //2級菜單list const secondAuthMenuList = useMemo(() => { return flattenList.filter((res) => res.level === 2); }, [flattenList]); // 初始化菜單 useEffect(() => { const initMenuItem = secondAuthMenuList[0]; if (initMenuItem) { const initRoute = initMenuItem.level > 2 ? pathname : getRoutePath(initMenuItem)!; navigate(initRoute, { replace: true }); } }, [secondAuthMenuList, flattenList]);
那如何根據(jù)點擊的菜單進(jìn)行跳轉(zhuǎn)呢?也是需要獲取對應(yīng)key值的二級菜單path進(jìn)行跳轉(zhuǎn):
const findSecondMenuByKey = (key: string) => secondAuthMenuList.find((item) => item.key === key); // 點擊菜單進(jìn)行跳轉(zhuǎn) const handleMenuChange = ({ key }: { key: string }) => { setMenuSelectKeys([key]); let chooseItem = findSecondMenuByKey(key); if (chooseItem?.path) navigate(getRoutePath(chooseItem) || ""); };
最終實現(xiàn)效果如下:
組件權(quán)限控制
組件權(quán)限控制相對簡單,需要通過menucode也就是權(quán)限碼進(jìn)行組件的顯示與隱藏,這里以按鈕組件為例子,我們需要一個高階組件AuthBuutonHOC作為按鈕組件的父組件進(jìn)行顯示,同時,我們通過hasAuth函數(shù)判斷是否有當(dāng)前指定權(quán)限:
// 是否有當(dāng)前權(quán)限 export const hasAuth = (meunCode: string) => { // 當(dāng)前打平的角色權(quán)限樹 let flattenAuthList: LocalMenuType[] = getCurrentFlattenRole(); return !!flattenAuthList.find((auth) => auth.menucode === meunCode); }; AuthBuutonHOC.tsx const AuthButton: React.FC<Props> = ({ menuCode, children }) => { // 沒有當(dāng)前權(quán)限則不顯示 if (!hasAuth(menuCode)) return null; return <>{children}</>; }; export default React.memo(AuthButton);
使用AuthButton包裹按鈕,就能實現(xiàn)組件級別的權(quán)限控制:
- 有當(dāng)前權(quán)限:
- 無當(dāng)前權(quán)限:
路由權(quán)限控制
路由權(quán)限控制需要對用戶輸入的路徑名進(jìn)行校驗,我們通過useLocation獲取到當(dāng)前用戶輸入的pathname;并路徑匹配matchPath判斷當(dāng)前路徑與權(quán)限菜單路徑是否對應(yīng),若對應(yīng)上則表示當(dāng)前角色擁有權(quán)限,若對應(yīng)不上則跳轉(zhuǎn)到404頁面。
通過以上邏輯,我們先創(chuàng)建一個hasAuthByRoutePath函數(shù)來判斷是否有當(dāng)前的路由權(quán)限:
// 是否有當(dāng)前的路由權(quán)限 export const hasAuthByRoutePath = (path: string) => { let flattenAuthList: LocalMenuType[] = getCurrentFlattenRole(); return !!flattenAuthList.find((auth) => routePathMatch(path, auth.path || "") ); }; // 判斷路由是否一致 export const routePathMatch = (path: string, menuPath: string | string[]) => { if (typeof menuPath === "object") { return menuPath.some((item) => matchPath(item, path)); } return !!matchPath(menuPath, path); };
在home頁面監(jiān)聽用戶輸入的路徑名進(jìn)行判斷,有則跳轉(zhuǎn)到當(dāng)前菜單:
useEffect(() => { // 獲取當(dāng)前匹配到的菜單 const matchMenuItem = flattenList.find((item) => routePathMatch(pathname, item.path || "") ); if (matchMenuItem) { // 如果當(dāng)前菜單level為3級或者更小則設(shè)置其父id,否則設(shè)置其id matchMenuItem.level > 2 ? setMenuSelectKeys([matchMenuItem.parentmenuid!]) : setMenuSelectKeys([matchMenuItem.key]); // 如果當(dāng)前菜單level為3級或者更小則通過父id找到2級菜單 const newSecondMenu = matchMenuItem.level > 2 ? findSecondMenuByKey(matchMenuItem.parentmenuid!) : matchMenuItem; // 有對應(yīng)的二級菜單則定位到當(dāng)前側(cè)邊菜單欄位置 if (newSecondMenu) { setMenuOpenKeys((preOpenKeys) => { const openKeysSet = new Set(preOpenKeys || []); openKeysSet.add(newSecondMenu.parentmenuid!); return Array.from(openKeysSet); }); } else { setMenuSelectKeys([]); } } }, [secondAuthMenuList, flattenList, pathname]);
最后,如果沒有當(dāng)前路徑權(quán)限則跳轉(zhuǎn)到404頁面,為此我們需要一個authLayout高階組件來包裹HomePage來實現(xiàn)跳轉(zhuǎn):
// 白名單 const routerWhiteList = ["/home"]; const AuthLayout = ({ children }: { children: JSX.Element }) => { const { pathname } = useLocation(); // 判斷當(dāng)前路由是否在白名單內(nèi)或者有當(dāng)前權(quán)限路由 const hasAuthRoute = useMemo(() => { return ( routePathMatch(pathname, routerWhiteList) || hasAuthByRoutePath(pathname) ); }, [pathname]); // 沒有權(quán)限則跳轉(zhuǎn)至404頁面 if (!hasAuthRoute) return <Navigate to="/404" replace />; return children; }; export default AuthLayout;
將以上組件包裹在HomePage外層即可實現(xiàn)路由權(quán)限控制:
{ path: "/home", element: ( <AuthLayout> <HomePage /> </AuthLayout> ), }
具體實現(xiàn)效果如下:
總結(jié)
到此這篇關(guān)于React實現(xiàn)路由鑒權(quán)的實例詳解的文章就介紹到這了,更多相關(guān)React路由鑒權(quán)內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
React-Native之TextInput組件的設(shè)置以及如何獲取輸入框的內(nèi)容
這篇文章主要介紹了React-Native之TextInput組件的設(shè)置以及如何獲取輸入框的內(nèi)容問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2023-05-05React使用hook如何實現(xiàn)數(shù)據(jù)雙向綁定
這篇文章主要介紹了React使用hook如何實現(xiàn)數(shù)據(jù)雙向綁定方式,具有很好的參考價值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-03-03