跳到主要内容

为你的 Supabase 应用添加认证 (Authentication)

Supabase 基础知识

Supabase 利用 Postgres 的行级安全 (Row-Level Security) 来控制数据访问权限。简单来说,通过为数据库中的表创建行级安全策略,我们可以限制和管理谁可以读取、写入和更新表中的数据。

假设你的数据库中有一个名为 "posts" 的表,内容如下:

Posts table

表中的 user_id 字段表示每条帖子数据所属的用户。你可以根据 user_id 字段限制每个用户只能访问自己的帖子数据。

但在实现这一点之前,Supabase 需要能够识别当前访问数据库的用户。

向 Supabase 请求中添加用户数据

得益于 Supabase 对 JWT 的支持,当我们的应用与 Supabase 交互时,可以使用 Supabase 提供的 JWT secret 生成包含用户数据的 JWT。然后在请求时将该 JWT 作为认证 (Authentication) 头部。Supabase 在收到请求后会自动验证 JWT 的有效性,并在后续流程中允许访问其中包含的数据。

首先,我们可以在 Supabase 控制台的 “Project Settings” 中获取 Supabase 提供的 JWT secret:

Supabase API settings page

接着,当我们使用 Supabase SDK 向 Supabase 发起请求时,就可以利用这个 secret 生成我们的 JWT,并将其作为认证 (Authentication) 头部附加到请求中。(请注意,这一过程应在你的应用后端服务中进行,JWT secret 不应暴露给第三方。)

import { createClient } from '@supabase/supabase-js';
import { sign } from 'jsonwebtoken';

/
* 注意:
* 你可以在获取 JWT Secret 的同一位置找到 SUPABASE_URLSUPABASE_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 get user ID function

图片中使用的代码如下:

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 表中属于自己的帖子数据。

  1. 进入 Supabase 控制台的 Table Editor 页面,选择 posts 表。
  2. 点击表格顶部的 "Add RLS Policy"。
  3. 在弹出的窗口中点击 "Create policy"。
  4. 输入策略名称,并选择 SELECT Policy 命令。
  5. 在如下代码的 using 块中输入:
auth.user_id() = user_id
Create RLS policy

通过这样的策略,即可实现 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 相同的页面找到:

Service role 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
});