跳至主要內容

Supabase Auth 認證整合全攻略:從設定到生產環境

6 分鐘閱讀 1,050 字

Supabase Auth 認證整合全攻略:從設定到生產環境

Supabase Auth 是一套功能完整的認證服務,支援電子郵件/密碼、魔法連結、多種 OAuth 提供商,以及電話號碼驗證。本文將帶你從零開始完成整合,並介紹幾個在生產環境中常被忽略的細節。

初始設定

安裝 Supabase 客戶端:

npm install @supabase/supabase-js

建立客戶端實例:

// lib/supabase.ts
import { createClient } from '@supabase/supabase-js';
import type { Database } from './database.types';

const supabaseUrl = import.meta.env.VITE_SUPABASE_URL;
const supabaseAnonKey = import.meta.env.VITE_SUPABASE_ANON_KEY;

export const supabase = createClient<Database>(supabaseUrl, supabaseAnonKey, {
  auth: {
    autoRefreshToken: true,
    persistSession: true,
    detectSessionInUrl: true
  }
});

電子郵件認證

註冊

async function signUp(email: string, password: string, username: string) {
  const { data, error } = await supabase.auth.signUp({
    email,
    password,
    options: {
      // 額外的使用者資訊會存入 auth.users 的 user_metadata
      data: {
        username,
        avatar_url: `https://api.dicebear.com/7.x/avataaars/svg?seed=${username}`
      },
      // 確認信中的跳轉 URL
      emailRedirectTo: `${window.location.origin}/auth/callback`
    }
  });

  if (error) throw error;

  // data.user 不為 null 但 data.session 為 null 表示需要確認信
  if (data.user && !data.session) {
    return { status: 'confirm_email' };
  }

  return { status: 'success', user: data.user };
}

登入

async function signIn(email: string, password: string) {
  const { data, error } = await supabase.auth.signInWithPassword({
    email,
    password
  });

  if (error) {
    // 區分不同的錯誤類型
    if (error.message === 'Invalid login credentials') {
      throw new Error('電子郵件或密碼錯誤');
    }
    if (error.message.includes('Email not confirmed')) {
      throw new Error('請先確認您的電子郵件');
    }
    throw error;
  }

  return data;
}

OAuth 社交登入

async function signInWithGoogle() {
  const { error } = await supabase.auth.signInWithOAuth({
    provider: 'google',
    options: {
      redirectTo: `${window.location.origin}/auth/callback`,
      queryParams: {
        // 強制顯示帳號選擇介面
        prompt: 'select_account'
      }
    }
  });

  if (error) throw error;
}

async function signInWithGitHub() {
  const { error } = await supabase.auth.signInWithOAuth({
    provider: 'github',
    options: {
      redirectTo: `${window.location.origin}/auth/callback`,
      scopes: 'read:user user:email'
    }
  });

  if (error) throw error;
}

Auth Callback 頁面

OAuth 登入後,使用者會被重導回你的應用,需要處理 callback:

// pages/auth/callback.ts
import { supabase } from '../lib/supabase';

export async function handleAuthCallback() {
  const { data, error } = await supabase.auth.exchangeCodeForSession(
    window.location.search
  );

  if (error) {
    console.error('Auth callback error:', error);
    return { success: false, error };
  }

  // 成功後跳轉到目標頁面
  const returnTo = localStorage.getItem('auth_return_to') || '/dashboard';
  localStorage.removeItem('auth_return_to');

  return { success: true, user: data.user, returnTo };
}

監聽認證狀態

// 在應用初始化時設定
supabase.auth.onAuthStateChange((event, session) => {
  console.log('Auth event:', event);

  switch (event) {
    case 'SIGNED_IN':
      // 設定使用者狀態、載入使用者資料
      userStore.setUser(session.user);
      break;

    case 'SIGNED_OUT':
      // 清除所有使用者狀態
      userStore.clearUser();
      router.push('/login');
      break;

    case 'TOKEN_REFRESHED':
      // Token 已自動更新,通常不需要處理
      break;

    case 'USER_UPDATED':
      // 使用者資料更新(如更改 email 後確認)
      userStore.setUser(session.user);
      break;

    case 'PASSWORD_RECOVERY':
      // 使用者點擊重設密碼連結
      router.push('/auth/reset-password');
      break;
  }
});

Row Level Security(RLS)整合

Auth 的核心價值在於與 RLS 結合,讓資料庫層面就確保安全性:

-- 建立 profiles 表,與 auth.users 關聯
CREATE TABLE public.profiles (
  id UUID REFERENCES auth.users(id) ON DELETE CASCADE PRIMARY KEY,
  username TEXT UNIQUE NOT NULL,
  avatar_url TEXT,
  updated_at TIMESTAMPTZ DEFAULT NOW()
);

-- 開啟 RLS
ALTER TABLE public.profiles ENABLE ROW LEVEL SECURITY;

-- 任何人可以讀取 profiles
CREATE POLICY "profiles are viewable by everyone"
  ON public.profiles
  FOR SELECT
  USING (true);

-- 只有本人可以更新自己的 profile
CREATE POLICY "users can update own profile"
  ON public.profiles
  FOR UPDATE
  USING (auth.uid() = id);

-- 當新使用者註冊時,自動建立 profile
CREATE OR REPLACE FUNCTION public.handle_new_user()
RETURNS TRIGGER AS $$
BEGIN
  INSERT INTO public.profiles (id, username, avatar_url)
  VALUES (
    NEW.id,
    NEW.raw_user_meta_data->>'username',
    NEW.raw_user_meta_data->>'avatar_url'
  );
  RETURN NEW;
END;
$$ LANGUAGE plpgsql SECURITY DEFINER;

CREATE TRIGGER on_auth_user_created
  AFTER INSERT ON auth.users
  FOR EACH ROW EXECUTE PROCEDURE public.handle_new_user();

在 Vue 3 中的完整整合

// stores/auth.ts (Pinia)
import { defineStore } from 'pinia';
import { supabase } from '../lib/supabase';
import type { User } from '@supabase/supabase-js';

export const useAuthStore = defineStore('auth', {
  state: () => ({
    user: null as User | null,
    loading: true
  }),

  getters: {
    isAuthenticated: (state) => !!state.user,
    userId: (state) => state.user?.id
  },

  actions: {
    async initialize() {
      // 從 session 中恢復使用者狀態
      const { data: { session } } = await supabase.auth.getSession();
      this.user = session?.user ?? null;
      this.loading = false;

      // 監聽後續變化
      supabase.auth.onAuthStateChange((_, session) => {
        this.user = session?.user ?? null;
      });
    },

    async signOut() {
      await supabase.auth.signOut();
      this.user = null;
    }
  }
});

路由守衛

// router/index.ts
router.beforeEach(async (to) => {
  const authStore = useAuthStore();

  // 等待初始化完成
  if (authStore.loading) {
    await new Promise(resolve => {
      const unwatch = watch(
        () => authStore.loading,
        (loading) => { if (!loading) { unwatch(); resolve(undefined); } }
      );
    });
  }

  const requiresAuth = to.meta.requiresAuth;
  const requiresGuest = to.meta.requiresGuest;

  if (requiresAuth && !authStore.isAuthenticated) {
    localStorage.setItem('auth_return_to', to.fullPath);
    return { name: 'login' };
  }

  if (requiresGuest && authStore.isAuthenticated) {
    return { name: 'dashboard' };
  }
});

常見問題

Q:部署後 OAuth 登入失敗? A:確認在 Supabase Dashboard 的 Auth > URL Configuration 中,已將生產環境的 URL 加入 Redirect URLs 白名單。

Q:Token 過期後使用者被強制登出? A:確認建立 client 時有設定 autoRefreshToken: true,且沒有手動覆蓋 storage 導致 refresh token 遺失。

Q:如何讓同一 email 可以用不同的 provider 登入? A:在 Supabase Dashboard 開啟「Link accounts」設定,或使用 supabase.auth.linkIdentity() 手動關聯帳號。

小結

Supabase Auth 結合 PostgreSQL RLS 提供了一套安全且易用的認證方案。關鍵是在資料庫層面做好存取控制,讓 auth.uid() 成為安全的核心,而不是只依賴前端的路由守衛。

分享這篇文章