身份驗證 (Authentication)
要在 Next.js 中實作身份驗證,您需要先熟悉三個基礎概念:
- 身份驗證 (Authentication) 驗證使用者是否為其所聲稱的身份。要求使用者透過使用者名稱和密碼等方式證明身份。
- 會話管理 (Session Management) 追蹤使用者在多個請求間的狀態(例如登入狀態)。
- 授權 (Authorization) 決定使用者可以存取應用程式的哪些部分。
本頁將示範如何使用 Next.js 功能來實作常見的身份驗證、授權和會話管理模式,讓您可以根據應用需求選擇最佳解決方案。
身份驗證 (Authentication)
身份驗證用於確認使用者身份。當使用者透過使用者名稱和密碼或 Google 等服務登入時,即進行此過程。其目的是確保使用者確實是其所聲稱的身份,保護使用者資料和應用程式免於未授權存取或詐騙活動。
身份驗證策略
現代網頁應用程式常用的身份驗證策略包括:
- OAuth/OpenID Connect (OIDC):允許第三方存取而不需分享使用者憑證。適用於社群媒體登入和單一登入 (SSO) 解決方案。OpenID Connect 增加了身份層。
- 基於憑證的登入 (Email + Password):網頁應用程式的標準選擇,使用者透過電子郵件和密碼登入。熟悉且易於實作,但需針對釣魚等威脅採取強健的安全措施。
- 無密碼/基於令牌的身份驗證 (Token-based authentication):使用電子郵件魔法連結或簡訊一次性代碼實現安全、無密碼的存取。因其便利性和增強的安全性而流行,可減少密碼疲勞。其限制在於依賴使用者的電子郵件或電話可用性。
- 通行金鑰/WebAuthn (Passkeys/WebAuthn):使用每個網站獨有的加密憑證,提供高安全性以防釣魚。安全但較新,此策略可能難以實作。
選擇身份驗證策略應符合應用程式的特定需求、使用者介面考量和安全目標。
實作身份驗證
本節將探討如何為網頁應用程式添加基本的電子郵件-密碼身份驗證。雖然此方法提供基礎安全級別,但考慮使用 OAuth 或無密碼登入等進階選項可增強對常見安全威脅的防護。我們將討論的身份驗證流程如下:
- 使用者透過登入表單提交憑證。
- 表單呼叫伺服器動作 (Server Action)。
- 驗證成功後,流程完成,表示使用者身份驗證成功。
- 若驗證失敗,則顯示錯誤訊息。
考慮一個登入表單,使用者可輸入憑證:
import { authenticate } from '@/app/lib/actions'
export default function Page() {
return (
<form action={authenticate}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
)
}
import { authenticate } from '@/app/lib/actions'
export default function Page() {
return (
<form action={authenticate}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<button type="submit">Login</button>
</form>
)
}
上述表單有兩個輸入欄位,用於捕捉使用者的電子郵件和密碼。提交時,會呼叫 authenticate
伺服器動作。
然後您可以在伺服器動作中呼叫您的身份驗證提供者 API 來處理身份驗證:
'use server'
import { signIn } from '@/auth'
export async function authenticate(_currentState: unknown, formData: FormData) {
try {
await signIn('credentials', formData)
} catch (error) {
if (error) {
switch (error.type) {
case 'CredentialsSignin':
return '無效的憑證。'
default:
return '發生錯誤。'
}
}
throw error
}
}
'use server'
import { signIn } from '@/auth'
export async function authenticate(_currentState, formData) {
try {
await signIn('credentials', formData)
} catch (error) {
if (error) {
switch (error.type) {
case 'CredentialsSignin':
return '無效的憑證。'
default:
return '發生錯誤。'
}
}
throw error
}
}
在此程式碼中,signIn
方法會根據儲存的使用者資料檢查憑證。
身份驗證提供者處理憑證後,有兩種可能的結果:
- 身份驗證成功:表示登入成功。隨後可啟動進一步操作,例如存取受保護路由和取得使用者資訊。
- 身份驗證失敗:若憑證不正確或遇到錯誤,函式會回傳相應的錯誤訊息以表示身份驗證失敗。
最後,在您的 login-form.tsx
元件中,可以使用 React 的 useFormState
來呼叫伺服器動作並處理表單錯誤,並使用 useFormStatus
來處理表單的待處理狀態:
'use client'
import { authenticate } from '@/app/lib/actions'
import { useFormState, useFormStatus } from 'react-dom'
export default function Page() {
const [errorMessage, dispatch] = useFormState(authenticate, undefined)
return (
<form action={dispatch}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<div>{errorMessage && <p>{errorMessage}</p>}</div>
<LoginButton />
</form>
)
}
function LoginButton() {
const { pending } = useFormStatus()
const handleClick = (event) => {
if (pending) {
event.preventDefault()
}
}
return (
<button aria-disabled={pending} type="submit" onClick={handleClick}>
Login
</button>
)
}
'use client'
import { authenticate } from '@/app/lib/actions'
import { useFormState, useFormStatus } from 'react-dom'
export default function Page() {
const [errorMessage, dispatch] = useFormState(authenticate, undefined)
return (
<form action={dispatch}>
<input type="email" name="email" placeholder="Email" required />
<input type="password" name="password" placeholder="Password" required />
<div>{errorMessage && <p>{errorMessage}</p>}</div>
<LoginButton />
</form>
)
}
function LoginButton() {
const { pending } = useFormStatus()
const handleClick = (event) => {
if (pending) {
event.preventDefault()
}
}
return (
<button aria-disabled={pending} type="submit" onClick={handleClick}>
Login
</button>
)
}
為了在 Next.js 專案中建立更流暢的身份驗證設定,特別是提供多種登入方法時,可以考慮使用全面的身份驗證解決方案。
授權 (Authorization)
一旦使用者通過身份驗證,您需要確保使用者被允許存取特定路由,並執行諸如使用伺服器動作變更資料和呼叫路由處理器等操作。
使用中介軟體 (Middleware) 保護路由
Next.js 中的中介軟體 (Middleware) 可幫助您控制誰能存取網站的不同部分。這對於保護使用者儀表板等區域同時讓行銷頁面等頁面公開非常重要。建議在所有路由上應用中介軟體,並為公開存取指定例外。
以下是在 Next.js 中實作身份驗證中介軟體的方法:
設定中介軟體:
- 在專案根目錄中建立
middleware.ts
或.js
檔案。 - 包含授權使用者存取的邏輯,例如檢查身份驗證令牌。
定義受保護路由:
- 並非所有路由都需要授權。使用中介軟體中的
matcher
選項指定不需要授權檢查的任何路由。
中介軟體邏輯:
- 編寫邏輯以驗證使用者是否通過身份驗證。檢查使用者角色或權限以進行路由授權。
處理未授權存取:
- 將未授權使用者重新導向至登入頁面或錯誤頁面。
中介軟體檔案範例:
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
const currentUser = request.cookies.get('currentUser')?.value
if (currentUser && !request.nextUrl.pathname.startsWith('/dashboard')) {
return Response.redirect(new URL('/dashboard', request.url))
}
if (!currentUser && !request.nextUrl.pathname.startsWith('/login')) {
return Response.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
export function middleware(request) {
const currentUser = request.cookies.get('currentUser')?.value
if (currentUser && !request.nextUrl.pathname.startsWith('/dashboard')) {
return Response.redirect(new URL('/dashboard', request.url))
}
if (!currentUser && !request.nextUrl.pathname.startsWith('/login')) {
return Response.redirect(new URL('/login', request.url))
}
}
export const config = {
matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}
此範例使用 Response.redirect
在請求管線早期處理重新導向,使其高效並集中存取控制。
對於特定的重新導向需求,可以在伺服器元件、路由處理器和伺服器動作中使用 redirect
函式以提供更多控制。這對於基於角色的導航或情境敏感的場景非常有用。
import { redirect } from 'next/navigation'
export default function Page() {
// 判斷是否需要重新導向的邏輯
const accessDenied = true
if (accessDenied) {
redirect('/login')
}
// 定義其他路由和邏輯
}
import { redirect } from 'next/navigation'
export default function Page() {
// 判斷是否需要重新導向的邏輯
const accessDenied = true
if (accessDenied) {
redirect('/login')
}
// 定義其他路由和邏輯
}
身份驗證成功後,根據使用者角色管理導航非常重要。例如,管理員使用者可能會被重新導向至管理儀表板,而一般使用者則會被導向至其他頁面。這對於角色特定的體驗和條件式導航(例如在需要時提示使用者完成個人資料)非常重要。
設定授權時,重要的是確保主要安全檢查發生在應用程式存取或變更資料的地方。雖然中介軟體可用於初始驗證,但不應成為保護資料的唯一防線。大部分安全檢查應在資料存取層 (DAL) 中執行。
這種方法,在這篇安全性部落格中有強調,提倡將所有資料存取集中在專用的 DAL (資料存取層) 中。此策略確保一致的資料存取,減少授權錯誤,並簡化維護工作。為了確保全面的安全性,請考慮以下關鍵領域:
- 伺服器動作 (Server Actions):在伺服器端流程中實作安全性檢查,特別是針對敏感操作。
- 路由處理器 (Route Handlers):管理傳入請求時應採取安全措施,確保只有授權使用者能存取。
- 資料存取層 (DAL):直接與資料庫互動,對於驗證和授權資料交易至關重要。在 DAL 中執行關鍵檢查,以在資料最關鍵的互動點(存取或修改)確保安全。
有關保護 DAL 的詳細指南,包括範例程式碼片段和進階安全實踐,請參閱我們安全性指南中的資料存取層章節。
保護伺服器動作
對待伺服器動作時,應與公開 API 端點採取相同的安全考量。驗證每個動作的使用者授權至關重要。在伺服器動作中實作檢查以確定使用者權限,例如限制某些動作僅供管理員使用者執行。
在以下範例中,我們在允許動作繼續之前檢查使用者的角色:
'use server'
// ...
export async function serverAction() {
const session = await getSession()
const userRole = session?.user?.role
// 檢查使用者是否有權執行該動作
if (userRole !== 'admin') {
throw new Error('未授權存取:使用者無管理員權限。')
}
// 為授權使用者繼續執行動作
// ... 動作的實作
}
'use server'
// ...
export async function serverAction() {
const session = await getSession()
const userRole = session?.user?.role
// 檢查使用者是否有權執行該動作
if (userRole !== 'admin') {
throw new Error('未授權存取:使用者無管理員權限。')
}
// 為授權使用者繼續執行動作
// ... 動作的實作
}
保護路由處理器
Next.js 中的路由處理器在管理傳入請求方面扮演重要角色。與伺服器動作一樣,它們應受到保護,確保只有授權使用者能存取特定功能。這通常涉及驗證使用者的認證狀態及其權限。
以下是一個保護路由處理器的範例:
export async function GET() {
// 使用者認證和角色驗證
const session = await getSession()
// 檢查使用者是否已認證
if (!session) {
return new Response(null, { status: 401 }) // 使用者未認證
}
// 檢查使用者是否具有 'admin' 角色
if (session.user.role !== 'admin') {
return new Response(null, { status: 403 }) // 使用者已認證但無權限
}
// 為授權使用者取得資料
}
export async function GET() {
// 使用者認證和角色驗證
const session = await getSession()
// 檢查使用者是否已認證
if (!session) {
return new Response(null, { status: 401 }) // 使用者未認證
}
// 檢查使用者是否具有 'admin' 角色
if (session.user.role !== 'admin') {
return new Response(null, { status: 403 }) // 使用者已認證但無權限
}
// 為授權使用者取得資料
}
此範例展示了一個具有雙層安全檢查的路由處理器,用於認證和授權。它首先檢查是否有有效的會話,然後驗證登入使用者是否為 'admin'。這種方法確保了只有經過認證和授權的使用者才能存取,維持了請求處理的強健安全性。
使用伺服器元件進行授權
Next.js 中的伺服器元件專為伺服器端執行而設計,為整合複雜邏輯(如授權)提供了安全的環境。它們能直接存取後端資源,優化了資料密集型任務的效能,並增強了敏感操作的安全性。
在伺服器元件中,常見的做法是根據使用者角色條件式渲染 UI 元素。這種方法透過確保使用者只能存取他們有權查看的內容,提升了使用者體驗和安全性。
範例:
export default async function Dashboard() {
const session = await getSession()
const userRole = session?.user?.role // 假設 'role' 是會話物件的一部分
if (userRole === 'admin') {
return <AdminDashboard /> // 管理員使用者的元件
} else if (userRole === 'user') {
return <UserDashboard /> // 一般使用者的元件
} else {
return <AccessDenied /> // 未授權存取時顯示的元件
}
}
export default function Dashboard() {
const session = await getSession()
const userRole = session?.user?.role // 假設 'role' 是會話物件的一部分
if (userRole === 'admin') {
return <AdminDashboard /> // 管理員使用者的元件
} else if (userRole === 'user') {
return <UserDashboard /> // 一般使用者的元件
} else {
return <AccessDenied /> // 未授權存取時顯示的元件
}
}
在此範例中,Dashboard 元件根據 'admin'、'user' 和未授權角色渲染不同的 UI。這種模式確保每位使用者僅與符合其角色的元件互動,同時提升了安全性和使用者體驗。
最佳實踐
- 安全的會話管理:優先保護會話資料的安全,防止未授權存取和資料外洩。使用加密和安全儲存實踐。
- 動態角色管理:使用彈性的使用者角色系統,輕鬆適應權限和角色的變更,避免硬編碼角色。
- 安全優先的思維:在所有授權邏輯中,優先考慮安全性以保護使用者資料並維護應用程式的完整性。這包括徹底的測試和考慮潛在的安全漏洞。
會話管理
會話管理涉及追蹤和管理使用者與應用程式的互動,確保其認證狀態在應用程式的不同部分之間持續存在。
這避免了重複登入的需求,同時提升了安全性和使用者便利性。會話管理主要有兩種方法:基於 Cookie 的會話和資料庫會話。
基於 Cookie 的會話
🎥 觀看: 了解更多關於基於 Cookie 的會話和 Next.js 認證 → YouTube (11 分鐘)。
基於 Cookie 的會話透過將加密的會話資訊直接儲存在瀏覽器 Cookie 中來管理使用者資料。使用者登入後,這些加密資料會儲存在 Cookie 中。後續的每個伺服器請求都會包含此 Cookie,減少了重複伺服器查詢的需求,提升了客戶端效率。
然而,此方法需要謹慎加密以保護敏感資料,因為 Cookie 容易受到客戶端安全風險的影響。加密 Cookie 中的會話資料是保護使用者資訊免受未授權存取的關鍵。這確保即使 Cookie 被竊取,其中的資料仍無法被讀取。
此外,雖然單個 Cookie 的大小有限(通常約 4KB),但像 Cookie 分塊這樣的技術可以透過將大型會話資料分割成多個 Cookie 來克服此限制。
在 Next.js 專案中設定 Cookie 可能如下所示:
在伺服器上設定 Cookie:
'use server'
import { cookies } from 'next/headers'
export async function handleLogin(sessionData) {
const encryptedSessionData = encrypt(sessionData) // 加密你的會話資料
cookies().set('session', encryptedSessionData, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 一週
path: '/',
})
// 設定 Cookie 後重新導向或處理回應
}
'use server'
import { cookies } from 'next/headers'
export async function handleLogin(sessionData) {
const encryptedSessionData = encrypt(sessionData) // 加密你的會話資料
cookies().set('session', encryptedSessionData, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
maxAge: 60 * 60 * 24 * 7, // 一週
path: '/',
})
// 設定 Cookie 後重新導向或處理回應
}
在伺服器元件中存取儲存在 Cookie 中的會話資料:
import { cookies } from 'next/headers'
export async function getSessionData(req) {
const encryptedSessionData = cookies().get('session')?.value
return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null
}
import { cookies } from 'next/headers'
export async function getSessionData(req) {
const encryptedSessionData = cookies().get('session')?.value
return encryptedSessionData ? JSON.parse(decrypt(encryptedSessionData)) : null
}
資料庫會話
資料庫會話管理涉及將會話資料儲存在伺服器上,使用者的瀏覽器僅接收會話 ID。此 ID 參照儲存在伺服器端的會話資料,而不包含資料本身。這種方法提升了安全性,因為它將敏感的會話資料遠離客戶端環境,降低了暴露於客戶端攻擊的風險。資料庫會話也更具有擴展性,能適應更大的資料儲存需求。
然而,這種方法有其權衡。由於每次使用者互動都需要進行資料庫查詢,可能會增加效能開銷。像會話資料快取這樣的策略可以幫助緩解此問題。此外,依賴資料庫意味著會話管理的可靠性取決於資料庫的效能和可用性。
以下是一個在 Next.js 應用中實作資料庫會話的簡化範例:
在伺服器上建立會話:
import db from './lib/db'
export async function createSession(user) {
const sessionId = generateSessionId() // 產生唯一的會話 ID
await db.insertSession({ sessionId, userId: user.id, createdAt: new Date() })
return sessionId
}
在中間件或伺服器端邏輯中取得會話:
import { cookies } from 'next/headers'
import db from './lib/db'
export async function getSession() {
const sessionId = cookies().get('sessionId')?.value
return sessionId ? await db.findSession(sessionId) : null
}
在 Next.js 中選擇會話管理方式
在 Next.js 中選擇基於 Cookie 或基於資料庫的會話管理,取決於應用程式的需求。基於 Cookie 的會話較為簡單,適合伺服器負載較低的小型應用程式,但安全性可能較低。基於資料庫的會話雖然較複雜,但能提供更好的安全性和擴展性,是大型且對資料敏感的應用程式的理想選擇。
透過如 NextAuth.js 這樣的身份驗證解決方案,會話管理會更加高效,無論是使用 Cookie 還是資料庫儲存。這種自動化簡化了開發流程,但了解所選解決方案使用的會話管理方法非常重要。確保它符合應用程式的安全性和效能需求。
無論選擇哪種方式,都應在會話管理策略中優先考慮安全性。對於基於 Cookie 的會話,使用安全且僅限 HTTP 的 Cookie 對於保護會話資料至關重要。對於基於資料庫的會話,定期備份和安全處理會話資料是必不可少的。在兩種方法中,實作會話過期和清理機制對於防止未經授權的存取以及維護應用程式的效能和可靠性都非常重要。
範例
以下是與 Next.js 相容的身份驗證解決方案,請參考以下快速入門指南,了解如何在 Next.js 應用程式中配置它們:
延伸閱讀
若要繼續學習有關身份驗證和安全的知識,請查看以下資源: