跳到主要內容

FE Day 22 認證與 session 管理

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

留言

這個網誌中的熱門文章

Day 2 變數與資料型別

Day 2 變數與資料型別 引言 寫程式的過程中,變數與資料型別是處理資料的基礎。變數是存放資料的容器,資料型別則決定這筆資料有哪些特性、可以進行哪些操作。學會定義變數、認識各種資料型別,是學好 Python 的關鍵一步。 這篇文章會帶你了解 Python 中變數的觀念、如何定義變數,以及常見的資料型別,包括整數、浮點數、字串、布林值,還有串列、元組、字典與集合等容器型別。我們也會介紹變數的命名規則與撰寫風格建議,以及如何用 type() 檢查資料型別。 什麼是變數?如何在 Python 中定義變數 變數是在程式執行時用來存放資料的名稱。透過定義變數,我們可以給一筆資料一個名字,並在程式的其他地方用這個名字取用該筆資料。在 Python 中,變數不需要事先宣告型別,因為 Python 是動態型別語言,變數的型別由指定給它的值決定。 定義變數的基本語法 在 Python 中定義變數非常簡單,只要用賦值符號 = 把值指定給變數即可。例如: x = 5 # 定義變數 x,並把整數 5 賦值給它 name = "Alice" # 定義變數 name,並把字串 "Alice" 賦值給它 在這裡,x 是一個變數,被賦予整數 5;name 是另一個變數,被賦予字串 "Alice"。 變數的更新與覆寫 變數的值可以修改,也就是說,我們可以在程式的不同地方給同一個變數新的值。例如: x = 10 # x 最初被賦予 10 x = 15 # x 的值現在被更新為 15 這樣就能依照需求,在程式執行過程中靈活調整變數的值。 Python 的動態型別系統 Python 和某些靜態型別語言不同,定義變數時不需要宣告型別。賦值時,Python 會根據值自動判斷變數的型別。例如: x = 5 # x 是整數 x = 3.14 # x 變成浮點數 x = "Hi" # x 變成字串 同一個變數在程式執行過程中可以存放不同型別的值,這是 Python 的彈性之一。 常見資料型別 在 Python 中,資料型別決定我們可以對變數進行哪些操作...

Day 1 Python 簡介與環境設定

Day 1 Python 簡介與環境設定 引言 在現在的科技環境裡,程式設計已經是一項重要技能。無論你是對資料科學有興趣、想成為開發者,或是想踏入人工智慧(AI)領域,學會寫程式都能明顯提升你的競爭力。在眾多程式語言中,Python 因為語法簡單、功能強大、應用範圍廣泛,成為許多人進入程式世界的第一選擇。這篇文章會帶你認識 Python 的背景與優勢,並一步步教你在不同系統上安裝與設定 Python 開發環境,最後寫出第一支 Python 程式。 為什麼選擇 Python? Python 是一種高階程式語言,由 Guido van Rossum 在 1991 年發布。Python 的設計哲學強調程式碼的可讀性,並用縮排來定義程式區塊,這點和許多使用大括號的語言不同。簡潔的語法讓它成為初學者的理想選擇;就算是經驗豐富的開發者,也能用它完成複雜的專案。 Python 的優勢如下: 簡單易學 :Python 的語法清楚、結構簡潔,初學者很快就能上手。和其他語言相比,學習曲線相對平緩,不需要先弄懂一堆複雜觀念,就能開始寫程式。 應用範圍廣泛 :從資料科學、網頁開發、人工智慧、機器學習、自動化測試到網路爬蟲,Python 都有大量開源函式庫與工具支援,而且在這些領域都扮演關鍵角色。 豐富的函式庫與框架 :Python 的函式庫生態系非常龐大。做資料分析有 NumPy、Pandas;開發網站有 Django、Flask;做深度學習有 TensorFlow、PyTorch。各種需求幾乎都能找到對應的套件,讓開發更有效率。 跨平台支援 :Python 支援 Windows、macOS、Linux 等作業系統,程式通常不需要太多修改就能跨平台執行,讓開發與部署更有彈性。 活躍的社群 :Python 擁有龐大的開發者社群。學習或開發上遇到問題,幾乎都能在社群與論壇(例如 Stack Overflow)找到答案,對初學者來說是很強的後盾,也能減少卡關時的挫折感。 Python 的應用領域 Python 的流行與強大功能,讓許多領域都開始大量使用它。以下是幾個常見的應用方向: 資料科學 :隨著大數據與人工智慧興起,資料科學大量使用 Python。NumPy、Pandas 與 Matplotlib 等工具能處理和分析龐...

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門

Python 從入門到 PyTorch 深度學習:開啟 AI 世界的大門 隨著人工智慧(AI)與深度學習(Deep Learning)快速發展,越來越多人對這些技術產生興趣。不論你是想踏入 AI 領域的初學者,還是已經有程式基礎的開發者,學好 Python 與深度學習框架(例如 PyTorch),都能為你打開更多可能。 為什麼選擇 Python? Python 已經是資料科學與人工智慧領域的首選語言。它的語法簡潔、容易上手,而且擁有龐大的生態系與大量開源函式庫。無論是資料處理、資料視覺化,還是建立機器學習與深度學習模型,Python 都能勝任。對想進入 AI 或資料科學領域的人來說,它幾乎是必備工具。 PyTorch 是什麼? PyTorch 是由 Meta(原 Facebook)AI 研究團隊開發的開源深度學習框架,以易用、靈活和動態計算圖著稱,是許多 AI 研究人員與開發者的首選。相較於其他框架,PyTorch 的寫法更貼近原生 Python,對初學者相對友善。無論是簡單的實驗,還是複雜的深度學習模型,PyTorch 都能提供強大的支援。 這個系列能帶給你什麼? 這個系列會從 Python 的基礎開始,帶你一步一步學習,最後能自己用 PyTorch 建立深度學習模型。即使你完全沒有寫過程式,也能跟著文章的節奏累積技能,理解 AI 與深度學習的核心觀念。 本系列涵蓋的主題 Python 基礎:從變數、條件判斷到函式與模組。 資料處理工具:用 NumPy 與 Pandas 有效率地操作資料。 資料視覺化:用 Matplotlib 與 Seaborn 把資料畫成圖表。 深度學習的數學基礎:線性代數、微積分與機率。 PyTorch 入門:理解張量、模型建構與 GPU 加速。 基礎深度學習模型:CNN 與 RNN 的實作應用。 深度學習專案實戰:從資料前處理到模型部署的端到端流程。 誰適合這個系列? 程式初學者 :如果你對 AI 充滿好奇,卻還沒寫過程式,系列的第一部分會帶你快速上手 Python,並幫助你理解深度學習的基本觀念。 資料科學愛好者 :如果你已經熟悉一些資料處理方法,進階部分會教你如何用 PyTorch 建構深度學習模型。 開發者與研究人員 :想更深入了...