切换语言
切换主题

Supabase Auth 深度配置:OAuth、SSO 与权限控制

那天下午,销售同事跑过来跟我说:“客户要求用 Okta 登录,两周内要上线 SSO。“我当时愣了一下——我的应用只支持邮箱注册和 Google OAuth,SAML?完全没接触过。

说实话,这种场景在 B2B SaaS 里太常见了。企业客户不会接受让你的员工单独注册账号,他们有统一的身份管理系统(Okta、Azure AD、Google Workspace),登录要一键跳转,员工离职账号自动失效。

这篇文章就是为这种场景准备的。我们会从 OAuth 社交登录讲起,过渡到 SAML SSO 企业集成,最后用 Row Level Security(RLS)实现多租户权限隔离——一套完整的认证与授权方案,从消费者级到企业级都能覆盖。

一、OAuth 多 Provider 配置实战

OAuth 社交登录是大多数应用的起点。用户不想记密码,你也不想处理密码存储和验证——让 Google 或 GitHub 来做这件事,双方都省心。

Supabase 支持的 OAuth Provider 很多:Google、GitHub、Apple、Facebook、Discord、Twitter……但实际生产环境里,Google 和 GitHub 使用最广,Apple 则是 iOS 应用的硬性要求(App Store 审核需要)。我们先从 Google 开始。

Google OAuth 配置

打开 Google Cloud Console,那密密麻麻的菜单有时候真让人头大。别慌,直接搜 “OAuth Client ID” 就能找到入口。

创建时选 Web application 类型,关键一步是填写 Authorized redirect URI:

https://<你的项目ref>.supabase.co/auth/v1/callback

这个地址是 Supabase Auth 服务接收 OAuth 回调的地方。项目 ref 在 Supabase Dashboard 左上角能看到,形如 abcdefghijklmnop

拿到 Client ID 和 Client Secret 后,回到 Supabase Dashboard,找到 Authentication > Providers,启用 Google,填入这两个值。

代码调用就简单了:

import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  'https://abcdefghijklmnop.supabase.co',
  'your-anon-key'
)

// 发起 OAuth 登录
const { data, error } = await supabase.auth.signInWithOAuth({
  provider: 'google',
  options: {
    redirectTo: 'https://your-app.com/auth/callback',
    scopes: 'email profile'
  }
})

if (error) {
  console.error('登录失败:', error.message)
  return
}

// data.url 是跳转地址,直接 window.location.href = data.url 即可

redirectTo 是你的应用接收登录结果的地址,Supabase 会把 code 和 state 参数传过去。你需要在那个页面调用 supabase.auth.exchangeCodeForSession() 完成登录:

// 在 /auth/callback 页面
const { error } = await supabase.auth.exchangeCodeForSession()
if (!error) {
  // 登录成功,跳转到主页
  window.location.href = '/'
}

GitHub OAuth 配置

GitHub 的配置类似,入口在 Settings > Developer settings > OAuth Apps。创建时填的 Authorization callback URL 同样是 https://<项目ref>.supabase.co/auth/v1/callback

有一点区别:GitHub OAuth App 没有 scopes 配置界面,scopes 在代码里指定:

await supabase.auth.signInWithOAuth({
  provider: 'github',
  options: {
    redirectTo: 'https://your-app.com/auth/callback',
    scopes: 'repo user'  // repo 可以访问私有仓库
  }
})

如果你只是做用户登录,用默认 scopes 就够了。如果需要访问用户的 GitHub 数据(比如同步仓库列表),scopes 就得加上 repo

Apple OAuth 配置

Apple 的配置最麻烦。要在 Apple Developer Portal 创建 Services ID,还要生成私钥(.p8 文件),私钥只能下载一次,丢了就得重新生成。

关键参数:

  • Services ID:类似 Client ID
  • Team ID:在 Membership 页面找
  • Key ID:私钥的 ID
  • Private Key:下载的 .p8 文件内容

Supabase Dashboard 里填这些值时,Private Key 要把文件内容完整复制进去(包括 BEGIN/END 那两行)。

调用代码跟 Google/GitHub 一样:

await supabase.auth.signInWithOAuth({
  provider: 'apple',
  options: {
    redirectTo: 'https://your-app.com/auth/callback'
  }
})

iOS 应用还有一种方式:直接调用原生 Sign in with Apple API,拿到 identity token 后传给 Supabase:

// 前端收到 Apple 返回的 identity_token
const { data, error } = await supabase.auth.signInWithIdTokenCredentials({
  provider: 'apple',
  token: identityToken
})

这种方式适合已经集成了原生 Apple 登录的 iOS app,可以复用现有逻辑。

二、SAML SSO 企业集成

回到开头那个场景:客户要求用 Okta 登录。这时候 OAuth 就不够用了,需要 SAML 2.0 SSO。

SAML 的工作方式和 OAuth 完全不同。OAuth 是用户授权第三方应用访问自己的数据,而 SAML 是企业身份提供商(IdP)向你的应用(ServiceProvider, SP)发送用户身份信息。对企业来说,SAML 更可控——用户离职,IdP 禁掉账号,所有接入的应用都自动失效。

配置前的准备工作

你需要从客户那里拿到 IdP 的 Metadata。这个文件包含了 IdP 的证书、端点地址等信息。Okta、Azure AD、Google Workspace 都有导出 Metadata 的入口。

Supabase 提供的关键 URL:

EntityID(SP 标识):
https://<项目ref>.supabase.co/auth/v1/sso/saml/metadata

ACS URL(接收 SAML Response 的地址):
https://<项目ref>.supabase.co/auth/v1/sso/saml/acs

Metadata URL(可下载):
https://<项目ref>.supabase.co/auth/v1/sso/saml/metadata?download=true

把这几个 URL 告诉客户的 IT 管理员,让他们在 Okta/Azure AD 里创建 SAML 应用。

使用 Supabase CLI 配置 SSO

Supabase Dashboard 现在也支持 SAML 配置,但我更习惯用 CLI——毕竟企业客户的配置可能要反复调试,命令行更可控。

# 添加 SAML 连接
supabase sso add --type saml --project-ref <项目ref> \
  --metadata-url 'https://company.okta.com/app/exk123/saml/samlmetadata' \
  --domains company.com

--domains 参数很重要。它告诉 Supabase:所有 company.com 域名的用户登录时,应该走这个 SAML 连接。

你也可以用 --metadata-file 代替 --metadata-url,直接上传 XML 文件:

supabase sso add --type saml --project-ref <项目ref> \
  --metadata-file ./okta-metadata.xml \
  --domains company.com

命令执行后会返回一个 sso_provider_id,类似 abc123def456。这个 ID 是 RLS 权限隔离的关键。

Attribute Mapping 配置

SAML Response 里包含用户信息(邮箱、姓名、部门等),但字段命名可能不统一。你需要告诉 Supabase 如何映射这些字段。

创建一个 JSON 文件:

{
  "keys": {
    "email": {
      "name": "email",
      "names": ["EmailAddress", "email", "mail"],
      "required": true
    },
    "first_name": {
      "name": "first_name",
      "names": ["FirstName", "givenName", "first_name"]
    },
    "last_name": {
      "name": "last_name",
      "names": ["LastName", "surname", "last_name"]
    }
  }
}

然后用 supabase sso update 应用这个配置:

supabase sso update <sso_provider_id> \
  --project-ref <项目ref> \
  --attribute-mapping-file ./mapping.json

多租户 SSO 配置

一个项目可以添加多个 SAML 连接。比如你有两个企业客户,一个是 Acme Corp(用 Okta),一个是 Globex Inc(用 Azure AD):

# 添加 Acme Corp 的 SSO
supabase sso add --type saml --project-ref <项目ref> \
  --metadata-url 'https://acme.okta.com/.../metadata' \
  --domains acme.com

# 返回 sso_provider_id: provider_abc

# 添加 Globex Inc 的 SSO
supabase sso add --type saml --project-ref <项目ref> \
  --metadata-url 'https://globex.azure.com/.../metadata' \
  --domains globex.com

# 返回 sso_provider_id: provider_def

现在 Acme Corp 的员工登录时会走 provider_abc,Globex Inc 的员工走 provider_def。每个 SSO 连接的用户数据是隔离的。

用户登录体验

配置完成后,用户的登录流程是这样的:

  1. 用户输入邮箱(比如 [email protected]
  2. Supabase 检测到 acme.com 域名有 SSO 配置
  3. 自动跳转到 Acme 的 Okta 登录页面
  4. 用户在 Okta 输入账号密码(可能已经登录过了,直接通过)
  5. Okta 发送 SAML Response 到 Supabase
  6. Supabase 验证后创建 session,跳回你的应用

代码端发起 SSO 登录:

// 用户输入邮箱后,调用这个方法
const { data, error } = await supabase.auth.signInWithSSO({
  domain: 'acme.com'
})

if (data?.url) {
  // 跳转到 SSO 登录页面
  window.location.href = data.url
}

也可以直接用 sso_provider_id

const { data, error } = await supabase.auth.signInWithSSO({
  providerId: 'provider_abc'
})

SSO 用户的特点

SSO 登录创建的用户和普通用户不同:

  • 邮箱由 IdP 管理:用户不能在应用里改邮箱
  • 没有密码:密码存储在 IdP,Supabase 不参与验证
  • 邮箱不可验证:因为 IdP 已经验证过了

这意味着如果用户需要改邮箱,得让客户 IT 管理员在 Okta/Azure AD 里操作。

三、Row Level Security 进阶用法

认证解决了”用户是谁”的问题,授权解决”用户能做什么”的问题。

传统做法是在业务代码里检查权限:每个 API 都要判断当前用户能否访问这条数据。代码重复,容易遗漏,而且性能不好——每次请求都要查询数据库判断权限。

PostgreSQL 的 Row Level Security(RLS)把权限检查下沉到数据库层。每个查询自动带上权限过滤,不需要业务代码操心。

RLS 启用后的默认行为

这一条很多人踩坑:启用 RLS 后,默认拒绝所有访问

-- 启用 RLS
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;

-- 此时任何查询都返回空(包括管理员)
SELECT * FROM posts;  -- 返回 0 行

必须创建 Policy 才能允许访问。

Policy 的两个关键部分

Policy 有两个 clause:USING 和 WITH CHECK。

USING clause:判断用户能否”看到”或”定位”这条数据。用于 SELECT、UPDATE、DELETE 操作——用户必须先能看到数据,才能修改或删除。

WITH CHECK clause:判断用户能否”写入”这条数据。用于 INSERT、UPDATE 操作——写入后的数据必须满足条件。

-- 用户只能操作自己的 posts
CREATE POLICY "Users manage own posts" ON posts
FOR ALL TO authenticated
USING (auth.uid() = author_id)
WITH CHECK (auth.uid() = author_id);

-- 只允许查看(不能修改)
CREATE POLICY "Users view own posts" ON posts
FOR SELECT TO authenticated
USING (auth.uid() = author_id);

-- 只允许插入(不能查看别人的)
CREATE POLICY "Users insert posts" ON posts
FOR INSERT TO authenticated
WITH CHECK (auth.uid() = author_id);

auth.uid() 返回当前登录用户的 UUID。未登录返回 null,所以 TO authenticated 确保 Policy 只对登录用户生效。

RESTRICTIVE vs PERMISSIVE

一个表可以有多个 Policy。默认是 PERMISSIVE 模式——满足任一 Policy 即可访问。

-- Policy 1:用户能看到自己的
CREATE POLICY "Own data" ON posts
FOR SELECT USING (auth.uid() = author_id);

-- Policy 2:公开的帖子所有人能看到
CREATE POLICY "Public posts" ON posts
FOR SELECT USING (is_public = true);

-- 两个 PERMISSIVE Policy:满足任一即可

RESTRICTIVE Policy 则是”必须满足”。它会和 PERMISSIVE Policy 组合使用,形成更严格的限制。

-- 所有访问必须满足这个条件(作为"全局过滤器")
CREATE POLICY "Tenant isolation" ON posts
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
  SELECT tenant_id FROM users WHERE id = auth.uid()
));

-- 再加上 PERMISSIVE Policy 控制具体操作
CREATE POLICY "Authors edit own posts" ON posts
FOR UPDATE USING (auth.uid() = author_id);

这个组合的效果:用户必须属于正确的租户(RESTRICTIVE),并且是作者才能编辑(PERMISSIVE)。

多租户隔离完整模式

假设你有一个 SaaS 应用,每个企业客户是一个租户。用户表有 tenant_id,所有业务表都要按租户隔离。

-- 用户表
CREATE TABLE users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  tenant_id UUID NOT NULL,
  email TEXT,
  sso_provider_id TEXT  -- SSO 用户才有这个
);

-- 业务表
CREATE TABLE projects (
  id UUID PRIMARY KEY,
  tenant_id UUID NOT NULL,
  name TEXT,
  created_by UUID REFERENCES users(id)
);

-- 启用 RLS
ALTER TABLE projects ENABLE ROW LEVEL SECURITY;

-- 全局租户隔离(RESTRICTIVE)
CREATE POLICY "Tenant isolation" ON projects
AS RESTRICTIVE TO authenticated
USING (tenant_id = (
  SELECT tenant_id FROM users WHERE id = auth.uid()
));

-- 具体操作权限(PERMISSIVE)
CREATE POLICY "Tenant users can view" ON projects
FOR SELECT TO authenticated
USING (true);  -- 已经被 RESTRICTIVE 过滤了,这里直接放行

CREATE POLICY "Project creators can edit" ON projects
FOR UPDATE TO authenticated
USING (created_by = auth.uid());

这样配置后,每个查询自动带上租户过滤,即使业务代码写了 SELECT * FROM projects,数据库也只返回当前租户的数据。

SSO 用户的多租户隔离

SSO 登录的用户可以用 sso_provider_id 来隔离。JWT 里有一个字段存储了登录方式:

-- RLS Policy for SSO 用户
CREATE POLICY "SSO tenant isolation" ON organization_settings
AS RESTRICTIVE TO authenticated
USING (sso_provider_id = (
  SELECT auth.jwt()#>>'{amr,0,provider}'
));

auth.jwt()#>>'{amr,0,provider}' 从 JWT 里提取登录方式的 provider ID。SSO 登录时这个值就是 sso_provider_id

性能优化要点

RLS Policy 是隐式的 WHERE clause。每个查询都会自动加上,而且可能包含子查询。这会影响性能。

几个优化建议:

1. 给 Policy 里的字段建索引

CREATE INDEX idx_posts_author ON posts(author_id);
CREATE INDEX idx_projects_tenant ON projects(tenant_id);

2. 避免 Policy 里的子查询

子查询在每行数据上都执行一次,开销很大。更好的做法是在 JWT 里存储 tenant_id,直接用 auth.jwt() 提取:

-- 不推荐:子查询
USING (tenant_id = (SELECT tenant_id FROM users WHERE id = auth.uid()))

-- 推荐:JWT 里存储 tenant_id
USING (tenant_id = (auth.jwt()->>'tenant_id')::uuid)

后面我们会讲如何用 Custom Access Token Hook 把 tenant_id 放进 JWT。

3. 用 SECURITY DEFINER 函数封装复杂逻辑

Policy 里的复杂逻辑可以封装成函数,函数设为 SECURITY DEFINER,避免每次重复执行:

CREATE FUNCTION current_tenant_id() RETURNS UUID
LANGUAGE SQL STABLE SECURITY DEFINER AS $$
  SELECT tenant_id FROM users WHERE id = auth.uid();
$$;

-- Policy 里调用函数
CREATE POLICY "Tenant isolation" ON projects
AS RESTRICTIVE USING (tenant_id = current_tenant_id());

STABLE 表示函数在同一事务里返回值不变,PostgreSQL 会优化成只执行一次。

四、Custom Claims 与 RBAC 实现

JWT 里默认只有 Supabase 内置的字段:用户 ID、邮箱、角色(authenticated/anon)等。但很多时候你需要更多信息——用户角色(admin/moderator)、租户 ID、权限列表。

Supabase 提供 Custom Access Token Hook,让你在 JWT 发出前修改它的内容。

为什么需要 Custom Claims

几个典型场景:

  1. RBAC 权限控制:JWT 里存储用户角色,RLS Policy 根据角色判断权限
  2. 多租户隔离:JWT 里存储 tenant_id,避免 Policy 里的子查询
  3. 精简 JWT 大小:默认 JWT 包含很多字段,SSR 场景传输开销大,可以删掉不必要的

Supabase 的 JWT 默认字段比较多(session_id、aal、amr 等),每个请求都要带上。如果你的应用有大量 SSR 页面,JWT 在 cookie 里传输,体积会影响性能。

Custom Access Token Hook 实现

Hook 是一个 PL/pgSQL 函数,在 JWT 生成前执行。你可以添加、修改、删除 claims。

-- 创建 Hook 函数
CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
  claims jsonb;
  user_role text;
  tenant_id uuid;
BEGIN
  -- 获取 event 中的 claims
  claims := event->'claims';

  -- 从 user_roles 表获取用户角色
  SELECT role INTO user_role
  FROM public.user_roles
  WHERE user_id = (event->>'user_id')::uuid;

  -- 如果用户有角色,添加到 claims
  IF user_role IS NOT NULL THEN
    claims := jsonb_set(claims, '{user_role}', to_jsonb(user_role));
  END IF;

  -- 从 users 表获取 tenant_id
  SELECT tenant_id INTO tenant_id
  FROM public.users
  WHERE id = (event->>'user_id')::uuid;

  IF tenant_id IS NOT NULL THEN
    claims := jsonb_set(claims, '{tenant_id}', to_jsonb(tenant_id));
  END IF;

  -- 返回修改后的 claims
  RETURN jsonb_build_object('claims', claims);
END;
$$;

-- 授权 supabase_auth_admin 执行这个函数
GRANT EXECUTE ON FUNCTION public.custom_access_token_hook(jsonb)
TO supabase_auth_admin;

-- 在 Supabase Dashboard 启用这个 Hook
-- Authentication > Hooks > Custom Access Token > 选择上面这个函数

Hook 的输入参数 event 包含:

  • user_id:当前用户 UUID
  • claims:当前的 JWT claims
  • authentication_method:登录方式(password、oauth、sso/saml 等)

返回的 claims 会合并到最终 JWT 里。

RBAC 表结构设计

完整的角色权限系统需要几张表:

-- 定义角色类型
CREATE TYPE app_role AS ENUM ('admin', 'moderator', 'user');

-- 定义权限类型
CREATE TYPE app_permission AS ENUM (
  'posts.delete',      -- 删除任意帖子
  'posts.pin',         -- 置顶帖子
  'users.manage',      -- 管理用户
  'settings.edit'      -- 编辑设置
);

-- 用户-角色表
CREATE TABLE public.user_roles (
  user_id UUID REFERENCES auth.users(id) ON DELETE CASCADE,
  role app_role NOT NULL,
  PRIMARY KEY (user_id, role)
);

-- 角色-权限表
CREATE TABLE public.role_permissions (
  role app_role NOT NULL,
  permission app_permission NOT NULL,
  PRIMARY KEY (role, permission)
);

-- 插入默认权限
INSERT INTO role_permissions (role, permission) VALUES
  ('admin', 'posts.delete'),
  ('admin', 'posts.pin'),
  ('admin', 'users.manage'),
  ('admin', 'settings.edit'),
  ('moderator', 'posts.delete'),
  ('moderator', 'posts.pin');

authorize() 权限检查函数

有了角色和权限表,还需要一个函数来检查用户是否有某个权限:

CREATE OR REPLACE FUNCTION public.authorize(
  requested_permission app_permission
)
RETURNS boolean
LANGUAGE plpgsql
STABLE SECURITY DEFINER
AS $$
DECLARE
  user_role app_role;
BEGIN
  -- 从 JWT 获取用户角色
  SELECT (auth.jwt()->>'user_role')::app_role
  INTO user_role;

  -- 如果 JWT 里没有角色,返回 false
  IF user_role IS NULL THEN
    RETURN false;
  END IF;

  -- 检查角色是否有该权限
  RETURN EXISTS (
    SELECT 1 FROM public.role_permissions
    WHERE role = user_role
    AND permission = requested_permission
  );
END;
$$;

-- 授权给 authenticated 角色
GRANT EXECUTE ON FUNCTION public.authorize(app_permission)
TO authenticated;

RLS Policy 调用 authorize()

现在可以在 Policy 里用 authorize() 检查权限:

-- 只有 admin/moderator 能删除帖子
CREATE POLICY "Role-based delete" ON posts
FOR DELETE TO authenticated
USING (
  authorize('posts.delete') OR auth.uid() = author_id
);

-- 只有 admin 能置顶帖子
CREATE POLICY "Admin pin posts" ON posts
FOR UPDATE TO authenticated
USING (
  NOT is_pinned OR authorize('posts.pin')
);

第一个 Policy 的逻辑:用户能删除帖子,要么有 posts.delete 权限(admin/moderator),要么是帖子作者。

第二个 Policy 的逻辑:修改帖子时,如果要把 is_pinned 设为 true,必须有 posts.pin 权限。

前端读取 Custom Claims

Custom Claims 在 JWT 里,前端需要解码 access_token 才能读取:

import { jwtDecode } from 'jwt-decode'

// 获取 session
const { data: { session } } = await supabase.auth.getSession()

if (session) {
  const decoded = jwtDecode(session.access_token)
  console.log('用户角色:', decoded.user_role)
  console.log('租户ID:', decoded.tenant_id)
}

注意 jwtDecode 只解码 JWT,不验证签名。前端不需要验证——验证在 Supabase 服务端完成。

五、企业 SaaS 完整场景实战

现在我们把前面学的东西组合起来,构建一个完整的企业 SaaS 认证方案。

场景是这样的:你的 SaaS 产品有两类用户:

  1. 企业用户:通过客户公司的 SSO 登录(Okta/Azure AD),账号归属某个租户
  2. 个人用户:通过 Google/GitHub OAuth 登录,不属于任何租户,只能访问公开资源

数据隔离要求:企业用户只能看到自己租户的数据,个人用户不能看到企业数据。

用户表设计

CREATE TABLE public.users (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  email TEXT NOT NULL,

  -- 认证来源
  auth_type TEXT NOT NULL DEFAULT 'oauth',  -- 'oauth' 或 'sso'

  -- SSO 用户专属字段
  sso_provider_id TEXT,  -- SSO 连接的 ID
  tenant_id UUID,        -- 所属租户

  -- 基本信息
  full_name TEXT,
  avatar_url TEXT,

  -- 时间戳
  created_at TIMESTAMPTZ DEFAULT now(),
  updated_at TIMESTAMPTZ DEFAULT now()
);

-- 租户表
CREATE TABLE public.tenants (
  id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
  name TEXT NOT NULL,
  sso_provider_id TEXT NOT NULL UNIQUE,  -- 与 SSO 连接关联
  plan_type TEXT NOT NULL DEFAULT 'team',  -- 'team' 或 'enterprise'
  created_at TIMESTAMPTZ DEFAULT now()
);

Custom Access Token Hook 统一处理

Hook 需要区分两种用户:

CREATE OR REPLACE FUNCTION public.custom_access_token_hook(event jsonb)
RETURNS jsonb
LANGUAGE plpgsql
AS $$
DECLARE
  claims jsonb;
  user_record RECORD;
BEGIN
  claims := event->'claims';

  -- 获取用户信息
  SELECT auth_type, sso_provider_id, tenant_id, role
  INTO user_record
  FROM public.users
  WHERE id = (event->>'user_id')::uuid;

  -- 如果用户不存在(刚注册),跳过
  IF user_record IS NULL THEN
    RETURN jsonb_build_object('claims', claims);
  END IF;

  -- 添加认证类型
  claims := jsonb_set(claims, '{auth_type}', to_jsonb(user_record.auth_type));

  -- SSO 用户添加 tenant_id 和 sso_provider_id
  IF user_record.auth_type = 'sso' THEN
    IF user_record.tenant_id IS NOT NULL THEN
      claims := jsonb_set(claims, '{tenant_id}', to_jsonb(user_record.tenant_id));
    END IF;
    IF user_record.sso_provider_id IS NOT NULL THEN
      claims := jsonb_set(claims, '{sso_provider_id}', to_jsonb(user_record.sso_provider_id));
    END IF;
  END IF;

  -- 添加用户角色(如果有)
  IF user_record.role IS NOT NULL THEN
    claims := jsonb_set(claims, '{user_role}', to_jsonb(user_record.role));
  END IF;

  RETURN jsonb_build_object('claims', claims);
END;
$$;

登录后的用户创建流程

OAuth 和 SSO 登录都会触发 auth.users 表的记录创建。我们需要在登录成功后,把用户信息同步到 public.users 表。

可以用 Auth Hooks 的 mfa_verification_hook 或者业务代码处理:

// 在 auth callback 页面,登录成功后
const { data: { user } } = await supabase.auth.getUser()

if (user) {
  // 检查用户是否已存在于 public.users
  const { data: existingUser } = await supabase
    .from('users')
    .select('id')
    .eq('id', user.id)
    .single()

  if (!existingUser) {
    // 判断登录方式
    const authType = user.app_metadata?.provider || 'oauth'

    // 创建用户记录
    await supabase.from('users').insert({
      id: user.id,
      email: user.email,
      auth_type: authType.startsWith('sso') ? 'sso' : 'oauth',
      sso_provider_id: user.app_metadata?.sso_provider_id,
      tenant_id: null,  // 后面由管理员分配
      full_name: user.user_metadata?.full_name,
      avatar_url: user.user_metadata?.avatar_url
    })
  }
}

统一的 RLS Policy

业务表需要有统一的 RLS Policy,同时处理 OAuth 用户和 SSO 用户:

-- 假设有一个 projects 表
CREATE TABLE public.projects (
  id UUID PRIMARY KEY,
  tenant_id UUID REFERENCES tenants(id),
  name TEXT NOT NULL,
  is_public BOOLEAN DEFAULT false,
  created_by UUID REFERENCES users(id)
);

ALTER TABLE public.projects ENABLE ROW LEVEL SECURITY;

-- 全局隔离 Policy(RESTRICTIVE)
CREATE POLICY "Tenant or public access" ON public.projects
AS RESTRICTIVE TO authenticated
USING (
  -- SSO 用户:必须匹配 tenant_id
  (auth.jwt()->>'auth_type' = 'sso'
   AND tenant_id = (auth.jwt()->>'tenant_id')::uuid)
  OR
  -- OAuth 用户:只能看公开项目
  (auth.jwt()->>'auth_type' = 'oauth' AND is_public = true)
);

-- 允许查看(PERMISSIVE)
CREATE POLICY "Users can view" ON public.projects
FOR SELECT TO authenticated
USING (true);

-- 允许创建(租户用户才能创建)
CREATE POLICY "Tenant users can create" ON public.projects
FOR INSERT TO authenticated
WITH CHECK (
  auth.jwt()->>'auth_type' = 'sso'
  AND tenant_id = (auth.jwt()->>'tenant_id')::uuid
);

-- 允许编辑(创建者或管理员)
CREATE POLICY "Creators or admins can edit" ON public.projects
FOR UPDATE TO authenticated
USING (
  created_by = auth.uid()
  OR authorize('projects.edit')
);

这套 Policy 的逻辑:

  • SSO 用户只能看到自己租户的项目
  • OAuth 用户只能看到公开项目
  • 只有 SSO 用户能创建项目(需要归属租户)
  • 编辑权限:创建者或有 projects.edit 权限的管理员

企业客户的管理员分配租户

SSO 用户刚登录时 tenant_id 是 null。需要企业管理员手动分配:

-- 管理员把用户加入租户
UPDATE public.users
SET tenant_id = '<租户UUID>'
WHERE id = '<用户UUID>';

-- 设置用户角色
INSERT INTO public.user_roles (user_id, role)
VALUES ('<用户UUID>', 'admin');

这个过程可以做成一个管理界面,或者用 Auth Hooks 自动处理(根据 SSO provider 匿名映射到租户)。

完整流程图示

用户登录

   ├─ OAuth 用户
   │    │
   │    ├─ Google/GitHub 回调
   │    ├─ Supabase 创建 auth.users
   │    ├─ 业务代码创建 public.users (auth_type='oauth')
   │    └─ JWT: { auth_type: 'oauth' }
   │    │
   │    └─ RLS: 只能看到 is_public=true 的数据

   └─ SSO 用户

        ├─ Okta/Azure AD SAML 回调
        ├─ Supabase 创建 auth.users (带 sso_provider_id)
        ├─ 业务代码创建 public.users (auth_type='sso')
        ├─ 管理员分配 tenant_id
        ├─ Custom Hook 添加 tenant_id 到 JWT
        └─ JWT: { auth_type: 'sso', tenant_id: '...' }

        └─ RLS: 只能看到 tenant_id 匹配的数据

Supabase Auth 企业级配置完整流程

从 OAuth 社交登录到 SAML SSO 企业集成,再到 RLS 多租户权限隔离的完整配置步骤

⏱️ 预计耗时: 2 小时

  1. 1

    步骤1: 配置 OAuth Provider(Google/GitHub/Apple)

    1. 在 Provider 控制台创建 OAuth App(Google Cloud Console / GitHub Settings)
    2. 设置回调地址:https://<项目ref>.supabase.co/auth/v1/callback
    3. 在 Supabase Dashboard 启用 Provider,填入 Client ID 和 Secret
    4. 代码调用 signInWithOAuth(),指定 scopes 和 redirectTo
  2. 2

    步骤2: 配置 SAML SSO 企业集成

    1. 从企业 IdP(Okta/Azure AD)获取 Metadata 文件或 URL
    2. 使用 CLI 添加 SSO 连接:supabase sso add --type saml --domains company.com
    3. 配置 Attribute Mapping,映射 email、name 等字段
    4. 测试登录流程:用户输入邮箱后自动跳转到 IdP
  3. 3

    步骤3: 实现 RLS 多租户隔离

    1. 在业务表添加 tenant_id 字段
    2. 启用 RLS:ALTER TABLE projects ENABLE ROW LEVEL SECURITY
    3. 创建 RESTRICTIVE Policy 实现全局租户过滤
    4. 创建 PERMISSIVE Policy 控制具体操作权限(SELECT/INSERT/UPDATE)
    5. 给 tenant_id 字段添加索引优化性能
  4. 4

    步骤4: 配置 Custom Access Token Hook

    1. 创建 public.custom_access_token_hook() 函数
    2. 在函数中添加 tenant_id、user_role 等 custom claims
    3. 授权 supabase_auth_admin 执行函数
    4. 在 Supabase Dashboard 启用 Hook(Authentication > Hooks)
    5. 前端通过 jwtDecode() 读取 custom claims
  5. 5

    步骤5: 实现 RBAC 权限控制

    1. 创建 user_roles 和 role_permissions 表
    2. 定义 app_role 和 app_permission 枚举类型
    3. 创建 authorize() 函数检查用户权限
    4. 在 RLS Policy 中调用 authorize('permission.name')
    5. 前端根据 JWT 中的 user_role 显示/隐藏 UI 元素

常见问题

OAuth 和 SAML SSO 有什么区别?我该选哪个?
OAuth 适合个人消费者应用,用户用 Google/GitHub 账号登录,配置简单,维护成本低。SAML SSO 适合 B2B 企业应用,企业客户要求员工用公司统一账号(Okta/Azure AD)登录,账号由企业管理,员工离职自动失效。

如果你做 B2B SaaS,SSO 是必备功能;如果是个人产品,OAuth 就够了。
启用 RLS 后查询返回空结果,怎么办?
这是 RLS 的默认行为:启用后默认拒绝所有访问。你需要创建 Policy 才能允许访问。例如:CREATE POLICY "Users view own posts" ON posts FOR SELECT TO authenticated USING (auth.uid() = author_id);
RLS Policy 性能差,如何优化?
三个优化方向:

• 给 Policy 中的字段建索引:CREATE INDEX idx_tenant ON projects(tenant_id);
• 避免 Policy 中的子查询:用 Custom Access Token Hook 把 tenant_id 放进 JWT,直接用 auth.jwt()->>'tenant_id' 提取
• 用 SECURITY DEFINER 函数封装复杂逻辑,PostgreSQL 会优化成只执行一次
如何在 JWT 中存储自定义字段(如 tenant_id、user_role)?
使用 Supabase 的 Custom Access Token Hook:

1. 创建 PL/pgSQL 函数 custom_access_token_hook(event jsonb)
2. 在函数中用 jsonb_set() 添加 custom claims
3. 在 Supabase Dashboard 启用 Hook(Authentication > Hooks > Custom Access Token)
4. 前端用 jwtDecode() 读取 JWT 中的 custom claims
多租户 SaaS 如何实现企业用户和个人用户的数据隔离?
核心思路:JWT 中存储 auth_type('oauth' 或 'sso')和 tenant_id,RLS Policy 根据这两个字段过滤数据:

• SSO 用户:只能看到 tenant_id 匹配的租户数据
• OAuth 用户:只能看到 is_public=true 的公开数据

使用 RESTRICTIVE Policy 作为全局过滤器,PERMISSIVE Policy 控制具体操作。
Supabase 支持 Apple Sign In 吗?配置复杂吗?
支持,但配置比 Google/GitHub 复杂:

1. 在 Apple Developer Portal 创建 Services ID
2. 生成私钥(.p8 文件,只能下载一次)
3. 在 Supabase Dashboard 填入 Team ID、Key ID、Services ID 和私钥内容

iOS 应用可以用原生 Sign in with Apple API,拿到 identity_token 后传给 Supabase 的 signInWithIdTokenCredentials()。
企业客户要求 SSO,但我不知道他们的 IdP 类型,怎么办?
让客户的 IT 管理员提供 IdP Metadata 文件或 URL(Okta/Azure AD/Google Workspace 都支持导出)。你只需要拿到 Metadata,然后用 supabase sso add --metadata-file 命令添加连接。Supabase 会自动解析 Metadata 中的端点和证书。

我们从 OAuth 社交登录起步,走到 SAML SSO 企业集成,最后用 RLS 和 Custom Claims 搭起完整的权限体系。这套方案能覆盖从个人产品到企业 SaaS 的各种场景。

几个关键决策点:

如果你的用户主要是个人消费者:OAuth(Google/GitHub)就够了。配置简单,用户习惯,维护成本低。RLS 基础版——用户只能访问自己的数据——足矣。

如果你做 B2B SaaS:SSO 是必备功能。企业客户会要求,没有的话可能直接被淘汰。准备好 Okta/Azure AD/Google Workspace 的集成,预留多租户架构。

如果你做复杂的企业应用:RBAC + RLS 组合是标准方案。角色权限表、Custom Claims、authorize() 函数——这套组合拳能处理精细的权限需求,比如”管理员能删除但不能置顶”、“租户成员能编辑但不能删除”。

下一步建议:先从 OAuth 开始,跑通基本流程。等项目成熟、有企业客户需求时,再叠加 SSO 和 RBAC。Supabase 的架构支持渐进式升级,不用一开始就把所有功能都配好。

13 分钟阅读 · 发布于: 2026年4月21日 · 修改于: 2026年4月25日

相关文章

BetterLink

想持续收到这个主题的更新?

你可以直接关注作者更新、订阅 RSS,或者继续沿着系列入口往下读,避免下次又回到搜索结果重新找。

关注公众号

评论

使用 GitHub 账号登录后即可评论