限制特定網頁
限制特定網頁(Auth 路由保護)
TL;DR前端 router guard 只是 UX 改善,不是真正的安全層。真正的保護必須在 server side(SSR getServerSideProps / App Router middleware / API route 驗證 token)。CSR-only 的 router guard 任何人都繞得過。2026 推薦:Next.js App Router 的 middleware.ts 統一處理。
主要參考Protected Routes in Next.js — Danish Shakeel
核心觀念:前端限制 ≠ 真安全
使用者輸入 /admin URL
│
↓
┌─────────────────┐
│ 你的網頁載入 │ ← ==駭客已經拿到頁面 HTML / JS==
└─────────┬───────┘
↓
┌─────────────────┐
│ router guard │ ← 太晚了,可以禁用 JS 跳過
│ router.push │
│ ('/login') │
└─────────────────┘
router guard 在 client 端跑,就一定可以被繞過:
- 禁用 JS
- 改 source map / debugger 中斷 redirect
- 直接 fetch API endpoint 拿資料(如果 API 沒驗證)
所以 router guard 只是「友善地把人擋下」,真實安全要靠 server。
4 種 Render 模式的保護策略
CSR(Client-Side Rendering)— 最不安全
// 純 client guard,只是 UX
'use client';
import { useRouter } from 'next/navigation';
import { useEffect } from 'react';
export default function AdminPage() {
const router = useRouter();
useEffect(() => {
const token = localStorage.getItem('token');
if (!token || !verifyTokenLocally(token)) {
router.push('/login');
}
}, [router]);
return <div>Admin Content</div>;
}
問題:
- 頁面 HTML / JS bundle 已下載,駭客能看 source code
- 禁用 JS 就跳過 redirect
- 真正的 admin data 應該 server 驗證後才回傳,前端只是「可以看到 UI」
CSR-only 真實漏洞如果 admin data 透過 client-side fetch 拿到,駭客可以:
- 偽造 token / 改 token payload
- 直接打 API endpoint
- 拿到資料(因為 API 沒驗證)
根本解:API endpoint 必須在 server 驗證 token,而不是相信 client 帶來的 token。
SSR(Server-Side Rendering)— 最安全
// pages/admin.tsx(Pages Router)
import { GetServerSideProps } from 'next';
import jwt from 'jsonwebtoken';
export const getServerSideProps: GetServerSideProps = async (ctx) => {
const token = ctx.req.cookies['session']; // ⭐ 從 cookie 讀,不是 localStorage
if (!token) {
return { redirect: { destination: '/login', permanent: false } };
}
try {
const decoded = jwt.verify(token, process.env.JWT_SECRET!);
return { props: { user: decoded } };
} catch {
return { redirect: { destination: '/login', permanent: false } };
}
};
export default function AdminPage({ user }) {
return <div>Welcome {user.name}</div>;
}
為什麼最安全:
- 驗證在 server 端,駭客碰不到 secret
- 失敗直接 302 redirect,使用者根本沒拿到 admin HTML
- cookie 用 httpOnly,JS 偷不走 token
App Router(Next.js 13+)— 現代首選
方案 A:Middleware 統一處理
// middleware.ts(專案根目錄)
import { NextResponse } from 'next/server';
import type { NextRequest } from 'next/server';
const PROTECTED_PATHS = ['/admin', '/dashboard', '/profile'];
export async function middleware(request: NextRequest) {
const path = request.nextUrl.pathname;
const needsAuth = PROTECTED_PATHS.some((p) => path.startsWith(p));
if (!needsAuth) return NextResponse.next();
const token = request.cookies.get('session')?.value;
if (!token) {
return NextResponse.redirect(new URL('/login', request.url));
}
try {
await verifyJWT(token); // 你的驗證邏輯
return NextResponse.next();
} catch {
return NextResponse.redirect(new URL('/login', request.url));
}
}
export const config = {
matcher: ['/admin/:path*', '/dashboard/:path*', '/profile/:path*'],
};
middleware 在 edge runtime 跑,所有 request 進來前先過這層。最簡潔的做法。
方案 B:在 Server Component 直接驗證
// app/admin/page.tsx — Server Component
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
import { verifyJWT } from '@/lib/auth';
export default async function AdminPage() {
const token = cookies().get('session')?.value;
if (!token) redirect('/login');
try {
const user = await verifyJWT(token);
return <div>Welcome {user.name}</div>;
} catch {
redirect('/login');
}
}
Server Component 直接 await 驗證,失敗 redirect。比 Pages Router 的 getServerSideProps 簡潔。
SSG(Static Site Generation)— 特殊處理
問題:SSG 在 build 時 預生成 HTML,根本沒有 request 概念,不能在 build 時驗證使用者。
只能在 client 加 guard:
// app/protected/page.tsx
'use client';
import { useEffect, useState } from 'react';
import { useRouter } from 'next/navigation';
export default function ProtectedPage() {
const router = useRouter();
const [authed, setAuthed] = useState(false);
useEffect(() => {
fetch('/api/verify-session') // ⭐ 由 API 驗證,不是 client 自己驗
.then((r) => r.ok ? setAuthed(true) : router.push('/login'))
.catch(() => router.push('/login'));
}, [router]);
if (!authed) return <Skeleton />;
return <div>Protected content</div>;
}
重點:client 不自己驗,打 server endpoint 驗。server 才能讀 httpOnly cookie 跟 JWT secret。
SSG 是否該保護?SSG 適合純公開內容(部落格、行銷頁、文件)。如果頁面需要保護,改用 SSR 或 ISR。硬要 SSG + client guard 是 anti-pattern。
ISR(Incremental Static Regeneration)— 介於 SSG 跟 SSR
類似 SSG,只是每隔一段時間 server 重新生成。保護策略跟 SSG 一樣(client guard + API 驗證)。
完整流程:從登入到保護路由
1. User 登入
client → POST /api/login { email, password }
server 驗證 → set httpOnly cookie 'session'
server → 200 OK
2. User 訪問 /admin
browser 自動帶 cookie
middleware.ts / Server Component 讀 cookie
驗證 OK → 顯示頁面
驗證失敗 → 302 /login
3. User 打 /api/admin/users(資料 endpoint)
browser 自動帶 cookie
API route 讀 cookie 驗證
通過 → 回資料
失敗 → 401 Unauthorized
每一層都驗證 — middleware + server component + API route 都要驗,因為駭客可以繞過任一層直接打另一層。
HOC 寫法(Pages Router 經典)
// hocs/withAuth.tsx
import { GetServerSideProps } from 'next';
import jwt from 'jsonwebtoken';
export function withAuth(getServerSidePropsFunc?: GetServerSideProps): GetServerSideProps {
return async (ctx) => {
const token = ctx.req.cookies['session'];
if (!token) {
return { redirect: { destination: '/login', permanent: false } };
}
try {
const user = jwt.verify(token, process.env.JWT_SECRET!);
// 如果有原本的 getServerSideProps,呼叫它並合併 user
if (getServerSidePropsFunc) {
const result = await getServerSidePropsFunc(ctx);
if ('props' in result) {
return { props: { ...result.props, user } };
}
return result;
}
return { props: { user } };
} catch {
return { redirect: { destination: '/login', permanent: false } };
}
};
}
// 使用
import { withAuth } from '@/hocs/withAuth';
export const getServerSideProps = withAuth(async (ctx) => {
const data = await fetchData(ctx.params!.id);
return { props: { data } };
});
export default function AdminPage({ user, data }) {
return <div>...</div>;
}
包裝後的 getServerSideProps 自動處理 auth,業務邏輯只管自己的事。
NextAuth.js / Auth.js(主流方案)
2026 年首選 auth library:
npm install next-auth
// app/api/auth/[...nextauth]/route.ts
import NextAuth from 'next-auth';
import GoogleProvider from 'next-auth/providers/google';
const handler = NextAuth({
providers: [
GoogleProvider({
clientId: process.env.GOOGLE_CLIENT_ID!,
clientSecret: process.env.GOOGLE_CLIENT_SECRET!,
}),
],
});
export { handler as GET, handler as POST };
// 在 Server Component 取 session
import { getServerSession } from 'next-auth';
export default async function AdminPage() {
const session = await getServerSession();
if (!session) redirect('/login');
return <div>Welcome {session.user?.name}</div>;
}
NextAuth.js 處理 :OAuth、JWT、Session、Cookie security、CSRF 防禦。省 99% 自己寫的麻煩。
安全性對照總結
| 渲染模式 | Auth 在哪驗 | 安全性 | 適合 |
|---|---|---|---|
| SSR / Middleware | server | ⭐⭐⭐⭐⭐ | dashboard、私人頁 |
| Server Components(App Router) | server | ⭐⭐⭐⭐⭐ | 同上,2026 推薦 |
| ISR + client guard | client | ⭐⭐⭐ | 半公開頁(部分 user 限定) |
| SSG + client guard | client | ⭐⭐ | 不該保護 SSG |
| CSR only | client | ⭐ | 不該用於需保護的頁 |
黃金法則
- Auth 永遠在 server 驗證
- token 用 httpOnly cookie,不是 localStorage
- API endpoint 也要驗(不能信 client request)
- middleware 統一處理(App Router)
- 正式環境用 NextAuth 而不是自己寫
常見地雷
1. 信任 client 帶來的 token// ❌ API route 信 client decoded token const user = req.body.user; if (user.role === 'admin') { /* ... */ }駭客可以亂改 body。必須在 server 重新驗 JWT signature。
2. JWT alg: none 攻擊古老 JWT lib bug:接受 { "alg": "none" } 的 token,跳過 signature 驗證。確保 lib 強制 algorithm:
jwt.verify(token, secret, { algorithms: ['HS256'] }); // ⭐ 必加
3. CSRF 沒擋用 cookie auth 的 API 沒設 SameSite:Set-Cookie: session=xxx; HttpOnly; Secure; SameSite=Strict沒 SameSite 等於 允許其他網站偽造 request。
4. 在 Server Component 用 client-only auth// ❌ Server Component 不能用 useAuth() hook export default function Page() { const { user } = useAuth(); // 報錯 }Server Component 用
getServerSession()/ cookies(),不用 hook。