一、核心原理
問題
Supabase Auth 原生不支援飛書、微信等國內 OAuth 平台。我們需要用這些第三方平台做身份驗證,同時複用 Supabase 的會話管理能力(JWT、refresh token、RLS)。解決思路
把第三方 OAuth 當作「門禁系統」——只負責驗證身份,不負責管理通行證。驗證通過後,由 Supabase Auth 簽發真正的會話令牌。 數據流:二、user_identities 表設計
表結構
關鍵設計決策
1.oauth_provider 和 oauth_open_id 是可空欄位
這是最重要的設計決策。原因:
觸發器對所有2. 用部分唯一索引替代複合唯一約束auth.users插入都會執行。如果用戶是通過郵箱/密碼、手機號等非 OAuth 方式註冊的,user_metadata裡不會有oauth_provider和oauth_open_id。如果這兩個欄位是NOT NULL,觸發器執行時會因為NULL值違反非空約束而報錯,導致用戶註冊失敗。 所以必須將這兩個欄位設為可空,讓觸發器能正常為所有類型的用戶創建 identity 記錄——OAuth 用戶填入具體值,非 OAuth 用戶留NULL。
UNIQUE(oauth_provider, oauth_open_id)?
- PostgreSQL 的
UNIQUE約束中,NULL不等於NULL,所以多個(NULL, NULL)的行不會衝突 - 但語義上不夠清晰,用部分唯一索引更明確地表達意圖:僅在 OAuth 欄位非空時才強制唯一
- 這保證了同一平台的同一用戶不會被創建兩次,同時允許任意數量的非 OAuth 用戶存在
id 直接引用 auth.users(id) 且 ON DELETE CASCADE
user_identities.id和auth.users.id是同一個 UUID- Supabase 管理後台刪除用戶時,profile 記錄自動清理
- RLS 策略可以直接用
auth.uid() = id做權限控制
raw_metadata 存各平台特有欄位
不同 OAuth 平台返回的用戶信息欄位不同(飛書有 tenant_key、微信有 unionid),與其給每個平台加專屬列,不如統一放進 JSONB,表結構永遠不需要為新平台改動。
RLS 策略
三、觸發器
觸發器函數
綁定觸發器
觸發器行為說明
| 註冊方式 | oauth_provider | oauth_open_id | raw_metadata |
|---|---|---|---|
| 飛書 OAuth | 'feishu' | 'ou_xxxxx' | 包含 name、avatar_url、tenant_key 等 |
| 微信 OAuth | 'wechat' | 'oXXXX' | 包含 nickname、headimgurl、unionid 等 |
| 郵箱密碼註冊 | NULL | NULL | {} 或包含註冊時傳入的 metadata |
| 手機號註冊 | NULL | NULL | {} |
SECURITY DEFINER 確保觸發器以創建者權限執行,繞過 RLS 寫入 profiles 表。NEW.raw_user_meta_data 是 Supabase Auth 在 createUser 時傳入的 user_metadata 對象。
四、Edge Function 關鍵代碼(以飛書為例)
用戶查找與創建
簽發會話令牌
前端建立會話
五、前端認證狀態讀取
不需要自定義 Context 或 localStorage,直接用 Supabase Auth 內建的會話管理:session.user.user_metadata 裡,就是 createUser 時傳入的那個 userMetadata 對象。
六、擴展新的 OAuth 平台
以接入微信為例,只需要:- 新建 Edge Function
wechat-auth:實現微信 OAuth 授權碼交換、用戶信息獲取 - 同樣的 Supabase 邏輯:查
user_identities→ 創建/更新用戶 →generateLink→ 返回token_hash - 查找時換 provider:
.eq('oauth_provider', 'wechat').eq('oauth_open_id', wechatOpenId)
user_identities 表結構、觸發器、前端 useAuth、verifyOtp 邏輯、RLS 策略——全部複用。
七、踩坑記錄
| 問題 | 原因 | 解決方案 |
|---|---|---|
| 非 OAuth 用戶註冊時觸發器報錯 | oauth_provider 和 oauth_open_id 設為 NOT NULL,但非 OAuth 用戶的 metadata 裡沒有這兩個欄位 | 改為可空欄位,觸發器統一處理:有 OAuth 信息就填,沒有就留 NULL |
| 部分唯一索引 vs 複合唯一約束 | UNIQUE(a, b) 在 PostgreSQL 中允許多個 (NULL, NULL) 行(因為 NULL != NULL),語義不夠明確 | 用 CREATE UNIQUE INDEX ... WHERE ... IS NOT NULL 部分索引,明確只約束非空值 |
generateLink 返回的 hashed_token 一次性使用 | verifyOtp 成功後 token 失效 | 前端必須在回調頁立即調用 verifyOtp,不能延遲或重試 |
Edge Function 中 createUser 的 email 必填 | Supabase Auth 要求每個用戶有 email | 固定使用虛擬郵箱 {provider}_{openid}@oauth.local,不使用 OAuth 平台返回的真實郵箱,避免與普通註冊賬號衝突 |

