Supabase MFA 與 SSR:多因子驗證與伺服器端渲染 | Supabase 完整教學

2026/10/12
Supabase MFA 與 SSR:多因子驗證與伺服器端渲染 | Supabase 完整教學

前面幾篇我們把「怎麼進門」講完了——email 密碼、OAuth、Magic Link 與 OTP,全都通往同一組 session。這一篇談「進門之後怎麼守得更穩」:先幫登入加上第二道防線,也就是 MFA(多因子驗證)/TOTP,用 enroll/challenge/verify 三步把 session 從 AAL1 升級成 AAL2,再用 RLS 依 aal 限制高敏感操作;接著處理 SSR——用 @supabase/ssr 以 cookie 保管 session,靠 middleware 在每個請求刷新 token,伺服器端一律用 getUser() 而非 getSession() 做授權。這是整個 Auth 段的收尾。

前言

一句話定義本篇兩個主題:MFA(Multi-Factor Authentication,多因子驗證)是在密碼/社群登入之後,多加一道「你手上要有那支驗證器」的關卡,把 session 從 aal1 升級到 aal2;SSR(Server-Side Rendering,伺服器端渲染)則是在 Next.js 這類伺服器會先渲染頁面的框架裡,改用 HTTP cookie 而非瀏覽器的 localStorage 來保管 session,並用 middleware 在每個請求刷新 token。 這兩件事看似無關,卻是「登入之後把安全與架構做對」的兩塊拼圖,所以放在同一篇收尾。

延續整個 Auth 段反覆強調的那句話:無論用哪種方式登入,最終產物都是同一組 session(access token + refresh token)。MFA 沒有換掉這組 session,它只是替 session 貼上一個「已完成第二因子」的標記(aal 這個 claim 從 aal1 變 aal2);SSR 也沒有換掉這組 session,它只是換了「session 存在哪、由誰刷新」的位置——從瀏覽器 localStorage 搬到伺服器讀得到的 cookie。抓住「session 沒變、只是加標記或換位置」這個核心,兩個主題就都好懂了。

用一個類比幫你抓住全貌:MFA 就像高級住宅的「門禁 + 保險箱」兩層。 你刷卡進了大門(第一因子:密碼/OAuth,這時是 aal1),但要動用保險箱裡的貴重物品(高敏感操作,例如改密碼、看帳務),還得再輸入一組只有你手機動態產生的密碼(第二因子:TOTP,通過後升級 aal2)。而 SSR 的 cookie + middleware 就像大樓的「門禁系統值班室」——每個住戶進出(每個 HTTP 請求)都會經過值班室(middleware)刷一次卡、必要時換發一張快過期的新卡(刷新 token),確保住戶手上的通行證永遠有效,而不是讓過期卡把人擋在外面。

本篇你會學到:

  • MFA 三步驟:supabase.auth.mfa.enroll/challenge/verify 各自在做什麼,如何把 session 升級到 aal2
  • AAL1 vs AAL2:aal claim 的意義,以及用 getAuthenticatorAssuranceLevel() 判斷「該不該要求 MFA」
  • RLS 依 AAL 限制:如何在 Policy 裡用 (select auth.jwt() ->> 'aal') = 'aal2' 保護高敏感資料(呼應 009–011 篇的 RLS)
  • @supabase/ssr 三件套:createBrowserClient、createServerClient + cookie、middleware 刷新 token
  • 授權底線:伺服器端為什麼一律用 getUser(),getSession() 為什麼絕不能拿來做授權判斷

核心概念

這一篇有兩條主線:一條是 MFA(AAL 升級),一條是 SSR(cookie-based session)。先把兩條線各自的心智模型建立起來。

一、MFA 與 AAL:session 的「安全等級」

MFA 的核心不是「多一個密碼」,而是「多一個你必須實體握有的東西」。密碼是「你知道的東西」(可能被猜、被外洩),TOTP 驗證器則是「你擁有的東西」(存在你手機的 Authenticator App 裡,攻擊者就算拿到密碼也生不出那組 30 秒變一次的動態碼)。兩者疊加,帳號被盜的門檻大幅提高。

Supabase 用 JWT 裡的一個 claim——aal(Authenticator Assurance Level,驗證保證等級)——來標記「這個 session 到底驗到多嚴」:

等級意義何時達到
aal1只完成第一因子密碼、Magic Link、OAuth 登入後的預設狀態
aal2已額外完成 MFA通過 TOTP/Phone MFA 的 verify 之後

關鍵觀念:登入完成的當下,session 只是 aal1。 就算使用者有註冊 MFA,剛登入時他仍停在 aal1——你必須在應用層「發現他該做 MFA」(用 getAuthenticatorAssuranceLevel() 比較 currentLevel 與 nextLevel),引導他完成 challenge + verify,session 才升級到 aal2。

TOTP 的三個動作對應兩個階段:

【綁定階段,只做一次】
  enroll   → Supabase 產生密鑰與 QR Code,使用者用 Authenticator App 掃描綁定
     │        (回傳 factorId、data.totp.qr_code)
     └─ 綁定完成後,馬上做一次 challenge + verify 確認綁定有效

【驗證階段,每次登入都要】
  challenge → 發起一次挑戰,產生 challengeId(「現在請證明你握有那支驗證器」)
     │
  verify    → 送出使用者從 App 讀到的 6 碼,核對成功 → session 升級 aal2

API 名稱以官方文件為準:本文採用 supabase.auth.mfa.enroll/challenge/verify/listFactors/getAuthenticatorAssuranceLevel。回傳欄位(如 data.totp.qr_code、data.id)在不同 SDK 版本可能略有差異,實作時請對照當前版本文件。

二、SSR:為什麼不能沿用 SPA 那一套

在純前端(SPA)情境,Supabase SDK 預設把 session 存在瀏覽器的 localStorage,SDK 自己會在背景刷新 token,一切都在瀏覽器裡發生。但一旦進到 SSR 框架(Next.js App Router、SvelteKit),三個限制冒出來:

  1. Server Component 讀不到 localStorage——它跑在伺服器,根本沒有瀏覽器的 localStorage。
  2. Server Component 不能寫 cookie——它可以「讀」cookie,但依 Next.js 的設計不能在渲染中「寫」cookie,所以 token 需要刷新時它寫不回去。
  3. 多個請求可能同時想刷新 token——需要一個統一的地方協調,避免競態。

解法是換一套儲存位置與刷新機制:用 @supabase/ssr 套件,把 session 存在 HTTP cookie,並用三個角色分工。

角色用什麼建立職責跑在哪
瀏覽器端 clientcreateBrowserClientClient Component 讀寫 session瀏覽器
伺服器端 clientcreateServerClient + cookies()Server Component/Action/Route Handler 讀 session伺服器
middlewarecreateServerClient + request/response cookie每個請求刷新 token、同步寫回 cookie伺服器(請求進頁面前)

這三者不是可有可無的重複,而是各補一個環境的能力缺口:Client Component 需要瀏覽器 client;Server Component 需要能讀 cookie 的 server client;而「刷新 token 並寫回 cookie」這件 Server Component 做不到的事,就交給 middleware。少任何一個,SSR 的 session 就會在某個環節斷掉。

三、授權判斷的鐵律:getUser() 而非 getSession()

這是 SSR 最重要、也最常被誤用的一條規則。三個看似都能「拿到使用者」的方法,安全性天差地別:

方法怎麼驗速度能不能拿來做授權
getSession()直接讀 cookie,不重新驗證最快絕對不可(cookie 是使用者可偽造的輸入)
getClaims()本地驗 JWT 簽名極快(無網路)可(middleware 快速把關)
getUser()向 Supabase Auth 伺服器求證較慢(有網路往返)可,且最保險

一句話:在伺服器端,凡是「決定要不要放行、要不要顯示敏感資料」的判斷,用 getUser()(或 getClaims());getSession() 只適合「我不打算用它做安全決策、只想快速看一眼」的場景。 為什麼?因為伺服器收到的 cookie 是「使用者送來的輸入」,getSession() 不驗證它就直接信,等於把大門鑰匙交給任何會偽造 cookie 的人。這一條記牢,SSR 的安全就守住了一大半。

四、關鍵術語小抄

術語意義
MFAMulti-Factor Authentication,多因子驗證
TOTPTime-based One-Time Password,時間基礎一次性密碼(Authenticator App 生成的 6 碼)
aalAuthenticator Assurance Level,JWT 中標記驗證等級的 claim
aal1 / aal2僅第一因子 / 已完成 MFA
enroll / challenge / verifyMFA 綁定 / 發起挑戰 / 核對驗證碼
@supabase/ssrSupabase 官方 SSR 套件,以 cookie 儲存 session
createBrowserClient / createServerClient瀏覽器端 / 伺服器端 client 建構函式
middlewareNext.js 在請求進頁面前執行的攔截層,負責刷新 token
getUser() / getSession()向伺服器求證 / 只讀 cookie(不可用於授權)

實作範例

以下用 supabase-js(TypeScript)示範,分兩大塊:先做 MFA/TOTP + 依 AAL 的 RLS,再做 @supabase/ssr 的 Next.js 三件套。API 名稱以官方文件為準,不確定的欄位我會標註。

一、MFA:enroll → challenge → verify(首次綁定)

首次幫使用者設定 TOTP,三步連著跑:enroll 拿 QR Code,使用者掃描後,challenge + verify 確認綁定成功。

'use client'
import { createClient } from '@/utils/supabase/client'

// 首次設定 MFA:綁定一支 TOTP 驗證器
async function enrollTotp() {
  const supabase = createClient()

  // 第一步:enroll —— 註冊一個 TOTP 因子,拿到 QR Code 與 factorId
  const { data: enrollData, error: enrollError } = await supabase.auth.mfa.enroll({
    factorType: 'totp',
    friendlyName: 'My Authenticator', // 可選:給這支因子取個好記的名字
  })
  if (enrollError) {
    console.error('enroll 失敗:', enrollError.message)
    return
  }

  // enrollData.totp.qr_code:SVG 格式 QR Code,顯示給使用者用 App 掃描
  // enrollData.totp.secret:手動輸入用的密鑰(掃不了 QR 時的備援)
  // enrollData.id:factorId,後續 challenge/verify 都要用
  const factorId = enrollData.id
  const qrCodeSvg = enrollData.totp.qr_code

  return { factorId, qrCodeSvg }
}

// 使用者掃描 QR、輸入 App 顯示的 6 碼後,確認綁定
async function confirmEnroll(factorId: string, code: string) {
  const supabase = createClient()

  // 第二步:challenge —— 發起一次挑戰,拿 challengeId
  const { data: challenge, error: challengeError } =
    await supabase.auth.mfa.challenge({ factorId })
  if (challengeError) {
    console.error('challenge 失敗:', challengeError.message)
    return
  }

  // 第三步:verify —— 送出使用者輸入的 6 碼核對,成功即升級 aal2
  const { data, error: verifyError } = await supabase.auth.mfa.verify({
    factorId,
    challengeId: challenge.id,
    code, // 使用者從 Authenticator App 讀到的 6 位數,例如 '123456'
  })
  if (verifyError) {
    console.error('驗證失敗:', verifyError.message)
    return
  }

  // 成功!此 session 現在是 aal2
  console.log('MFA 綁定完成,session 已升級 aal2')
  return data
}

三個要點:一、data.totp.qr_code 是 SVG,前端可以用 dangerouslySetInnerHTML 直接渲染成圖給使用者掃;二、data.totp.secret 是給「無法掃 QR」的使用者手動輸入的備援密鑰;三、verify 成功後 session 才升級 aal2——光 enroll 不會升級,一定要走完 verify。

二、登入後判斷「該不該要求 MFA」

使用者登入完成的當下只是 aal1。你要用 getAuthenticatorAssuranceLevel() 判斷「他有沒有註冊 MFA、目前該不該補做第二因子」:

async function checkAndPromptMfa() {
  const supabase = createClient()

  // currentLevel:目前已達到的等級;nextLevel:這個帳號「應該」達到的等級
  const { data, error } = await supabase.auth.mfa.getAuthenticatorAssuranceLevel()
  if (error) {
    console.error('讀取 AAL 失敗:', error.message)
    return
  }

  const { currentLevel, nextLevel } = data

  if (currentLevel === 'aal1' && nextLevel === 'aal2') {
    // 使用者已註冊 MFA,但這個 session 還沒完成第二因子 → 引導他做 challenge/verify
    return 'need_mfa'
  }

  if (currentLevel === 'aal2') {
    return 'mfa_done' // 已完成 MFA
  }

  return 'no_mfa' // 使用者根本沒註冊 MFA(nextLevel 也是 aal1)
}

判斷邏輯的白話翻譯:currentLevel 是「你現在驗到哪」,nextLevel 是「照你的設定該驗到哪」。 當 currentLevel === 'aal1' 而 nextLevel === 'aal2',代表「你有 MFA 但還沒做」——這時把使用者導到 MFA 輸入畫面,跑一次 challenge + verify(用上一段 confirmEnroll 裡的 challenge/verify,factorId 從 supabase.auth.mfa.listFactors() 取得已綁定的因子)。

三、用 RLS 依 AAL 限制高敏感操作

MFA 只在應用層攔還不夠——真正的防線要下沉到資料庫。呼應本系列 009–011 篇的 RLS:我們在 Policy 裡讀 JWT 的 aal claim,要求高敏感操作必須 aal2 才放行。這樣就算前端被繞過,資料庫這一層仍擋得住。

-- 場景:profiles 一般欄位人人可讀,但「更新」這種敏感操作要求已完成 MFA
-- restrictive policy 會「額外疊加」限制條件,與其他 permissive policy 做 AND

-- 只有完成 MFA(aal2)的使用者才能更新自己的 profile
create policy "更新 profile 需 aal2"
  on public.profiles
  as restrictive          -- 關鍵:restrictive 讓此條件與其他 policy 相 AND
  for update
  to authenticated
  using (
    (select auth.jwt() ->> 'aal') = 'aal2'
  );

-- 搭配一條 permissive policy 限制「只能改自己的」(呼應 009–011 篇)
create policy "使用者只能更新自己的 profile"
  on public.profiles
  for update
  to authenticated
  using ((select auth.uid()) = id)
  with check ((select auth.uid()) = id);

兩條 Policy 合起來的效果:使用者要更新自己的 profile,必須同時滿足「是自己的資料」(permissive)且「session 是 aal2」(restrictive)。這裡沿用 009–011 篇的兩個最佳實踐——用 (select ...) 包裝 auth.jwt()/auth.uid() 啟用結果快取、用 as restrictive 讓 MFA 條件與既有規則相 AND 而非相 OR。

auth.jwt() ->> 'aal' 讀的是當前請求 JWT 裡的 aal claim。若使用者只是 aal1 登入,這條 restrictive policy 會直接擋下 update;等他完成 MFA(session 換成帶 aal2 的新 token)後才放行。

四、@supabase/ssr:瀏覽器端 client

接下來切到 SSR。先安裝套件(npm install @supabase/supabase-js @supabase/ssr)。第一個角色——瀏覽器端 client,給 Client Component 用:

// utils/supabase/client.ts
import { createBrowserClient } from '@supabase/ssr'

export function createClient() {
  return createBrowserClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
  )
}

這支最單純——它跑在瀏覽器,能讀寫 document.cookie,SDK 自動處理 session。前面 MFA 那幾段用的 createClient() 就是它。

五、@supabase/ssr:伺服器端 client

第二個角色——伺服器端 client,給 Server Component、Server Action、Route Handler 用。關鍵是把 Next.js 的 cookies() 接進去,讓 SDK 從 cookie 讀 session:

// utils/supabase/server.ts
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'

export async function createClient() {
  const cookieStore = await cookies()

  return createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return cookieStore.getAll()
        },
        setAll(cookiesToSet) {
          try {
            cookiesToSet.forEach(({ name, value, options }) =>
              cookieStore.set(name, value, options)
            )
          } catch {
            // Server Component 不能寫 cookie,這裡會拋錯 → 交給 middleware 處理
            // 這個 catch 是「預期中」的,不要拿掉
          }
        },
      },
    }
  )
}

注意那個 catch 區塊:Server Component 在渲染時不能寫 cookie,setAll 會拋錯,我們吞掉它——因為真正負責寫回 cookie(刷新 token)的是 middleware,這裡吞錯是「預期行為」。

六、@supabase/ssr:middleware 刷新 token(最關鍵)

第三個角色——middleware,這是 SSR 最容易漏、也最關鍵的一塊。它在每個請求進入頁面前執行,負責:呼叫 getUser()(順帶觸發 token 刷新)、把新 token 同時寫回 request cookie(給 Server Component 讀)與 response cookie(送回瀏覽器):

// middleware.ts(放在專案根目錄或 src/)
import { createServerClient } from '@supabase/ssr'
import { NextResponse, type NextRequest } from 'next/server'

export async function middleware(request: NextRequest) {
  let supabaseResponse = NextResponse.next({ request })

  const supabase = createServerClient(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
    {
      cookies: {
        getAll() {
          return request.cookies.getAll()
        },
        setAll(cookiesToSet) {
          // 1. 更新 request cookies —— 讓後續 Server Components 讀到「新」token
          cookiesToSet.forEach(({ name, value }) =>
            request.cookies.set(name, value)
          )
          // 2. 重建 Response 並更新 response cookies —— 把新 token 送回瀏覽器
          supabaseResponse = NextResponse.next({ request })
          cookiesToSet.forEach(({ name, value, options }) =>
            supabaseResponse.cookies.set(name, value, options)
          )
        },
      },
    }
  )

  // 關鍵:呼叫 getUser() 會在 token 快過期時觸發刷新,並經由上面的 setAll 寫回 cookie
  // 這裡用 getUser() 而非 getSession()(授權判斷一律向伺服器求證)
  const {
    data: { user },
  } = await supabase.auth.getUser()

  // 保護路由:未登入者導向 /login
  if (
    !user &&
    !request.nextUrl.pathname.startsWith('/login') &&
    !request.nextUrl.pathname.startsWith('/auth')
  ) {
    const url = request.nextUrl.clone()
    url.pathname = '/login'
    return NextResponse.redirect(url)
  }

  return supabaseResponse
}

export const config = {
  matcher: [
    // 排除靜態資源與 _next 系統路徑,避免不必要的 middleware 執行
    '/((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*)',
  ],
}

setAll 那兩步是整段的靈魂:先改 request cookie(讓這次請求裡的 Server Component 讀到新 token),再改 response cookie(讓瀏覽器下次帶新 token 回來)——兩邊都寫,token 刷新才不會斷。少了任何一邊,都會出現「Server Component 讀到舊 token」或「瀏覽器沒收到新 token」的怪問題。

七、在 Server Component 用 getUser() 取用戶並套 RLS

最後,在受保護的 Server Component 裡取得使用者、並直接查資料(RLS 會自動依這個使用者的身份過濾):

// app/dashboard/page.tsx
import { redirect } from 'next/navigation'
import { createClient } from '@/utils/supabase/server'

export default async function DashboardPage() {
  const supabase = await createClient()

  // 授權判斷:一律用 getUser(),不要用 getSession()
  const {
    data: { user },
  } = await supabase.auth.getUser()

  if (!user) {
    redirect('/login')
  }

  // 這個 query 會自動帶上使用者的 JWT,RLS 依 auth.uid()、auth.jwt() 過濾
  // (呼應 009–011 篇:資料庫層才是真正的授權防線)
  const { data: posts } = await supabase
    .from('posts')
    .select('*') // RLS 保證只回傳這個使用者有權看的列

  return (
    <div>
      <p>歡迎,{user.email}</p>
      <ul>{posts?.map((p) => <li key={p.id}>{p.title}</li>)}</ul>
    </div>
  )
}

這裡把整個 Auth 段串起來了:middleware 保證 token 新鮮 → Server Component 用 getUser() 確認身份 → 帶著 JWT 查資料 → RLS(009–011 篇)在資料庫層做最終授權。前端、middleware、RLS 三層防護,缺一不可。

常見錯誤與最佳實踐

坑一:SSR 用 getSession() 做授權判斷。 這是 SSR 最危險、也最常見的錯。getSession() 只是「讀 cookie 有沒有一張 token」,不向伺服器驗證真偽;在伺服器端,cookie 是「使用者送來、可被偽造的輸入」,你若信它就放行,等於門戶大開。正確做法:伺服器端一切授權判斷(要不要放行、要不要顯示敏感資料)都用 getUser()(向 Supabase Auth 求證)或 getClaims()(本地驗簽名),永遠不用 getSession() 做安全決策。getSession() 只適合「純顯示、不涉及授權」的場景。

坑二:middleware 沒刷新 token,或 setAll 只寫一邊。 少了 middleware,access token(預設 1 小時)過期後沒人換發,使用者會莫名被登出;或者 middleware 有了,但 setAll 只更新了 response cookie 卻忘了同步 request cookie,導致「這次請求裡的 Server Component 仍讀到舊 token」。正確做法:middleware 一定要有,且 setAll 必須兩邊都寫——先 request.cookies.set(給本次的 Server Component),再重建 Response 並 supabaseResponse.cookies.set(給瀏覽器)。middleware 裡呼叫 getUser() 就是觸發刷新的動作,別省。

坑三:MFA 只在前端攔,沒下沉到 RLS 強制 aal2。 只在前端用 getAuthenticatorAssuranceLevel() 判斷「該不該要求 MFA」是不夠的——前端可以被繞過(直接打 API)。若高敏感操作沒在資料庫層要求 aal2,攻擊者用 aal1 的 token 一樣能改資料。正確做法:把「需要 MFA」的操作用 as restrictive 的 RLS Policy 綁死 (select auth.jwt() ->> 'aal') = 'aal2',讓資料庫成為最後防線(呼應 009–011 篇:真正的授權在 RLS)。前端引導 + 資料庫強制,雙保險。

坑四:以為 enroll 就完成綁定、忘了 verify。 enroll 只是「產生密鑰與 QR Code」,這時因子還在「未驗證」狀態;一定要讓使用者掃描後跑一次 challenge + verify,Supabase 才確認「這支驗證器真的綁對了」,session 也才升級 aal2。正確做法:綁定流程三步一氣呵成——enroll 拿 QR → 使用者掃描輸入碼 → challenge + verify 確認。少了 verify,使用者以為設好了,下次登入卻發現 MFA 沒生效。

最佳實踐總結:

  • 授權一律 getUser()/getClaims():伺服器端絕不用 getSession() 做安全決策
  • middleware 必備、setAll 兩邊都寫:request cookie(給 Server Component)+ response cookie(給瀏覽器)
  • MFA 下沉到 RLS:高敏感操作用 restrictive policy 綁 aal2,資料庫層才是真防線(呼應 009–011)
  • MFA 綁定走完 verify:enroll 只拿 QR,verify 才真正升級 aal2
  • 三個 client 各司其職:createBrowserClient(前端)、createServerClient(Server Component)、middleware(刷新 token)
  • API 名稱以官方文件為準:mfa.* 系列與 @supabase/ssr 的回傳欄位可能隨版本微調,實作時對照當前版本

小結

這是 Supabase 系列教學的第 023 篇,也是整個 Auth 段的收尾。上一篇《Magic Link 與 OTP》我們把「完全無密碼」的進門方式講完,讓使用者只留一個 email 或手機號就能登入。這一篇轉向「進門之後怎麼守得更穩」——先幫登入加第二道防線(MFA/TOTP),再把 session 在 SSR 環境安全地保管好。

核心觀念濃縮成一句話:MFA 用 enroll/challenge/verify 三步把 session 從 aal1 升級到 aal2,並用 as restrictive 的 RLS Policy 綁 (select auth.jwt() ->> 'aal') = 'aal2' 讓資料庫層強制第二因子;SSR 則用 @supabase/ssr 的三件套——createBrowserClient(前端)、createServerClient + cookie(Server Component)、middleware(每個請求刷新 token)——把 session 從 localStorage 搬到 cookie,而伺服器端一切授權判斷一律用 getUser()(或 getClaims()),getSession() 絕不碰授權。

  • MFA 三步:enroll(綁定拿 QR)→ challenge(發起挑戰)→ verify(核對升級 aal2)
  • AAL 判斷:getAuthenticatorAssuranceLevel() 比較 currentLevel 與 nextLevel,決定該不該補做 MFA
  • RLS 依 AAL:restrictive policy 綁 aal2,把 MFA 下沉到資料庫(呼應 009–011)
  • SSR 三件套:browser/server client + middleware,cookie-based session
  • 授權鐵律:伺服器端 getUser() 而非 getSession()

到這裡,整個 Auth 段——從 email 密碼、OAuth、Magic Link/OTP,到 MFA 與 SSR——全部走完了。你已經掌握「怎麼讓使用者進門」以及「進門之後怎麼守穩」的完整拼圖。下一篇《Storage 物件儲存入門》,我們要換一個全新主題:如何用 Supabase Storage 上傳、管理、存取檔案(圖片、影片、文件),並用你已經熟透的 RLS 保護每一個檔案。登入的故事講完了,接下來聊「檔案」。下一篇見。

BenZ Software Developer

熱愛技術的軟體開發者,在這裡分享程式開發經驗與學習筆記。

本週主打

AI 自動化入門包

你每天手動在做的那些煩事,其實 AI 可以自己跑。這份給你 10 個照著做就會的自動化工作流 + 50 個複製即用的提示詞,不用會寫程式。

看看這個產品 →