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. 菜單權限

一般來說,後臺通過維護userrolemenuuser_rolemenu_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!

推薦閱讀: