FE Day 22 認證與 session 管理
執行需求:CPU 可跑。今天是「前端開發實戰:React 與 Next.js 全套」系列的第二十二篇。前二十天你學會了元件、狀態、Hook、App Router、Server Action、表單,今天進入「認證」——也就是「誰在使用這個系統」、「他有什麼權限」、「session 怎麼儲存」。這個題目對後端工程師並不陌生(cookie、JWT、httpOnly、CSRF 早就滾瓜爛熟),但 Next.js 15 App Router 的 Server Component 環境帶來一些新工具:cookies() 在 Server Component 內讀 cookie、middleware.ts 在路由層做權限檢查、Server Action 內可以做 session 驗證。我們會在 booking-fe 上實作完整的「帳號密碼登入 → cookie session → 個人化預約清單 → 管理者後台」流程,並說明每一層的取捨與安全考量。
引言
認證是「前後端分離」架構下最容易被搞錯的一塊。前端 React 通常不存敏感資料(密碼、token)、所有機密都靠 HTTP-only cookie 保護;Server Component 在 Next.js 15 之後可以「直接讀 cookie、執行權限檢查」,把後端的 middleware 邏輯往前推一層。本篇會從「認證 vs session vs 權限」三個觀念出發,建立 src/lib/auth.ts 工具層,並實作「登入頁 → Server Action → cookie 寫入 → middleware 保護 → Server Component 讀取」一條龍流程。
今天的重點有四個。第一,認證(authentication)跟授權(authorization)的差別:前者確認「你是誰」,後者確認「你能做什麼」。前者用帳號密碼 + cookie/JWT;後者用角色 / 權限清單。第二,cookie-based session 跟 token-based(JWT)session 的取捨:cookie 簡單、相對安全,但跨網域較麻煩;token 彈性大,但要小心 XSS 與儲存位置。第三,Next.js 15 的 middleware.ts 在路由層做權限檢查,效能與安全性最好。第四,把 session 檢查抽成 Server Component 內可呼叫的工具函式,避免每個頁面都重寫。
實務上 booking 預約系統有三種身份:訪客(未登入,只能看公開頁面)、會員(已登入,能管理自己的預約)、管理員(能看後台、審核預約)。Day 31 的專案會把這套權限模型實作完整;今天先把底層工具做好。為了聚焦觀念與 Next.js 機制,這篇用「本機記憶體儲存帳號」模擬後端,沒有外部認證服務;串接真實 OAuth 或 Auth.js 之類的函式庫留給讀者當練習。
認證、Session、權限的差別
三個詞在文章與官方說明裡常被混用,但它們指不同層次:
認證(Authentication):驗證使用者的身份。流程是「使用者提供帳號密碼 → 伺服器比對 → 比對成功就發一張『通行證』」。常見的通行證形式:cookie(瀏覽器自動帶上)、JWT(前端手動加到 Authorization header)、OAuth token(來自第三方)。
Session:伺服器端儲存的「使用者狀態」。Session 本身存在伺服器(記憶體、Redis、資料庫都可以),瀏覽器只拿到一個 session ID(透過 cookie)。每當瀏覽器送 request,伺服器讀 cookie 內的 session ID、查到對應的 session 資料、確認使用者身份。
權限(Authorization):確認「這個使用者能做什麼」。通常用「角色」表達:管理員 / 會員 / 訪客;或用「權限清單」:read:booking、write:booking、delete:user。授權邏輯通常在業務邏輯層(middleware 或 handler)。
把這三件事的關係整理一下:認證確認「你是誰」、Session 是「伺服器怎麼記得你」、權限是「你被允許做什麼」。實務上的標準流程:登入 → 認證 → 建立 session → 後續 request 用 session 認身份 → 業務邏輯層依權限決定能不能執行。booking 預約系統的設計範例:使用者登入 → 寫入 httpOnly cookie 的 session token → middleware 解析 token → Server Component 拿使用者資料 → 業務邏輯檢查角色。
完整實作:帳號密碼登入 + Cookie Session
從零做一個完整的登入流程。我們分四個檔:src/lib/auth.ts(session 工具)、src/app/login/page.tsx(登入頁)、src/app/login/actions.ts(登入 action)、middleware.ts(路由保護)。
先寫 auth 工具層。這個檔集中所有 session 相關的操作:讀 session、寫 session、刪 session、檢查登入狀態、檢查角色:
// src/lib/auth.ts
// Session 管理工具:cookie-based session,純 Server 端使用
import { cookies } from "next/headers";
import { redirect } from "next/navigation";
import { cache } from "react";
// 模擬使用者資料庫:實務上會從資料庫查
const USERS = [
{ id: "u_001", email: "user@example.com", password: "demo1234", name: "王小明", role: "member" },
{ id: "u_002", email: "admin@example.com", password: "admin1234", name: "林管理", role: "admin" },
] as const;
export type SessionUser = { id: string; email: string; name: string; role: "member" | "admin" };
export type Session = { user: SessionUser; expiresAt: number };
const COOKIE_NAME = "booking_session";
const SESSION_TTL_MS = 1000 * 60 * 60 * 24 * 7; // 7 天
// 驗證帳號密碼:實務上會用 bcrypt 等雜湊比對
export async function verifyCredentials(email: string, password: string): Promise<SessionUser | null> {
const user = USERS.find((u) => u.email === email && u.password === password);
if (!user) return null;
return { id: user.id, email: user.email, name: user.name, role: user.role };
}
// 建立 session 並寫入 cookie
export async function createSession(user: SessionUser): Promise<void> {
const session: Session = { user, expiresAt: Date.now() + SESSION_TTL_MS };
// 序列化 session 為 token:實務上用 JWT 簽名或加密
const token = Buffer.from(JSON.stringify(session)).toString("base64url");
const jar = await cookies();
jar.set(COOKIE_NAME, token, {
httpOnly: true, // 防止 JavaScript 讀取
secure: process.env.NODE_ENV === "production", // 強制 HTTPS
sameSite: "lax", // CSRF 防護
maxAge: SESSION_TTL_MS / 1000,
path: "/",
});
}
// 讀取 session
export async function getSession(): Promise<Session | null> {
const jar = await cookies();
const token = jar.get(COOKIE_NAME)?.value;
if (!token) return null;
try {
const session = JSON.parse(Buffer.from(token, "base64url").toString()) as Session;
if (session.expiresAt < Date.now()) return null;
return session;
} catch {
return null;
}
}
// 刪除 session(登出)
export async function destroySession(): Promise<void> {
const jar = await cookies();
jar.delete(COOKIE_NAME);
}
// 用 React cache:同一次 render 內 getSession 只會跑一次
export const getCurrentUser = cache(async (): Promise<SessionUser | null> => {
const session = await getSession();
return session?.user ?? null;
});
// 強制登入:未登入就跳轉到登入頁
export async function requireUser(): Promise<SessionUser> {
const user = await getCurrentUser();
if (!user) redirect("/login");
return user;
}
// 強制管理員:未登入或非管理員就跳轉
export async function requireAdmin(): Promise<SessionUser> {
const user = await requireUser();
if (user.role !== "admin") redirect("/");
return user;
}
這支工具檔把「session 相關的所有操作」集中起來。注意幾個關鍵設計:
httpOnly cookie:JavaScript 無法讀取這個 cookie,所以即使頁面有 XSS 漏洞、攻擊者也偷不到 session token。這是對抗 XSS 最有效的防線。
sameSite="lax":限制跨站請求帶上 cookie,防止 CSRF 攻擊。「lax」允許 top-level navigation 帶 cookie(例如從外部連結點進來),阻擋 cross-site form submit 跟圖片請求。
secure:生產環境強制 HTTPS,cookie 只在加密連線下傳輸。本機開發設為 false(因為 localhost 沒有 HTTPS)。
React cache():把 getCurrentUser 包進 React 的 cache()。這樣同一次 render tree 內多次呼叫 getCurrentUser() 只會實際執行一次(結果被 memo 起來)。對一個頁面有多個元件都需要 session 的情況特別有用。
密碼明文比較:範例為了簡化用明文比對。實務上一定要用 bcrypt 或 argon2 雜湊:await bcrypt.compare(password, user.passwordHash)。
登入 Server Action
Server Action 接收表單送出、驗證帳密、建立 session、跳轉到首頁或回原頁:
// src/app/login/actions.ts
// 登入 / 登出 action
"use server";
import { redirect } from "next/navigation";
import { z } from "zod";
import { verifyCredentials, createSession, destroySession } from "@/lib/auth";
const LoginSchema = z.object({
email: z.string().email("Email 格式錯誤"),
password: z.string().min(6, "密碼至少 6 個字元"),
redirectTo: z.string().optional(),
});
export type LoginState =
| null
| { ok: false; fieldErrors?: { email?: string; password?: string }; formError?: string };
export async function loginAction(_prev: LoginState, formData: FormData): Promise<LoginState> {
const parsed = LoginSchema.safeParse({
email: formData.get("email")?.toString() ?? "",
password: formData.get("password")?.toString() ?? "",
redirectTo: formData.get("redirectTo")?.toString() ?? undefined,
});
if (!parsed.success) {
const fieldErrors: LoginState & { ok: false } extends infer T
? T extends { fieldErrors?: infer F } ? F : never : never = {};
for (const issue of parsed.error.issues) {
const key = issue.path[0] as "email" | "password" | undefined;
if (key && !fieldErrors[key]) fieldErrors[key] = issue.message;
}
return { ok: false, fieldErrors };
}
const user = await verifyCredentials(parsed.data.email, parsed.data.password);
if (!user) {
return { ok: false, formError: "Email 或密碼錯誤" };
}
await createSession(user);
const safeRedirect = parsed.data.redirectTo ?? "/dashboard";
redirect(safeRedirect);
}
export async function logoutAction(): Promise<void> {
await destroySession();
redirect("/");
}
這個 Server Action 跟 Day 21 的表單處理一樣,用 useActionState 串接表單元件。差別只在「成功之後呼叫 createSession」——這是認證特有的步驟。redirect 跳轉到使用者原本想去的頁面(透過 redirectTo 隱藏欄位傳遞),沒指定的話跳到 /dashboard。
middleware:路由層保護
middleware 在請求進入頁面之前執行,適合做「全站性」的權限檢查。我們寫一支 middleware.ts 放在專案根目錄(跟 src/ 同層),用 Next.js 15 的新版 Edge runtime:
// middleware.ts
// 路由層保護:未登入存取 /dashboard、/bookings/new 就跳轉到登入頁
import { NextResponse, type NextRequest } from "next/server";
const PROTECTED_PATHS = ["/dashboard", "/bookings/new", "/admin"];
export function middleware(req: NextRequest) {
const { pathname, search } = req.nextUrl;
const isProtected = PROTECTED_PATHS.some((p) => pathname === p || pathname.startsWith(p + "/"));
if (!isProtected) return NextResponse.next();
// 讀 cookie:注意 middleware 不能用 next/headers 的 cookies(),要直接從 req 讀
const token = req.cookies.get("booking_session")?.value;
if (!token) {
const loginUrl = req.nextUrl.clone();
loginUrl.pathname = "/login";
loginUrl.searchParams.set("redirectTo", pathname + search);
return NextResponse.redirect(loginUrl);
}
// 簡單檢查:實務上要在 middleware 內驗證 token 簽名 / 過期
try {
const session = JSON.parse(Buffer.from(token, "base64url").toString());
if (session.expiresAt < Date.now()) {
const loginUrl = req.nextUrl.clone();
loginUrl.pathname = "/login";
loginUrl.search = "";
return NextResponse.redirect(loginUrl);
}
if (pathname.startsWith("/admin") && session.user.role !== "admin") {
const home = req.nextUrl.clone();
home.pathname = "/";
return NextResponse.redirect(home);
}
} catch {
const loginUrl = req.nextUrl.clone();
loginUrl.pathname = "/login";
return NextResponse.redirect(loginUrl);
}
return NextResponse.next();
}
// 設定 middleware 在哪些路徑執行
export const config = {
matcher: ["/dashboard/:path*", "/bookings/new", "/admin/:path*"],
};
這支 middleware 做四件事:第一,判斷目前路徑是否需要保護(PROTECTED_PATHS 內);第二,沒帶 cookie 就跳轉到 /login 並帶上 redirectTo 參數;第三,cookie 過期或損壞也跳轉;第四,路徑是 /admin 但使用者不是管理員也跳轉。實務上你也可以把「需要保護的路徑」寫進 config.matcher 內(避免 middleware 在每個請求都跑)。
特別注意 middleware 內的限制:不能 import「需要 Node.js runtime」的函式庫、不能讀寫檔案系統、不能用 next/headers 的 cookies()(要直接從 req.cookies 讀)。因為 middleware 跑在 Edge runtime 上,環境限制比 Server Component 嚴格。
Server Component 內的權限檢查
除了 middleware,每個需要登入的 Server Component 也應該「自己檢查 session」。原因是 middleware 不是萬靈丹——它擋的是「未登入就跳轉」,但業務邏輯可能還有「管理員才能刪除」這種更細的權限:
// src/app/bookings/[id]/page.tsx
// 個人化頁面:只有本人或管理員能看
import { notFound } from "next/navigation";
import { getBooking } from "@/lib/api";
import { getCurrentUser } from "@/lib/auth";
import { BookingDetail } from "@/features/booking/components/BookingDetail";
type Params = { id: string };
export default async function BookingDetailPage({ params }: { params: Promise<Params> }) {
const { id } = await params;
const user = await getCurrentUser();
const booking = await getBooking(id);
if (!booking) notFound();
// 個人預約只有本人或管理員能看
if (!user || (user.id !== booking.customerId && user.role !== "admin")) {
notFound(); // 用 notFound 而不是 403,避免洩漏資源存在性
}
return <BookingDetail booking={booking} />;
}
這個範例展示了「兩層權限」的標準設計:middleware 擋未登入(路徑層)、Server Component 擋個人資源(資料層)。兩者互補——middleware 處理「該不該進到這個頁面」、Server Component 處理「該不該看這筆資料」。
Client Component 內讀取使用者
Client Component 不能用 cookies() 讀 session(因為它在瀏覽器端),但可以透過 props 接收「目前使用者」資料。常見的模式是 Server Component 讀完 user、傳給 Client Component:
// src/app/dashboard/page.tsx
// Server Component:把 user 傳給 Client Component
import { requireUser } from "@/lib/auth";
import { listMyBookings } from "@/lib/api";
import { DashboardClient } from "@/features/dashboard/components/DashboardClient";
export default async function DashboardPage() {
const user = await requireUser();
const bookings = await listMyBookings(user.id);
return <DashboardClient user={user} bookings={bookings} />;
}
Client Component 拿到 user 物件後可以做條件渲染(「管理員看到後台連結」)、顯示個人化資訊(「歡迎,王小明」)。但不能修改 user——修改必須透過 Server Action 重新走完整的流程。
常見錯誤與踩雷
第一個雷是「cookie 沒設 httpOnly」。如果為了方便在前端讀 user 資料而把 session token 設成前端可讀,等於把所有使用者暴露在 XSS 攻擊下。任何被注入的 script 都能讀 document.cookie、把 token 送到攻擊者伺服器。修法:session token 一律 httpOnly,需要在前端用的「使用者資訊」透過 Server Component 讀完後用 props 傳遞。
第二個雷是「middleware 內讀取完整 session 物件」。middleware 跑在 Edge runtime、沒有完整 Node.js API;如果你在 auth.ts 用了 jsonwebtoken 之類的函式庫,middleware 內呼叫會炸。修法是 middleware 內只做「token 存在性 + 過期檢查」的快速判斷,完整的權限檢查交給 Server Component 用 requireUser()。
第三個雷是「密碼明文儲存 / 比較」。這個踩雷不只是觀念問題、是資安事件等級的疏失。實務上一定要用 bcrypt 或 argon2 把密碼雜湊後存進資料庫,登入時 await bcrypt.compare(input, hash) 比對。本範例用明文比對是為了聚焦 Next.js 機制,實戰千萬不要這樣寫。
第四個雷是「cache() 包了一個會過期的函式」。cache() 是「同一次 render tree 內 memo」,不是「整個 request lifecycle memo」。如果你在 Server Action 內呼叫 cache() 包過的函式,action 結束後 cache 就被清掉,下一次 render 重新計算。對 session 檢查這是好事(每次都重新讀最新 cookie);但對「昂貴的資料庫查詢」可能不是你要的。
第五個雷是「CSRF 防護只靠 cookie」。cookie 自動帶上是方便也是風險——攻擊者可以從別的網域送 POST 到你的 server,cookie 自動帶上、server 誤以為是本人操作。修法除了 sameSite="lax" 之外,Server Action 內建有「加密 action ID」機制,框架會驗證 POST 帶的 action ID 是不是來自你的頁面,不需要自己寫 CSRF token。
效能與實務提醒
cookie 的 size 要控制。瀏覽器對每個 domain 的 cookie 有限制(大約 4KB)、每次 request 都會帶上所有 cookie。如果 session token 太大或加了一堆多餘的 cookie,每次 request 都會浪費頻寬。實務上 session token 應該盡量小(只放 user ID、role、exp),不要把整個 user 物件放進去。
middleware 的影響範圍要嚴格控制。config.matcher 寫得越精準,middleware 跑得越少。如果 middleware 內要做的事情很多(例如查資料庫),把它放在「真正需要保護的路徑」才執行,例如:
export const config = {
matcher: [
"/dashboard/:path*", // 只對 /dashboard 開頭的路徑執行
"/admin/:path*",
"/bookings/new",
],
};
session 過期處理要明確。當 server 端 session 過期時、使用者的請求會被當作「未登入」處理;前端要有對應的 UX:跳轉到登入頁、提示「session 過期請重新登入」、保留使用者原本想做的動作。Next.js 15 的 Server Action 內 requireUser() 會自動跳轉,所以 UI 不必自己處理;但純 client side 的 fetch 還是要寫對應的 401 處理。
最後是「process.env.NODE_ENV 的判斷」。secure: process.env.NODE_ENV === "production" 這個寫法在 build time 被靜態取代——build 出來的 production bundle 裡這個判斷永遠是 true,development bundle 永遠是 false。實務上不會出問題,但要記得「不能用 process.env.NODE_ENV 做 runtime 判斷」。
小結
今天把 Next.js 15 的認證與 session 管理展開成五個層次。第一層是觀念:認證、session、權限是三件事,分別回答「你是誰」、「伺服器怎麼記得你」、「你能做什麼」。第二層是 cookie-based session:用 httpOnly、sameSite="lax"、secure 三個屬性保護 cookie,token 內只放最少必要的資料。第三層是 middleware:在路由層快速擋下未登入的請求,避免每個頁面都重複檢查。第四層是 Server Component:用 requireUser()、requireAdmin() 等工具函式把 session 檢查集中。第五層是 Client Component:透過 props 接收 user 資訊、不在前端儲存敏感資料。
這套設計對 booking 預約系統的價值是「個人化與權限分離」。「個人化」靠 Server Component 讀 cookie 拿到 user、用 user.id 查個人預約;「權限分離」靠 Server Action 內的 requireAdmin() 阻止非管理員執行審核操作。對照後端,這就像把 Django 的「login_required 裝飾子」與「user_passes_test」搬到 Next.js;差別是這些檢查在 Edge runtime 跑、效能更好、跟 Server Component 的整合更緊密。明天 Day 23 會進入 SEO 與 metadata,把 App Router 的 metadata API、Open Graph、sitemap、robots.txt 一次展開。
JWT vs Cookie Session 的取捨
實務上認證有兩種主流架構:cookie-based session(前面實作的)跟 token-based(最常見的是 JWT)。兩者各有優缺,選錯會影響資安與擴充性。
Cookie-based session 的優點:第一,安全性高,因為 cookie httpOnly + sameSite 是瀏覽器原生保護,XSS 攻擊拿不到 token。第二,伺服器端可以隨時撤銷 session(刪資料庫那筆即可),不用等 token 過期。第三,實作簡單,前面那段程式碼就是全部。第四,跟 SameSite、CSRF、Secure flag 整合良好。缺點:第一,跨網域比較麻煩(需要 CORS 設定 cookie attribute)。第二,每個 request 都帶 cookie,size 會增加流量成本。
JWT(JSON Web Token)的優點:第一,無狀態,伺服器不用存 session,適合分散式系統。第二,跨網域、跨服務方便,可以放在 Authorization header。第三,token 本身可以帶資料(payload),減少資料庫查詢。缺點:第一,難以撤銷,除非用 blacklist 或短 TTL + refresh token。第二,XSS 防護完全靠「不要把 token 放 localStorage」(要用 httpOnly cookie)。第三,size 比較大,每個 request 都帶著一長串。第四,secret 管理複雜,rotating secret 會把所有現存 token 失效。
對 booking 預約系統這種「單一 Next.js App + 同一個 domain + 個人化內容為主」的場景,cookie-based session 是較好的選擇。對「多個子網域共用同一個認證」、「行動 App + Web 都要登入」、「第三方 API 整合」的場景,JWT 可能更適合。我們本系列聚焦前者;後者的整合留給 OAuth 章節展開。
Session 過期與 Refresh 策略
Session 過期策略有三種:固定 TTL、滑動 TTL、絕對過期日。固定 TTL 最簡單(登入後 7 天過期);滑動 TTL 每次使用就延長(使用者持續活躍就持續登入);絕對過期日是「無論如何,14 天後一定要重新登入」。實務上常見的組合是「滑動 7 天 + 絕對 30 天」:使用者活躍時不會被踢出,但超過 30 天一定要重新認證。
// 滑動過期:每次讀 session 就延長
export async function getSessionWithRefresh(): Promise<Session | null> {
const session = await getSession();
if (!session) return null;
// 絕對過期日檢查
const ABSOLUTE_TTL = 1000 * 60 * 60 * 24 * 30; // 30 天
if (session.expiresAt > Date.now() + ABSOLUTE_TTL) {
await destroySession();
return null;
}
// 滑動過期:剩餘時間小於一半就延長
const HALF_TTL = SESSION_TTL_MS / 2;
if (session.expiresAt - Date.now() < HALF_TTL) {
const refreshed: Session = { user: session.user, expiresAt: Date.now() + SESSION_TTL_MS };
const token = Buffer.from(JSON.stringify(refreshed)).toString("base64url");
const jar = await cookies();
jar.set(COOKIE_NAME, token, {
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
maxAge: SESSION_TTL_MS / 1000,
path: "/",
});
return refreshed;
}
return session;
}
這個版本把簡單的 getSession() 換成 getSessionWithRefresh():每次讀 session 自動檢查過期、需要的話刷新 cookie 寫回。注意這種 refresh 邏輯不適合放進 cache(),因為每次請求都應該重新評估過期時間——把這個函式跟「cache() 包起來讓同 render tree 共用」兩件事拆開來看。
跟 Auth.js(NextAuth)整合的考量
前面我們自己寫了帳號密碼登入,實務上中型以上的應用會用 Auth.js(NextAuth 的新名字)。它處理掉 OAuth provider 串接、session 管理、CSRF、JWT 等瑣事,開發者只需要寫「自訂 sign-in callback」、「自訂 session callback」、「自訂 authorize callback」三個函式就能搞定。
整合的關鍵點:第一,Auth.js v5 對 Next.js App Router 是原生支援,auth() 函式可以同時在 Server Component 與 Route Handler 內呼叫。第二,auth() 回傳的 session 物件可以擴充(透過 session callback 加 role),讓你在 Server Component 內用 session.user.role === "admin" 做權限檢查。第三,Auth.js 的 middleware 整合(export { auth as middleware } from "@/auth")讓 middleware 直接用 Auth.js 處理過的 session 物件,省去自己 parse cookie。
// middleware.ts(用 Auth.js 整合的版本)
export { auth as middleware } from "@/auth";
export const config = {
matcher: ["/dashboard/:path*", "/admin/:path*"],
};
// src/auth.ts(Auth.js 設定檔)
import NextAuth from "next-auth";
import Credentials from "next-auth/providers/credentials";
export const { auth, handlers, signIn, signOut } = NextAuth({
providers: [
Credentials({
credentials: {
email: { label: "Email", type: "email" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const user = await verifyCredentials(credentials.email, credentials.password);
return user ?? null;
},
}),
],
callbacks: {
async session({ session, token }) {
// 把 role 從 token 搬到 session
session.user.role = token.role as "member" | "admin";
return session;
},
},
pages: {
signIn: "/login",
},
});
Auth.js 的設定比較複雜(callback chain、provider config、types augmentation),但對需要 OAuth(Google、GitHub、LINE Login)或多 provider 的應用是值得的。對純帳號密碼的內部系統,自己寫 80 行的 auth.ts 可能更直覺。兩者選哪個,取決於「需不需要 OAuth」這個關鍵問題。
結語
明天,我們會正式進入 Day 23「SEO 與 metadata」。我們會在 booking-fe 加上 metadata 物件(靜態與動態)、generateMetadata 函式(依 params 動態產生)、Open Graph 與 Twitter Card 標籤、sitemap.ts 自動產生 sitemap、robots.ts 設定爬蟲規則。讀完你應該能讓 booking 預約系統的每個頁面都有正確的 <title> 與 <meta> 標籤,並透過「社群分享卡片預覽」讓連結分享看起來更專業。
延伸資源
- Next.js 15 官方 Authentication 章節:
https://nextjs.org/docs/app/building-your-application/authentication - Next.js 15 官方 Middleware 章節:
https://nextjs.org/docs/app/building-your-application/routing/middleware - Next.js 官方 cookies() 函式(Server Component 讀 cookie):
https://nextjs.org/docs/app/api-reference/functions/cookies - OWASP 認證 Cheat Sheet:
https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html - bcrypt npm 套件(密碼雜湊):
https://www.npmjs.com/package/bcrypt
留言
張貼留言