为你的 Supabase 应用添加认证 (Authentication)
Supabase 基础知识
Supabase 利用 Postgres 的行级安全 (Row-Level Security) 来控制数据访问权限。简单来说,通过为数据库中的表创建行级安全策略,我们可以限制和管理谁可以读取、写入和更新表中的数据。
假设你的数据库中有一个名为 "posts" 的表,内容如下:

表中的 user_id
字段表示每条帖子数据所属的用户。你可以根据 user_id
字段限制每个用户只能访问自己的帖子数据。
但在实现这一点之前,Supabase 需要能够识别当前访问数据库的用户。
向 Supabase 请求中添加用户数据
得益于 Supabase 对 JWT 的支持,当我们的应用与 Supabase 交互时,可以使用 Supabase 提供的 JWT secret 生成包含用户数据的 JWT。然后在请求时将该 JWT 作为认证 (Authentication) 头部。Supabase 在收到请求后会自动验证 JWT 的有效性,并在后续流程中允许访问其中包含的数据。
首先,我们可以在 Supabase 控制台的 “Project Settings” 中获取 Supabase 提供的 JWT secret:

接着,当我们使用 Supabase SDK 向 Supabase 发起请求时,就可以利用这个 secret 生成我们的 JWT,并将其作为认证 (Authentication) 头部附加到请求中。(请注意,这一过程应在你的应用后端服务中进行,JWT secret 不应暴露给第三方。)
import { createClient } from '@supabase/supabase-js';
import { sign } from 'jsonwebtoken';
/
* 注意:
* 你可以在获取 JWT Secret 的同一位置找到 SUPABASE_URL、SUPABASE_ANON_KEY。
*/
const SUPABASE_URL = process.env.SUPABASE_URL;
const SUPABASE_ANON_KEY = process.env.SUPABASE_ANON_KEY;
const SUPABASE_JWT_SECRET = process.env.SUPABASE_JWT_SECRET;
export const getSupabaseClient = (userId) => {
const jwtPayload = {
userId,
};
const jwt = sign(jwtPayload, SUPABASE_JWT_SECRET, {
expiresIn: '1h', // 仅用于演示
});
const client = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
global: {
headers: {
Authorization: `Bearer ${jwt}`,
},
},
});
return client;
};
接下来,进入 Supabase 控制台的 SQL Editor,创建一个用于获取请求中携带的 userId 的函数:

图片中使用的代码如下:
create or replace function auth.user_id() returns text as $$
select nullif(current_setting('request.jwt.claims', true)::json->>'userId', '')::text;
$$ language sql stable;
如代码所示,在 Supabase 中,你可以通过调用 request.jwt.claims
获取我们生成的 JWT 的 payload。payload 内的 userId
字段就是我们设置的值。
有了这个函数,Supabase 就可以确定当前访问数据库的用户。
创建行级安全策略
接下来,我们可以创建一个行级安全策略,限制每个用户只能访问 posts 表中属于自己的帖子数据。
- 进入 Supabase 控制台的 Table Editor 页面,选择 posts 表。
- 点击表格顶部的 "Add RLS Policy"。
- 在弹出的窗口中点击 "Create policy"。
- 输入策略名称,并选择 SELECT Policy 命令。
- 在如下代码的
using
块中输入:
auth.user_id() = user_id

通过这样的策略,即可实现 Supabase 内的数据访问控制。
在实际应用中,你还会为数据插入、修改等操作创建不同的策略来限制用户行为。但这些内容超出了本文范围。关于行级安全 (RLS) 的更多信息,请参考 使用 Postgres 行级安全保护你的数据。
与 Logto 的基础集成流程
如前所述,由于 Supabase 采用 RLS 进行访问控制,集成 Logto(或其他认证 (Authentication) 服务)的关键在于获取已授权用户的 user id 并将其传递给 Supabase。整个流程如下图所示:
接下来,我们将基于该流程图讲解如何集成 Logto 与 Supabase。
Logto 集成
Logto 提供了多种框架和编程语言的集成指南。
通常,这些框架和语言构建的应用分为 Native app、SPA(单页应用)、传统 Web 应用和机器对机器 (M2M) 应用等类型。你可以访问 Logto 快速上手 页面,根据你的技术栈将 Logto 集成到你的应用中。之后,按照下方说明根据你的应用类型将 Logto 集成到你的项目。
Native app 或 SPA
Native app 和 SPA 都运行在你的设备上,登录后获得的凭证(访问令牌 (Access token))也会本地存储在你的设备上。
因此,在将你的应用与 Supabase 集成时,需要通过你的后端服务与 Supabase 交互,因为你不能在每个用户设备上暴露敏感信息(如 Supabase JWT secret)。
假设你正在使用 React 和 Express 构建 SPA,并已通过 Logto React SDK 指南 成功集成 Logto(可参考我们的 react 示例代码)。此外,你已根据 验证访问令牌 (Access tokens) 指南在后端服务器添加了 Logto 访问令牌 (Access token) 校验。
接下来,你将使用从 Logto 获取的访问令牌 (Access token) 向后端服务器请求用户数据:
import { useLogto } from '@logto/react';
import { useState, useEffect } from 'react';
import PostList from './PostList';
const endpoint = '<https://www.mysite.com/api/posts>';
const resource = '<https://www.mysite.com/api>';
function PostPage() {
const { isAuthenticated, getAccessToken } = useLogto();
const [posts, setPosts] = useState();
useEffect(() => {
const fetchPosts = async () => {
const response = await fetch(endpoint, {
headers: {
Authorization: `Bearer ${await getAccessToken(resource)}`,
},
});
setPosts(response.json());
};
if (isAuthenticated) {
void fetchPosts();
}
}, [isAuthenticated, getAccessToken]);
return <PostList posts={posts} />;
}
export default PostPage;
在你的后端服务器中,你已经通过中间件从访问令牌 (Access token) 中提取了已登录用户的 id:
// auth-middleware.ts
import { createRemoteJWKSet, jwtVerify } from 'jose';
//...
export const verifyAuthFromRequest = async (ctx, next) => {
// 提取 token
const token = extractBearerTokenFromHeaders(ctx.request.headers);
const { payload } = await jwtVerify(
token, // 从请求头提取的原始 Bearer Token
createRemoteJWKSet(new URL('https://<your-logto-domain>/oidc/jwks')), // 使用从 Logto 服务器获取的 jwks_uri 生成 jwks
{
// 期望的令牌发行者,应该由 Logto 服务器签发
issuer: 'https://<your-logto-domain>/oidc',
// 期望的受众,应该是当前 API 的资源指示器 (Resource indicator)
audience: '<your request listener resource indicator>',
}
);
// 如果你使用 RBAC
assert(payload.scope.includes('some_scope'));
// 自定义 payload 逻辑
ctx.auth = {
userId: payload.sub,
};
return next();
};
现在,你可以使用上文描述的 getSupabaseClient
,将 userId
附加到后续请求 Supabase 时使用的 JWT。或者,你可以创建一个中间件,为需要与 Supabase 交互的请求创建 Supabase 客户端:
export const withSupabaseClient = async (ctx, next) => {
ctx.supabase = getSupabaseClient(ctx.auth.userId);
return next();
};
在后续处理流程中,你可以直接调用 ctx.supabase
与 Supabase 交互:
const fetchPosts = async (ctx) => {
cosnt { data } = await ctx.supabase.from('posts').select('*');
return data;
}
在这段代码中,Supabase 会根据之前设置的策略,仅返回属于当前用户的帖子数据。
传统 Web 应用
传统 Web 应用与 Native app 或 SPA 的主要区别在于,传统 Web 应用仅在 Web 服务器端渲染和更新页面。因此,用户凭证由 Web 服务器直接管理,而 Native app 和 SPA 则存储在用户设备上。
在 Supabase 中集成 Logto 到传统 Web 应用时,可以直接在后端获取已登录用户的 id。
以 Next.js 项目为例,在你按照 Next.js SDK 指南 集成 Logto 后,可以使用 Logto SDK 获取用户信息,并构造用于与 Supabase 交互的 JWT。
import { getLogtoContext } from '@logto/next-server-actions';
import { logtoConfig } from '@/logto';
import { getSupabaseClient } from '@/utils';
import PostList from './PostList';
export default async function PostPage() {
const { cliams } = await getLogtoContext(logtoConfig);
// `cliams` 中的 `sub` 值即为用户 id。
const supabase = getSupabaseClient(cliams.sub);
const { data: posts } = await supabase.from('posts').select('*');
return <PostList posts={posts} />;
}
机器对机器 (Machine-to-machine) 应用
机器对机器 (M2M) 通常用于你的应用需要直接与资源服务器通信的场景,例如静态服务每日拉取帖子等。
你可以参考 机器对机器:使用 Logto 认证 (Authentication) 指南进行机器对机器应用的认证 (Authentication)。Supabase 与机器对机器应用的集成方式与 Native app 和 SPA 类似(详见 “Native app 或 SPA” 部分),即从 Logto 获取访问令牌 (Access token),然后通过受保护的后端 API 验证。
但需要注意的是,Native app 和 SPA 通常面向终端用户,因此获取到的用户 id 代表用户本身。而机器对机器应用的访问令牌 (Access token) 代表应用本身,访问令牌 (Access token) payload 中的 sub
字段是 M2M 应用的 client id,而不是具体用户。因此在开发时要区分哪些数据是为 M2M 应用准备的。
此外,如果你希望某个特定的 M2M 应用代表整个服务访问 Supabase,从而绕过 RLS 限制,可以使用 Supabase 的 service_role
secret 创建 Supabase 客户端。当你需要进行一些需要访问全部数据的管理或自动化任务时,这非常有用,不受为单个用户设置的行级安全策略限制。
service_role
secret 可在与 JWT secret 相同的页面找到:

创建 Supabase 客户端时,使用 service_role
secret,即可访问数据库中的所有数据:
import { createClient } from '@supabase/supabase-js';
// ...
const SUPABASE_SERVICE_ROLE_SCRET = process.env.SUPABASE_SERVICE_ROLE_SCRET;
const client = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_SCRET, {
// ...options
});