React-Router(V6)的權限控制實現示例
在一個後臺管理系統中,安全是很重要的。不光後端需要做權限校驗,前端也需要做權限控制。 我們可以大致將權限分為3種: 接口權限、頁面權限、按鈕權限。
在這當中,前端主要關註點則是頁面權限,按鈕權限,而前端做這些的主要目的則是:
- 禁止用戶訪問一些無權限訪問的頁面
- 過濾不必要的請求,減少服務器壓力
下面主要是思路的整理,以及一些核心實現
接口權限
接口權限一般是用戶登錄後,後端根據賬號密碼來認證
和授權
,並頒發token
或者session
等來保存用戶登錄狀態。
後續客戶端請求一般是在header
中攜帶token
,後端通過對token進行鑒權
是否合法來控制是否可以訪問接口。
一般後臺會通過用戶的角色等來做對應的接口權限控制
。
而需要我們前端做的是在請求中攜帶好登錄後回傳的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過期或者錯誤 window.location.replace("/login"); } return response.data.data; }, (err) => { Promise.reject(err.response); } );
頁面權限
首先,我們先完成路由配置
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 />, }, ];
然後是基於路由配置
來生成對應的路由組件
src/routes/root.tsx
const Root = () => { // 創建一個有子節點的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> ); }; // 創建一個沒有子節點的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> );
上面隻是完成瞭基本的配置,下面才是權限相關
路由權限主要分為兩個方向:
1. 菜單權限
一般來說,後臺通過維護user
、role
、menu
、user_role
、menu_role
這幾張表來做相應的權限設計。
所以,在登錄接口中,一般後臺會返回用戶對應的角色
、菜單
等信息。我們通過redux-toolkit
保存登錄數據。大致信息如下(未真正請求接口,隻寫瞭初始數據):
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
表示當前用戶的角色,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;
但是,隻根據權限列表來動態生成菜單並不能完全實現權限相關的目的。用戶還可以通過在地址欄輸入url的方式來訪問沒有在菜單中顯示的頁面。
2. 路由權限
我們可以通過實現一個AuthRoute
來解決上述的問題。
通過AuthRoute
來攔截頁面的訪問操作。
src/routes/AuthRoute.tsx
// 無需權限認證的白名單 // 一般是前端的一些報錯頁 const DONT_NEED_AUTHORIZED_PAGE = ["/unauthorized", "/*"]; const AuthRoute = ({ children, path }: any) => { // 該flag用於控制 受保護頁面的渲染時機,需要等待useEffect中所有的權限驗證條件完成後才表示可以渲染 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") || ""; // 在白名單中的無需驗證,直接跳轉 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("/"); } // 已登錄,根據後臺傳的權限列表做判斷 if (!menuUrls.includes(location.pathname)) { navigate("/unauthorized", { replace: true }); } } // 當上面的權限控制通過後,再渲染受保護的頁面 setRenderFlag(true); }, [token, location.pathname]); if (!canRender) return null; return children; }; export default AuthRoute;
然後,在我們生成Route
的時候在element
屬性中使用AuthRoute
,這一步,我們已經在上面src/routes/root.tsx
這個文件中寫進去瞭。
到這裡,我們就通過實現AuthRoute
來攔截頁面訪問,做權限相關處理。
然後我們可以運行該倉庫 代碼來看效果。
目前沒有實現登錄相關功能,所以需要手動在localStorage
中添加access_token
來模擬登錄。
- 如果沒有登錄(沒有access_token)或者登錄已過期,訪問任何路由都會被路由到
/login
。 - 如果已經登錄,但是再訪問登錄頁面,會被路由到
/
首頁 - 如果已經登錄,但是訪問瞭一個你無訪問的頁面,如
/protected
,則會被路由到/unauthorized
頁面
按鈕權限
按鈕級別的權限,根據當前用戶角色的不同,可以看到的按鈕和操作不同。這裡我隻簡單實現瞭一個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;
使用方法如下,新增瞭一個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
來查看不同的情況。
這種實現方式比較簡單,大夥可以根據自己的具體場景選擇更好的方案
參考
- 認證、授權、鑒權和權限控制
- segmentfault.com/a/1190000020887109
到此這篇關於React-Router(V6)的權限控制實現示例的文章就介紹到這瞭,更多相關React-Router權限控制內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!
推薦閱讀:
- react-router v6實現動態路由實例
- 詳解如何使用Vuex實現Vue後臺管理中的角色鑒權
- React-Router6版本的更新引起的路由用法變化
- vue實現動態路由詳細
- 使用React Router v6 添加身份驗證的方法