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() 成為安全的核心,而不是只依賴前端的路由守衛。
分享這篇文章