這幾天陸續看了一些在使用 Router 時,很常會遇到的一些使用情境,今天接著另一個在專案內也很常會需要處理的情境,那就是「頁面的權限控制」。
當我們在實作一個完整的產品時,很常都會加入權限控制的內容,只要有使用者的權限差異,那就代表頁面使用上一定也會有權限差異,也就是說在設定 Router 的時候,就必須去控制哪些角色可以進入哪些頁面和不能進入哪些頁面。
實際舉例來說:
今天就讓我們從 React 到 Next.js 來看看這部分要怎麼結合 Router 進行實作。
在 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 頁面。
剛剛看了 React 的實作方式後,我們也來看看在 Next.js 中要怎麼透過 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 頁面。
但是這兩個做法都存在著一個缺點,那就是需要把判斷權限的 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 端確認權限後,即使初次進入頁面,也不會出現畫面閃一下的問題。
那在 App Router 上該怎麼做?前幾天我們已經知道 App Router 和 Page Router 相比,能支援更直覺且便利的巢狀套用 layout 的功能,所以這裡我們一樣也直接用 layout
來實作頁面權限控管的部分。
在這裡我們可以使用 App Router 支援的路由分組功能,把路由分成需要權限的群組和不需要權限的群組(也就是利用()來命名資料夾名稱,例如:(private)
和(public)
),再另外把這個群組要套用的 layout 檔案給加上這個資料夾底下。
在 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;
雖然在 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。