Next.js API Route 驗證:如何保護你的端點
Next.js 的 API route 與頁面保護不同——Route Handler 必須回傳 HTTP 狀態碼,而不是重新導向。本篇涵蓋工作階段 Cookie、Bearer 權杖、RBAC 與 Server Actions,並附完整 TypeScript 範例。
為什麼 API Route 驗證與頁面保護不同
保護頁面時,你會檢查使用者工作階段,並把未驗證訪客導向登入畫面。API route 的行為不同。
Route Handler 是端點,不是頁面。呼叫方可能是行動 App、fetch(),或自動化腳本——它們都無法跟著 302 Found 重新導向走。因此 Route Handler 必須回傳正確的 HTTP 狀態碼:
- 401 Unauthorized——請求沒有有效憑證(缺少權杖、工作階段過期)。
- 403 Forbidden——請求有有效憑證,但使用者沒有執行此動作的權限。
這對 API 客戶端很重要:瀏覽器可以跟隨重新導向;行動 SDK 需要明確狀態碼,才知道要提示登入或顯示「存取遭拒」。
還有第二個重點:光靠 middleware 不夠。我們會在 常見錯誤 一節說明。
若你對 Next.js 驗證還不熟,建議先讀 Next.js 驗證指南,再回到本篇看 API 專屬模式。
App Router Route Handlers:快速複習
Next.js 13 推出 App Router,以 Route Handlers 取代 pages/api/ 目錄。你不再建立 pages/api/users.ts,而是建立 app/api/users/route.ts。
檔案以具名 HTTP 方法(GET、POST、PUT、DELETE 等)匯出,使用標準 Web 的 Request 與 Response API:
// app/api/users/route.ts
export async function GET(request: Request) {
return Response.json({ message: "Hello from Route Handler" });
}
與 pages/api 的主要差異:
- Route Handlers 使用標準的
Request/Response,而非舊式 Node.jsreq/res。 - Cookie 與標頭透過
next/headers的輔助函式存取(見下文)。 - Route Handlers 可放在
app/下任意位置,不限於app/api/。 - 可選擇 Edge Runtime 或 Node.js runtime。
若你正在從 pages/api 遷移,邏輯相同——只需調整函式簽章與回應寫法。
在 Route Handler 讀取工作階段 Cookie 與 Bearer 權杖
Route Handler 必須從進來的請求讀取憑證。常見有兩種模式。
讀取工作階段 Cookie
工作階段 Cookie 像演唱會手環:登入時由伺服器發給瀏覽器,之後每次請求自動帶上。在 Route Handler 使用 Next.js 的 cookies()(Next.js 15 起為 async):
import { cookies } from "next/headers";
export async function GET(request: Request) {
const cookieStore = await cookies();
const sessionToken = cookieStore.get("session")?.value;
if (!sessionToken) {
return Response.json({ error: "Unauthenticated" }, { status: 401 });
}
// validate sessionToken with your auth provider...
}
讀取 Bearer 權杖
機器對機器呼叫——行動 App、其他後端——通常會在 Authorization 標頭帶 JWT:
export async function GET(request: Request) {
const authHeader = request.headers.get("Authorization");
if (!authHeader || !authHeader.startsWith("Bearer ")) {
return Response.json({ error: "Missing or malformed token" }, { status: 401 });
}
const token = authHeader.slice(7); // strip "Bearer " prefix
// validate token with your auth provider...
}
該用哪一種?瀏覽器應用適合用工作階段 Cookie——由瀏覽器自動送出,避免在客戶端存權杖。非瀏覽器的 API 消費者較適合 Bearer。Authgear 兩者皆支援,下文會示範。
完整範例:受保護的 Route Handler
以下 GET 端點在使用者未驗證時回傳 401,已驗證則回傳使用者資料:
// app/api/profile/route.ts
import { NextRequest, NextResponse } from "next/server";
import { cookies } from "next/headers";
async function getSessionUser(sessionToken: string) {
// Replace with your real session validation logic:
// verify a JWT, query a session store, or call your auth provider.
if (sessionToken === "invalid") return null;
return { id: "user_123", email: "alice@example.com", role: "user" };
}
export async function GET(request: NextRequest) {
// 1. Read the session cookie
const cookieStore = await cookies();
const sessionToken = cookieStore.get("authgear-session")?.value;
// 2. If no token is present, reject immediately
if (!sessionToken) {
return NextResponse.json(
{ error: "Authentication required" },
{ status: 401 }
);
}
// 3. Validate the token
const user = await getSessionUser(sessionToken);
if (!user) {
return NextResponse.json(
{ error: "Invalid or expired session" },
{ status: 401 }
);
}
// 4. Return the protected resource
return NextResponse.json({
id: user.id,
email: user.email,
role: user.role,
});
}
注意兩段分開的 401 檢查:一段對應缺少權杖,一段對應無效權杖。這讓 API 客戶端有更清楚的錯誤訊息,又不過度暴露是哪個條件觸發拒絕。
在 Route Handlers 使用 Authgear Next.js SDK
若你使用 Authgear,@authgear/nextjs 的 currentUser() 會代你讀取並驗證工作階段 Cookie——建議用法,可保持程式簡潔並確保工作階段驗證一致。
請先依 Authgear Next.js 設定指南 安裝 SDK 並設定專案。完成後,保護 Route Handler 可寫成:
// app/api/profile/route.ts
import { NextResponse } from "next/server";
import { currentUser } from "@authgear/nextjs/server";
import { authgearConfig } from "@/lib/authgear";
export async function GET() {
// currentUser() reads the Authgear session cookie and validates it.
// It returns null if the user is not authenticated.
// It also refreshes an expired access token automatically before
// fetching user info from the Authgear userinfo endpoint.
const user = await currentUser(authgearConfig);
if (!user) {
return NextResponse.json(
{ error: "Authentication required" },
{ status: 401 }
);
}
// user.sub is the user's unique ID (subject claim)
// user.email is the user's email address
return NextResponse.json({
userId: user.sub,
email: user.email,
});
}
currentUser() 在 React Server Components、Route Handlers 與 Server Actions 行為一致——整個 Next.js 應用可採同一套模式。
以使用者 Claims 實作 RBAC(角色型存取控制)
驗證回答「這是誰?」授權回答「可以做什麼?」。驗證工作階段後,可從 claims 讀取角色以實作 RBAC。
Authgear 透過 /claims/user/roles claim 暴露角色——為 userinfo 回應中的字串陣列。以下為僅限管理員的端點:已登入但非 admin 角色則回 403 Forbidden:
// app/api/admin/users/route.ts
import { NextResponse } from "next/server";
import { currentUser } from "@authgear/nextjs/server";
import { authgearConfig } from "@/lib/authgear";
export async function GET() {
const user = await currentUser(authgearConfig);
// Step 1: Check authentication
if (!user) {
return NextResponse.json(
{ error: "Authentication required" },
{ status: 401 }
);
}
// Step 2: Check authorization
// Authgear returns roles in the /claims/user/roles claim.
// Assign roles to users in the Authgear portal under Users > Roles & Groups.
const roles = user["/claims/user/roles"] as string[] | undefined;
if (!roles?.includes("admin")) {
return NextResponse.json(
{ error: "Admin access required" },
{ status: 403 }
);
}
// Step 3: Return admin-only data
const users = await fetchAllUsers(); // your data layer
return NextResponse.json({ users });
}
兩步模式——先驗證身分、再檢查權限——是有意的:匿名請求沒必要評估角色。
保護 Server Actions
Server Actions("use server" 函式)讓你從元件直接呼叫伺服器邏輯。它們看起來像一般 async 函式,但底層仍是 HTTP 端點——因此需要與 Route Handler 相同等級的驗證檢查。
// app/actions/update-profile.ts
"use server";
import { currentUser } from "@authgear/nextjs/server";
import { authgearConfig } from "@/lib/authgear";
import { redirect } from "next/navigation";
export async function updateProfile(formData: FormData) {
// ALWAYS check auth inside the action itself.
// Never assume the caller is authenticated just because
// the page that rendered the form checked auth before rendering.
const user = await currentUser(authgearConfig);
if (!user) {
// For Server Actions called from forms, redirecting to login is acceptable.
// For actions called programmatically, consider throwing an error instead.
redirect("/login");
}
const displayName = formData.get("displayName") as string;
// Proceed with the update...
await saveDisplayName(user.sub, displayName);
}
驗證檢查必須在 action 內部——不能只做在渲染表單的頁面上。因為 Server Actions 是獨立的 HTTP 端點,任何人都能直接呼叫,繞過頁面層的驗證。
常見錯誤
只依賴 middleware
// middleware.ts — a first line of defence, but NOT sufficient on its own
import { NextRequest, NextResponse } from "next/server";
export function middleware(request: NextRequest) {
const session = request.cookies.get("authgear-session");
if (!session) {
return NextResponse.redirect(new URL("/login", request.url));
}
}
export const config = {
matcher: ["/api/:path*"],
};
問題: Middleware 在 Edge 於 Route Handler 之前執行。它能快速擋掉明顯無效的請求——但不能當唯一安全層:
- Middleware 設定可能被誤設,導致路由未受保護。
- Edge Runtime 的 middleware 無法使用完整驗證邏輯(無資料庫查詢、密碼學能力有限)。
- 若有人因 matcher 設定錯誤而繞過 middleware,Route Handler 內沒有備援檢查。
作法: 永遠在 Route Handler 內部再做驗證。把 middleware 當快速第一道防線——但把 handler 層檢查當成真正的安全保證。這就是多層防禦(defence in depth)。
更深入說明見 Next.js middleware 驗證指南。
其他常見錯誤
| 錯誤 | 為何有問題 | 修正 |
|---|---|---|
| 驗證失敗仍回 200,只在 body 寫錯誤 | API 客戶端若不解析 body 就無法可靠偵測驗證錯誤 | 一律使用 401 或 403 |
| 在 Server Actions 略過驗證 | Server Actions 是公開端點;任何人都能直接呼叫 | 每個非公開 action 頂端都加 currentUser() 檢查 |
把權杖存在 localStorage |
易受 XSS 攻擊 | 工作階段權杖請用 HttpOnly Cookie |
| 驗證通過後未檢查過期 | 過期工作階段仍可能通過基本簽章檢查 | 使用會自動驗證過期的函式庫或 SDK |
FAQ
Next.js API route 中 401 與 403 差在哪?
401 Unauthorized 表示請求沒有有效身分——Cookie 缺失、權杖沒帶、或權杖無效/過期。客戶端應提示登入。403 Forbidden 表示身分有效,但沒有權限執行此動作。客戶端應顯示「存取遭拒」,而非登入畫面。正確狀態碼讓 API 客戶端不必解析錯誤 body 也能正確反應。
currentUser() 能同時用在 Route Handler 與 Server Component 嗎?
可以——這正是 Authgear Next.js SDK 的優點之一。@authgear/nextjs/server 的 currentUser() 在 Route Handlers、Server Components 與 Server Actions 行為一致,以相同邏輯讀取並驗證工作階段 Cookie,不必為不同層寫不同驗證碼。
每個 Route Handler 都要驗證,還是只有敏感端點?
最安全的預設是:每個 Route Handler 都要求驗證,只有刻意公開的端點才標註例外。這種「預設安全、公開才 opt-out」可確保新加的端點不會不小心漏保護。對於本來就該公開的端點——例如健康檢查、webhook——請加註解說明為何不需驗證。
如何保護 Route Handler 免受跨來源請求?
Next.js 不會自動為 Route Handlers 加上 CORS 標頭。若端點只應被你自己的前端呼叫,請檢查 Origin 標頭並拒絕不符合網域的請求。若需要接受跨來源,請在 Response 明確設定 CORS。注意:Server Actions 內建以 Origin/Host 比對防 CSRF——Route Handlers 沒有這項自動保護。