iT邦幫忙

2025 iThome 鐵人賽

DAY 20
0
Modern Web

從 React 學 Next.js:不只要會用,還要真的懂系列 第 20

【Day 20】React vs Next.js 實作頁面導向的權限控制

  • 分享至 

  • xImage
  •  

這幾天陸續看了一些在使用 Router 時,很常會遇到的一些使用情境,今天接著另一個在專案內也很常會需要處理的情境,那就是「頁面的權限控制」。

為什麼需要頁面的權限控制?

當我們在實作一個完整的產品時,很常都會加入權限控制的內容,只要有使用者的權限差異,那就代表頁面使用上一定也會有權限差異,也就是說在設定 Router 的時候,就必須去控制哪些角色可以進入哪些頁面和不能進入哪些頁面。

實際舉例來說:

  • 一個購票系統,會區分為登入前和登入後可以進去的頁面
  • 一個後台系統,也有可能會依照角色權限的不同區分可以功能頁面有哪些

今天就讓我們從 React 到 Next.js 來看看這部分要怎麼結合 Router 進行實作。

在 React 中實作頁面權限控制

在 React 中,可以利用設定 layout 再加上 Outlet 的使用來實作頁面的權限控制。

這邊舉一個實際的例子來看看可以怎麼做。
假設頁面有分為登入前可以進入的頁面和登入後才可以進的頁面,我們可以先建立一個給登入後使用的 layout 檔案,並且使用在要登入才能進的路由上。

搭配 Outlet 的使用,建立一個給已登入頁面用的 layout

import { Outlet, Navigate } from "react-router-dom";

const PrivateRouterLayout = () => {
  const localStorageToken = localStorage.getItem("token");

  // 有token 就進入對應的子路由,沒有就導回登入頁
  return localStorageToken ? <Outlet /> : <Navigate to="/login" />;
};

export default PrivateRouterLayout;

設定好 PrivateRouterLayout 這個用來當作 layout 使用的檔案後,就把它放到 config 檔案裡面使用。

const Router = createHashRouter([
  {
    path: "/",
    element: <Layout />, // 統一 layout,可包含 Header/Footer 等
    children: [
      {
        index: true,
        element: <HomePage />,
      },
      {
        path: "login",
        element: <Login />,
      },
      {
        element: <PrivateRouterLayout />, // 這層會做 token 驗證
        children: [
          {
            path: "about",
            element: <AboutPage />,
          },
        ],
      },
    ],
  },
  {
    path: "*",
    element: <NotFound />,
  },
]);

export default Router;

這樣設定後,就可以在當作 layout 控制的 PrivateRouter 檔案中做一些和權限相關的邏輯操作,在進入頁面前,選擇要放行進入,還是擋住並且導轉。

實作結果就會像是下面這個樣子,因為沒有登入,所以即使連結是 /about,也會被導去登入頁,而不是進到 about 頁面。
https://i.imgur.com/Qcn2wWk.gif

在 Next.js 中實作頁面權限控制

剛剛看了 React 的實作方式後,我們也來看看在 Next.js 中要怎麼透過 Router 控制這個部分。實作情境是區分登入前、後能不能進的頁面。

使用 Page Router 的實作方式

在 Page Router 的部分,我們分成「Client 端」和「Server 端」這兩個部分來看權限控制的設定。

∙ Client 端
先來看 Client 端的部分,在 Page Router 中,如果想要在 Client 端進行權限確認和控管,那就和 React 一樣可以透過 layout 來統一做權限相關的處理,實作方法就和前幾天提到的 layout 主題內實作的方式一樣,先準備一個 layout,內容如下:

import { useEffect } from "react";
import { useRouter } from "next/router";
const PrivateRouterLayout = ({ children }: { children: React.ReactNode }) => {
  const router = useRouter();

  useEffect(() => {
    const token = localStorage.getItem("token");
    if (!token) {
      router.replace("/login");
    }
  }, [router]);

  return <>{children}</>;
};

export default PrivateRouterLayout;

接著可以透過 HOC (High Order Component) 的方式來用於會需要額外透過權限控制的頁面元件中,如下這樣寫。

import PrivateRouterLayout from "@/components/PrivateRouterLayout";

const AboutPage = () => {
  return (
    <PrivateRouterLayout>
      <div>
        <h1>About Page</h1>
      </div>
    </PrivateRouterLayout>
  );
};

export default AboutPage;

如果想用 layout 的方式帶入定的頁面中,也可以改成使用 getLayout 將 layout 套用上去,如下:

import PrivateRouterLayout from "@/components/PrivateRouterLayout";

const AboutPage = () => {
  return (
    <div>
      <h1>About Page</h1>
    </div>
  );
};

AboutPage.getLayout = function getLayout(page: React.ReactNode) {
  return <PrivateRouterLayout>{page}</PrivateRouterLayout>;
};

export default AboutPage;

不論是 HOC 還是把 layout 套用到指定的頁面上都可以實作出以下這個效果,當沒有 token 時,會被導到 login 頁面。
https://i.imgur.com/zS5SIwd.gif

但是這兩個做法都存在著一個缺點,那就是需要把判斷權限的 layout 各別加在需要判斷是否有登入才能進入的頁面中,這樣的做法也就帶來了一些不便利性。接下來再來看一個會更好的做法,也就是在 _app 檔案中,直接套用全域的 Router 上。

在下面的這個寫法中,會需要先用 pathname 判斷是不是不需要權限頁面,在依照是否需要檢查權限,套用 PrivateRouterLayout。

export default function App({ Component, pageProps }: AppProps) {
  const getLayout = (Component as any).getLayout || ((page: any) => page);

  const publicRoutes = ["/login", "/"];
  const { pathname } = useRouter();
  const isPublicRoute = publicRoutes.includes(pathname);

  const content = !isPublicRoute ? (
    <PrivateRouterLayout>
      <Component {...pageProps} />
    </PrivateRouterLayout>
  ) : (
    <Component {...pageProps} />
  );

  return <MainLayout>{getLayout(content)}</MainLayout>;
}

不論是上述這兩種方法的哪一個用法,在轉跳的過程中,都會有一瞬間進入到 about 頁面,這部分主要是因為當我們透過 CSR 渲染來處理時,進到 about 路由對應到的頁面,會先渲染完整的頁面,才會執行 useEffect 的緣故,所以如果是第一次進入這個頁面,就會出現閃一下的狀況。

以上都是在 Client 端處理權限控制的部分,接下來也來看看如果要在 Server 上就進行權限的控制的話,該怎麼處理。

∙ Server 端
要在 Page Router 中使用 SSR,會需要設定 getServerSideProps,如果要在 Server 端做權限的檢查,當然也就會需要在這個 getServerSideProps 函式中去做判斷處理。但是這裡需要強調的是想要在 Page Router 中的 SSR 模式下去做權限檢查,沒有辦法像前面看到的 Client 端的處理方式一樣統一在 _app 檔案中去做統一的處理,為了讓相關邏輯比較好管理,就會需要把共用的權限判斷邏輯拆成一個獨立的函式,例如以下這樣。

export const withPrivateRouter =
  (getServerSidePropsFunc) => async (context) => {
    const token = context.req.cookies.token;

    if (!token) {
      return {
        redirect: {
          destination: "/login",
          permanent: false,
        },
      };
    }

    if (getServerSidePropsFunc) {
      return await getServerSidePropsFunc(context);
    }

    return { props: {} };
  };

接著在要使用這個的頁面中個別加上以下這個部分,例如在 src/pages/about/index.tsx 檔案中加上這個部分。

export const getServerSideProps = withPrivateRouter();

加上這樣部分後,就能讓頁面可以在 Server 端就先進行權限的判斷。而且改成 在 Server 端確認權限後,即使初次進入頁面,也不會出現畫面閃一下的問題。
https://i.imgur.com/63gYW6H.gif

使用 App Router 的實作方式

那在 App Router 上該怎麼做?前幾天我們已經知道 App Router 和 Page Router 相比,能支援更直覺且便利的巢狀套用 layout 的功能,所以這裡我們一樣也直接用 layout 來實作頁面權限控管的部分。

在這裡我們可以使用 App Router 支援的路由分組功能,把路由分成需要權限的群組和不需要權限的群組(也就是利用()來命名資料夾名稱,例如:(private)(public)),再另外把這個群組要套用的 layout 檔案給加上這個資料夾底下。
https://ithelp.ithome.com.tw/upload/images/20250916/20130914ZQCNhjlg7Y.png

在 App Router 透過 layout 處理頁面權限的方式一樣可以分成 「Client 端」和 「Server 端」兩種。

∙ Client 端
在 App Router 中,如果想要在 client 端才確認是否能進入頁面的權限,和使用 Page Router 一樣,需要把判斷權限和導頁的部分寫在 useEffect 裡面。也因為是在 useEffect 才做相關的判斷,所以畫面也會有閃一下問題會出現,而且拿來判斷權限的 token 需要存在 client 端上。

"use client" // 記得要加上 "use client" 才能使用 hooks
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";

const PrivateRouterLayout = ({ children }: { children: React.ReactNode }) => {
  const router = useRouter();
  const [checked, setChecked] = useState(false);

  useEffect(() => {
    const token = localStorage.getItem("token");
    if (!token) {
      router.replace("/login");
    } else {
      setChecked(true);
    }
  }, [router]);

  if (!checked) {
    return <div>檢查權限中...</div>;
  }

  return <>{children}</>;
};

export default PrivateRouterLayout;

∙ Server 端
如果想要在 App Router 中,於 Server 端就透過 layout 來處理頁面相關的權限判斷,就必須把 use client 和 useEffect 的部分拿掉,讓 layout 變成是只會在 server 上執行的 RSC。

// src/app/(private)/layout.tsx
import { cookies } from "next/headers";
import { redirect } from "next/navigation";

const PrivateLayout = async ({ children }: { children: React.ReactNode }) => {
  const cookiesData = await cookies();
  const token = cookiesData.get("token")?.value;

  if (!token) {
    redirect("/login");
  }

  return <>{children}</>;
};

export default PrivateLayout;

https://i.imgur.com/JuRqlht.gif

雖然在 App Router 中,也可以在 client 端做頁面的權限控管,但是實務上還是建議在 App Router 中於 Server 端上進行頁面的權限控管,才不會有畫面閃一下的問題。

總結

頁面的權限控制幾乎是每個專案都會遇到的需求,無論是登入前後的頁面區分,或是依照使用者角色限制功能,都必須透過 Router 層來做攔截與管控。在 React 中只能在 client 端做檢查,在 Next.js 中,則可以分為 Client 端檢查與 Server 端檢查。
- Client 端檢查: 在 Next.js 中,若要從 client 端檢查權限,實作方式和 React 相同,但缺點是會出現「畫面閃一下」的體驗落差,且安全性較低。
- Server 端檢查: 在 Next.js 中,可以於 Page Router 透過 getServerSideProps 或於 App Router 透過 layout.tsx 在 server 上進行頁面的權限判斷,用這個方式處理權限判斷,就能避免畫面閃爍並帶來更好的安全性。

整體來說,若只是簡單的前端 UI 控管,可以用 Client 端檢查,但若要避免畫面閃爍,以及讓使用者能在進入頁面前就進行權限判斷,就比較建議透過 Server 端檢查。

目前看到 Page Router 和 App Router 透過路由來進行權限控制的方式,都會是在使用者發送請求後,才進行判斷,不論是 SSR,或是 CSR 的判斷方式。如果想要在使用者發送請求前,就先進行權限的判斷,其實還可以透過 middleware 進行處理。

明天我們就先暫時離開一下 Router 的世界,先往旁邊延伸看一下一樣也可以用來處理權限控制的 middleware。


上一篇
【Day 19】React vs Next.js 設定客製化錯誤頁面
下一篇
【Day 21】Middleware 是什麼?伺服器層級的權限控制
系列文
從 React 學 Next.js:不只要會用,還要真的懂21
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言