跳至主要內容

為你的 Supabase 應用程式新增驗證 (Authentication)

Supabase 基礎知識​

Supabase 利用 Postgres 的 Row-Level Security(行級安全性) 來控制資料存取權限。簡單來說,透過為資料庫中的資料表建立 Row Level Security(RLS)政策,我們可以限制並管理誰能讀取、寫入和更新資料表中的資料。

假設你的資料庫中有一個名為「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_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 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 就能判斷目前存取資料庫的使用者。

建立 Row-Level Security 政策​

接下來,我們可以建立一個 Row-Level Security 政策,根據 posts 資料表中的 user_id 欄位,限制每個使用者只能存取自己的貼文資料。

  1. 前往 Supabase 控制台的 Table Editor 頁面,選擇 posts 資料表。
  2. 點擊表格上方的「Add RLS Policy」。
  3. 在彈出的視窗中點擊「Create policy」。
  4. 輸入 Policy Name 並選擇 SELECT Policy 指令。
  5. 在下方 using 區塊輸入:
auth.user_id() = user_id
Create RLS policy

透過這類政策,即可實現 Supabase 內的資料存取控制。

在實際應用中,你會建立各種政策來限制使用者的資料插入與修改等操作,但這超出本文範圍。更多關於 Row-Level Security(RLS)的資訊,請參閱 Secure your data using Postgres Row Level Security。

與 Logto 的基本整合流程​

如前所述,Supabase 透過 RLS 進行存取控制,因此與 Logto(或其他驗證 (Authentication) 服務)整合的關鍵在於取得授權使用者的 user id 並傳遞給 Supabase。整個流程如下圖所示:

接下來,我們將根據此流程圖說明如何將 Logto 與 Supabase 整合。

Logto 整合​

Logto 提供多種框架與程式語言的整合指南。

一般來說,這些框架與語言建構的應用程式可分為 Native app、SPA(單頁應用程式)、傳統 Web app 以及 M2M(機器對機器)app。你可以前往 Logto 快速入門 頁面,根據你使用的技術棧將 Logto 整合進你的應用程式。之後,請依照下方說明,根據應用程式型態將 Logto 整合進你的專案。

Native app 或 SPA​

Native app 與 SPA 都在你的裝置上執行,登入後取得的憑證(存取權杖 (Access token))會儲存在本地裝置。

因此,當你的應用程式要與 Supabase 整合時,必須透過你的後端服務與 Supabase 互動,因為你不能在每個使用者裝置上暴露敏感資訊(如 Supabase JWT secret)。

假設你使用 React 與 Express 建構 SPA,並已依照 Logto React SDK 指南 成功將 Logto 整合進應用程式(可參考我們的 react 範例程式碼)。此外,你已根據 驗證存取權杖 (Validate access tokens) 指南,在後端伺服器加入 Logto 存取權杖驗證。

接下來,你將使用從 Logto 取得的存取權杖向後端伺服器請求使用者資料:

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;

在你的後端伺服器中,你已透過 middleware 從存取權杖中擷取登入使用者的 id:

// auth-middleware.ts
import { createRemoteJWKSet, jwtVerify } from 'jose';

//...

export const verifyAuthFromRequest = async (ctx, next) => {
// 擷取權杖
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。你也可以建立 middleware,為需要與 Supabase 互動的請求建立 Supabase client:

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 app​

傳統 Web app 與 Native app 或 SPA 的主要差異在於,傳統 Web app 僅在 Web 伺服器上進行頁面渲染與更新。因此,使用者憑證由 Web 伺服器直接管理,而 Native app 與 SPA 則儲存在使用者裝置上。

在 Supabase 中將 Logto 與傳統 Web app 整合時,可以直接從後端取得登入使用者的 id。

以 Next.js 專案為例,當你依照 Next.js SDK 指南 將 Logto 整合進專案後,可以使用 Logto SDK 取得使用者資訊,並產生對應的 JWT 與 Supabase 互動。

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` 值即為 user id。
const supabase = getSupabaseClient(cliams.sub);

const { data: posts } = await supabase.from('posts').select('*');

return <PostList posts={posts} />;
}

機器對機器 app​

機器對機器(M2M, Machine-to-machine)常用於你的應用程式需直接與資源伺服器溝通的情境,例如靜態服務每日拉取貼文等。

你可以參考 機器對機器:使用 Logto 驗證 (Authentication) 指南進行機器對機器 app 的驗證 (Authentication)。Supabase 與機器對機器 app 的整合方式與 Native app、SPA 類似(詳見「Native app 或 SPA」章節),即先從 Logto 取得存取權杖,再透過受保護的後端 API 驗證。

但需注意,Native app 與 SPA 通常面向終端使用者,因此取得的 user id 代表使用者本身;而機器對機器 app 的存取權杖則代表應用程式本身,access token payload 中的 sub 欄位是 M2M app 的 client id,而非特定使用者。因此,開發時需明確區分哪些資料是給 M2M app 使用。

此外,若你希望特定 M2M app 以整個服務的身分存取 Supabase、繞過 RLS 限制,可以使用 Supabase 的 service_role secret 建立 Supabase client。這在你需要執行管理或自動化任務、必須存取所有資料且不受個別使用者 RLS 政策限制時非常有用。

service_role secret 可在與 JWT secret 相同的頁面找到:

Service role secret

建立 Supabase client 時,使用 service_role secret,該 client 即可存取資料庫中的所有資料:

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