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ú)權(quá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)控制是否可以訪問接口。
一般后臺(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è)沒有子節(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
為用戶可訪問的菜單
然后在首頁(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)訪問沒有在菜單中顯示的頁(yè)面。
2. 路由權(quán)限
我們可以通過(guò)實(shí)現(xiàn)一個(gè)AuthRoute
來(lái)解決上述的問題。
通過(guò)AuthRoute
來(lái)攔截頁(yè)面的訪問操作。
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è)面訪問,做權(quán)限相關(guān)處理。
然后我們可以運(yùn)行該倉(cāng)庫(kù) 代碼來(lái)看效果。
目前沒有實(shí)現(xiàn)登錄相關(guān)功能,所以需要手動(dòng)在localStorage
中添加access_token
來(lái)模擬登錄。
- 如果沒有登錄(沒有access_token)或者登錄已過(guò)期,訪問任何路由都會(huì)被路由到
/login
。 - 如果已經(jīng)登錄,但是再訪問登錄頁(yè)面,會(huì)被路由到
/
首頁(yè) - 如果已經(jīng)登錄,但是訪問了一個(gè)你無(wú)訪問的頁(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
屬性,表示哪些角色可以看見該按鈕
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-08React實(shí)現(xiàn)動(dòng)效彈窗組件
最近在使用react開發(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拖拽效果引起的一系列問題
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-04React使用UI(Ant?Design)框架的詳細(xì)過(guò)程
Ant?Design主要用于中后臺(tái)系統(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è)參考。一起跟隨小編過(guò)來(lái)看看吧2017-10-10