身份驗證 (Authentication)

要在 Next.js 中實作身份驗證,您需要先熟悉三個基礎概念:

本頁將示範如何使用 Next.js 功能來實作常見的身份驗證、授權和會話管理模式,讓您可以根據應用需求選擇最佳解決方案。

身份驗證 (Authentication)

身份驗證用於確認使用者身份。當使用者透過使用者名稱和密碼或 Google 等服務登入時,即進行此過程。其目的是確保使用者確實是其所聲稱的身份,保護使用者資料和應用程式免於未授權存取或詐騙活動。

身份驗證策略

現代網頁應用程式常用的身份驗證策略包括:

  1. OAuth/OpenID Connect (OIDC):允許第三方存取而不需分享使用者憑證。適用於社群媒體登入和單一登入 (SSO) 解決方案。OpenID Connect 增加了身份層。
  2. 基於憑證的登入 (Email + Password):網頁應用程式的標準選擇,使用者透過電子郵件和密碼登入。熟悉且易於實作,但需針對釣魚等威脅採取強健的安全措施。
  3. 無密碼/基於令牌的身份驗證 (Token-based authentication):使用電子郵件魔法連結或簡訊一次性代碼實現安全、無密碼的存取。因其便利性和增強的安全性而流行,可減少密碼疲勞。其限制在於依賴使用者的電子郵件或電話可用性。
  4. 通行金鑰/WebAuthn (Passkeys/WebAuthn):使用每個網站獨有的加密憑證,提供高安全性以防釣魚。安全但較新,此策略可能難以實作。

選擇身份驗證策略應符合應用程式的特定需求、使用者介面考量和安全目標。

實作身份驗證

本節將探討如何為網頁應用程式添加基本的電子郵件-密碼身份驗證。雖然此方法提供基礎安全級別,但考慮使用 OAuth 或無密碼登入等進階選項可增強對常見安全威脅的防護。我們將討論的身份驗證流程如下:

  1. 使用者透過登入表單提交憑證。
  2. 表單發送請求,由 API 路由處理。
  3. 驗證成功後,流程完成,表示使用者身份驗證成功。
  4. 若驗證失敗,則顯示錯誤訊息。

考慮一個登入表單,使用者可輸入憑證:

import { FormEvent } from 'react'
import { useRouter } from 'next/router'

export default function LoginPage() {
  const router = useRouter()

  async function handleSubmit(event: FormEvent<HTMLFormElement>) {
    event.preventDefault()

    const formData = new FormData(event.currentTarget)
    const email = formData.get('email')
    const password = formData.get('password')

    const response = await fetch('/api/auth/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ email, password }),
    })

    if (response.ok) {
      router.push('/profile')
    } else {
      // 處理錯誤
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input type="email" name="email" placeholder="Email" required />
      <input type="password" name="password" placeholder="Password" required />
      <button type="submit">Login</button>
    </form>
  )
}

上述表單有兩個輸入欄位,用於捕捉使用者的電子郵件和密碼。提交時,會觸發一個函式,向 API 路由 (/api/auth/login) 發送 POST 請求。

然後您可以在 API 路由中呼叫您的身份驗證提供者 API 來處理身份驗證:

import { NextApiRequest, NextApiResponse } from 'next'
import { signIn } from '@/auth'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const { email, password } = req.body
    await signIn('credentials', { email, password })

    res.status(200).json({ success: true })
  } catch (error) {
    if (error.type === 'CredentialsSignin') {
      res.status(401).json({ error: '無效的憑證。' })
    } else {
      res.status(500).json({ error: '發生錯誤。' })
    }
  }
}

在此程式碼中,signIn 方法會根據儲存的使用者資料檢查憑證。 身份驗證提供者處理憑證後,有兩種可能的結果:

  • 身份驗證成功:表示登入成功。隨後可啟動進一步操作,例如存取受保護路由和取得使用者資訊。
  • 身份驗證失敗:若憑證不正確或遇到錯誤,函式會回傳相應的錯誤訊息以表示身份驗證失敗。

為了在 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$).*)'],
}

此範例使用 Response.redirect 在請求管線早期處理重新導向,使其高效並集中存取控制。

身份驗證成功後,根據使用者角色管理導航非常重要。例如,管理員使用者可能會被重新導向至管理儀表板,而一般使用者則會被導向至其他頁面。這對於角色特定的體驗和條件式導航(例如在需要時提示使用者完成個人資料)非常重要。

設定授權時,重要的是確保主要安全檢查發生在應用程式存取或變更資料的地方。雖然中介軟體可用於初始驗證,但不應成為保護資料的唯一防線。大部分安全檢查應在資料存取層 (DAL) 中執行。

保護 API 路由

Next.js 中的 API 路由對於處理伺服器端邏輯和資料管理至關重要。保護這些路由的安全,確保只有授權使用者能存取特定功能,是相當關鍵的。這通常涉及驗證使用者的認證狀態及其基於角色的權限。

以下是一個保護 API 路由的範例:

import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  const session = await getSession(req)

  // 檢查使用者是否已認證
  if (!session) {
    res.status(401).json({
      error: '使用者未認證',
    })
    return
  }

  // 檢查使用者是否具有 'admin' 角色
  if (session.user.role !== 'admin') {
    res.status(401).json({
      error: '未授權存取:使用者無管理員權限。',
    })
    return
  }

  // 為授權使用者繼續執行路由
  // ... API 路由的實作
}

此範例展示了一個具有雙層安全檢查的 API 路由,用於認證和授權。它首先檢查是否有有效的會話,然後驗證登入使用者是否為 'admin'。這種方法確保了只有經過認證和授權的使用者才能存取,維持了請求處理的強健安全性。

最佳實踐

  • 安全的會話管理:優先保護會話資料的安全,防止未授權存取和資料外洩。使用加密和安全儲存實踐。
  • 動態角色管理:使用彈性的使用者角色系統,輕鬆適應權限和角色的變更,避免硬編碼角色。
  • 安全優先的思維:在所有授權邏輯中,優先考慮安全性以保護使用者資料並維護應用程式的完整性。這包括徹底的測試和考慮潛在的安全漏洞。

會話管理

會話管理涉及追蹤和管理使用者與應用程式的互動,確保其認證狀態在應用程式的不同部分之間持續存在。

這避免了重複登入的需求,同時提升了安全性和使用者便利性。會話管理主要有兩種方法:基於 Cookie 的會話和資料庫會話。

🎥 觀看: 了解更多關於基於 Cookie 的會話和 Next.js 認證 → YouTube (11 分鐘)

基於 Cookie 的會話透過將加密的會話資訊直接儲存在瀏覽器 Cookie 中來管理使用者資料。使用者登入後,這些加密資料會儲存在 Cookie 中。後續的每個伺服器請求都會包含此 Cookie,減少了重複伺服器查詢的需求,提升了客戶端效率。

然而,此方法需要謹慎加密以保護敏感資料,因為 Cookie 容易受到客戶端安全風險的影響。加密 Cookie 中的會話資料是保護使用者資訊免受未授權存取的關鍵。這確保即使 Cookie 被竊取,其中的資料仍無法被讀取。

此外,雖然單個 Cookie 的大小有限(通常約 4KB),但像 Cookie 分塊這樣的技術可以透過將大型會話資料分割成多個 Cookie 來克服此限制。

在 Next.js 專案中設定 Cookie 可能如下所示:

在伺服器上設定 Cookie:

import { serialize } from 'cookie'
import type { NextApiRequest, NextApiResponse } from 'next'

export default function handler(req: NextApiRequest, res: NextApiResponse) {
  const sessionData = req.body
  const encryptedSessionData = encrypt(sessionData)

  const cookie = serialize('session', encryptedSessionData, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    maxAge: 60 * 60 * 24 * 7, // 一週
    path: '/',
  })
  res.setHeader('Set-Cookie', cookie)
  res.status(200).json({ message: '成功設定 Cookie!' })
}

資料庫會話

資料庫會話管理涉及將會話資料儲存在伺服器上,使用者的瀏覽器僅接收會話 ID。此 ID 參照儲存在伺服器端的會話資料,而不包含資料本身。這種方法提升了安全性,因為它將敏感的會話資料遠離客戶端環境,降低了暴露於客戶端攻擊的風險。資料庫會話也更具有擴展性,能適應更大的資料儲存需求。

然而,這種方法有其權衡。由於每次使用者互動都需要進行資料庫查詢,可能會增加效能開銷。像會話資料快取這樣的策略可以幫助緩解此問題。此外,依賴資料庫意味著會話管理的可靠性取決於資料庫的效能和可用性。

以下是一個在 Next.js 應用中實作資料庫會話的簡化範例:

在伺服器上建立會話

import db from '../../lib/db'
import { NextApiRequest, NextApiResponse } from 'next'

export default async function handler(
  req: NextApiRequest,
  res: NextApiResponse
) {
  try {
    const user = req.body
    const sessionId = generateSessionId()
    await db.insertSession({
      sessionId,
      userId: user.id,
      createdAt: new Date(),
    })

    res.status(200).json({ sessionId })
  } catch (error) {
    res.status(500).json({ error: '內部伺服器錯誤' })
  }
}

在 Next.js 中選擇會話管理方式

在 Next.js 中選擇基於 Cookie 或基於資料庫的會話管理,取決於應用程式的需求。基於 Cookie 的會話較為簡單,適合伺服器負載較低的小型應用程式,但安全性可能較低。基於資料庫的會話雖然較複雜,但能提供更好的安全性和擴展性,是大型且對資料敏感的應用程式的理想選擇。

透過如 NextAuth.js 這樣的身份驗證解決方案,會話管理會更加高效,無論是使用 Cookie 還是資料庫儲存。這種自動化簡化了開發流程,但了解所選解決方案使用的會話管理方法非常重要。確保它符合應用程式的安全性和效能需求。

無論選擇哪種方式,都應在會話管理策略中優先考慮安全性。對於基於 Cookie 的會話,使用安全且僅限 HTTP 的 Cookie 對於保護會話資料至關重要。對於基於資料庫的會話,定期備份和安全處理會話資料是必不可少的。在兩種方法中,實作會話過期和清理機制對於防止未經授權的存取以及維護應用程式的效能和可靠性都非常重要。

範例

以下是與 Next.js 相容的身份驗證解決方案,請參考以下快速入門指南,了解如何在 Next.js 應用程式中配置它們:

延伸閱讀

若要繼續學習有關身份驗證和安全的知識,請查看以下資源: