一、核心原理
问题
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 平台返回的真实邮箱,避免与普通注册账号冲突 |

