Authany 与 Supabase¶
用户身份验证是任何现代 Web 应用的基础组成部分,但要把它做对却出了名地困难。这正是 Authany 的用武之地。Authany 开箱即用地处理用户注册、登录流程、会话管理,以及生物识别登录、双重验证、社交登录等高级安全功能。
将 Authany 与 Supabase 的 Postgres 数据库以及强大的行级安全(Row-Level Security,RLS)搭配使用,可以为您构建面向单个用户的应用打下安全、可扩展的基础。
在本指南中,您将学习如何使用 React.js,把 Authany 和 Supabase 集成起来,构建一个基础的 CRUD 应用。

一个简单的乐器追踪应用,用户使用 Authany 登录后即可管理自己的收藏
我们将介绍如何使用 Authany 对用户进行身份验证、通过 Supabase Edge Function 交换令牌、基于 Authany 签发的 JWT 应用 RLS 策略,并将这一切整合进使用 Vite 搭建的现代 React 前端中。
┌─────────────┐ ┌──────────────┐ ┌──────────────┐
│ Browser │ │ Authgear │ │ Supabase │
│ (React App)│◄────────►│ (Auth IdP) │ │ (Database) │
└──────┬──────┘ └──────────────┘ └──────┬───────┘
│ │
│ 1. Login with Authgear │
│ 2. Get Authgear JWT (access token) │
│ 3. Call Supabase with Authgear JWT (request data)│
└──────────────────────────────────────────────────►
│ │
│ 4. Exchange JWT (Edge Function) │
│ 5. Return Supabase JWT │
│◄──────────────────────────────────────────────────
│ │
│ 6. Access data with Supabase JWT (enforce RLS) │
└──────────────────────────────────────────────────►
完整示例代码¶
完整的示例应用源代码已发布在 GitHub 上,您可以将其作为自己项目的起点:https://github.com/authgear/authgear-example-supabase
准备工作¶
- Authany 账号与项目 – 注册 Authany。
- Supabase 账号与项目 – 在 supabase.com 注册
第 1 步:搭建 Authany 项目¶
首先,配置 Authany 来为您的应用处理身份验证。
- 创建一个 Authany 应用: 登录 Authany 管理台,创建一个新应用,应用类型选择单页应用。
- 配置 OAuth 重定向: 在 Authany 应用设置中,设置重定向 URI:
- 重定向 URI:
http://localhost:5173/auth-redirect—— 登录成功后 Authany 会将浏览器重定向到这里(我们的 React 应用会处理这个路由)。 - 登出后重定向 URI:
http://localhost:5173/—— 用户登出后跳转到的地址。 - 记下您的 Authany 凭据: 创建完成后,记下以下信息:
- 您应用的 Authany 端点(形如
https://<your-project>.authgear.cloud)。 - 您 Authany 应用的客户端 ID。
在 React 应用和 Supabase 函数中初始化 Authany 时会用到这些值。
第 2 步:搭建 Supabase 项目¶
接下来,准备您的 Supabase 后端。
- 创建一个 Supabase 项目: 登录 Supabase 控制台,创建一个新项目(选择组织、项目名称、数据库密码和地区)。
- 获取项目密钥: 在您的 Supabase 项目中,进入Project Settings → API → API keys。复制您项目的 Publishable Key 和 URL(例如
https://xyzcompany.supabase.co)。 -
获取 JWT Secret: 仍在 Project settings 中,进入 JWT Keys,找到您项目的 Legacy JWT Secret。这是 Supabase 用来为 RLS 签名和验证 JWT 的密钥。复制这个 JWT secret 值——我们会在 Edge Function 的配置中用到它。

从 JWT Keys 页面复制 Legacy JWT Secret
-
(可选) 安装 Supabase CLI: 如果您打算用 CLI 部署 Edge Function,请按照 Supabase 的说明进行安装。您也可以通过控制台界面部署该函数。
第 3 步:创建一个用于交换 JWT 的 Edge Function¶
现在我们来搭建关键的一环:一个 Supabase Edge Function,用于接收 Authany 的 JWT,并返回一个用 Supabase 密钥签名的新 JWT。
- 进入 "Edge Functions" -> "Secrets",添加以下两个密钥:
AUTHGEAR_ENDPOINT= 您的 Authany 应用端点(例如https://myapp.authanyid.com)。SB_JWT_SECRET= 您的 Supabase JWT secret(来自上面的第 2 步)。- 部署该函数
- 在 Supabase 网页控制台中进入 "Edge Functions" -> "Functions"。复制粘贴以下代码并在此部署,将函数命名为
exchange-jwt。 - 您也可以使用 Supabase CLI,从本地机器构建并部署该 Edge Function 到您的 Supabase 项目。
// supabase/functions/exchange-jwt/index.ts
import "jsr:@supabase/functions-js/edge-runtime.d.ts"
import jwt from "npm:jsonwebtoken";
import jwks from "npm:jwks-rsa";
import * as jose from "https://deno.land/x/jose@v4.14.4/index.ts";
// CORS headers for cross-origin requests
const corsHeaders = {
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Headers": "Authorization, Content-Type",
"Access-Control-Allow-Methods": "GET",
"Access-Control-Max-Age": "3600"
};
// Read environment variables
const authgearEndpoint = Deno.env.get("AUTHGEAR_ENDPOINT");
const supabaseJwtSecret = Deno.env.get("SB_JWT_SECRET");
if (!authgearEndpoint || !supabaseJwtSecret) throw new Error("Missing env vars");
// Get JWKS URI from Authgear's OpenID configuration
async function getJwksUri(): Promise<string> {
const configUrl = `${authgearEndpoint}/.well-known/openid-configuration`;
const response = await fetch(configUrl);
const config = await response.json();
return config.jwks_uri;
}
// Extract Bearer token from Authorization header
function extractToken(req: Request): string | null {
const authHeader = req.headers.get("Authorization");
if (!authHeader) return null;
const parts = authHeader.split(" ");
if (parts.length !== 2 || parts[0] !== "Bearer") return null;
return parts[1];
}
// Verify JWT token with Authgear's public key
async function verifyToken(token: string): Promise<any> {
// Decode token to get key ID
const decoded = jwt.decode(token, { complete: true }) as any;
if (!decoded?.header?.kid) {
throw new Error("Invalid token: missing key ID");
}
// Get JWKS URI and create JWKS client
const jwksUri = await getJwksUri();
const jwksClient = jwks({ jwksUri });
// Get the signing key
const key = await jwksClient.getSigningKey(decoded.header.kid);
const signingKey = key.getPublicKey();
// Verify the token
const options = {
algorithms: ["RS256"],
issuer: authgearEndpoint,
};
return jwt.verify(token, signingKey, options);
}
// Sign a new JWT with Supabase secret
async function signSupabaseJwt(payload: any): Promise<string> {
payload.role = "authenticated"; // Required by Supabase
// Add or modify any other claims you need for RLS policies
// payload.some_claim = "some claim";
// Sign with Supabase JWT secret
const supabaseSecret = new TextEncoder().encode(supabaseJwtSecret);
const supabaseJwt = await new jose.SignJWT(payload)
.setProtectedHeader({ alg: "HS256", typ: "JWT" })
.setIssuer("supabase")
.setIssuedAt(payload.iat)
.setExpirationTime(payload.exp || "")
.sign(supabaseSecret);
return supabaseJwt;
}
// Main function handler
Deno.serve(async (req: Request) => {
// Handle CORS preflight requests
if (req.method === "OPTIONS") {
return new Response("ok", { headers: corsHeaders });
}
try {
// Extract token from request
const token = extractToken(req);
if (!token) {
return new Response(
JSON.stringify({ error: "Missing or invalid Authorization header" }),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 401
}
);
}
// Verify the token
const verified = await verifyToken(token);
// Sign a new JWT with Supabase secret
const supabaseJwt = await signSupabaseJwt(verified);
return new Response(
JSON.stringify({ supabaseJwt }),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 200,
}
);
} catch (error) {
console.error("Token verification failed:", error.message);
return new Response(
JSON.stringify({ error: "Token verification failed" }),
{
headers: { ...corsHeaders, "Content-Type": "application/json" },
status: 401,
}
);
}
});
部署完成后,您的函数可以通过以下地址访问:
第 4 步:创建数据库表和 RLS 策略¶
接下来,我们在 Supabase 中搭建数据库表和 RLS 规则。我们将创建一个表(instruments)来存储用户拥有的物品,并使用 RLS 确保每个用户只能操作自己的行。
在您的 Supabase 项目中打开 SQL 编辑器,运行以下 SQL 命令:
-- 1. Helper function to get the current Authgear user ID from the JWT
CREATE OR REPLACE FUNCTION current_user_id()
RETURNS TEXT AS $$
SELECT auth.jwt() ->> 'sub';
$$ LANGUAGE SQL STABLE;
我们创建了一个名为 current_user_id() 的 SQL 函数,用于从当前请求的 JWT 中返回 sub 声明(subject)。Supabase 的 Postgres 提供了一个 auth.jwt() 函数,用于暴露请求方 JWT 中的声明;auth.jwt() ->> 'sub' 会以文本形式提取 sub 字段。
-- 2. Instruments table: each row has a user_id to identify the owner
CREATE TABLE instruments (
id BIGSERIAL PRIMARY KEY,
created_at TIMESTAMPTZ DEFAULT NOW() NOT NULL,
name TEXT NOT NULL,
user_id TEXT NOT NULL -- store Authgear user’s ID (sub)
);
-- 3. Enable Row Level Security on the table
ALTER TABLE instruments ENABLE ROW LEVEL SECURITY;
instruments表有一个user_id列,用于为每件乐器存储对应 Authany 用户的 ID。- 我们在该表上启用了 RLS,这意味着默认情况下,除非有策略允许,否则任何行都无法被访问。
-- 4. RLS Policies:
-- Allow each authenticated user to SELECT rows where user_id = their own ID
CREATE POLICY "Users can read their own instruments"
ON instruments
FOR SELECT
TO authenticated
USING (user_id = current_user_id());
-- Allow INSERTs only if setting user_id to their own ID
CREATE POLICY "Users can insert their own instruments"
ON instruments
FOR INSERT
TO authenticated
WITH CHECK (user_id = current_user_id());
-- Allow UPDATE on rows they own
CREATE POLICY "Users can update their own instruments"
ON instruments
FOR UPDATE
TO authenticated
USING (user_id = current_user_id())
WITH CHECK (user_id = current_user_id());
-- Allow DELETE on rows they own
CREATE POLICY "Users can delete their own instruments"
ON instruments
FOR DELETE
TO authenticated
USING (user_id = current_user_id());
然后我们定义了四条策略,分别对应四种 CRUD 操作(SELECT、INSERT、UPDATE、DELETE)。每条策略都仅限于 authenticated 角色,并要求该行的 user_id 与来自 JWT 的用户 ID 相匹配。
第 5 步:使用 Supabase 客户端构建 React 应用¶
接下来是前端部分。我们将使用 Vite 创建一个 React 应用,集成 Authany 的 SDK 来做身份验证,并配置 Supabase 客户端,让它自动使用我们的令牌交换流程。
依赖项
- 使用
npm create vite@latest my-app -- --template react搭建一个 React 应用的脚手架 - 按照这里的指南搭建 SPA:React,搭建一个带有 Authany 身份验证的基础 React 应用
-
安装 Supabase 库
4. 在.env中配置环境变量,让 Supabase 的 URL 和 Publishable key 可用。您的.env.local应该形如:
VITE_SUPABASE_URL=https://your-project.supabase.co
VITE_SUPABASE_PUBLISHABLE_KEY=your-supabase-anon-key
VITE_AUTHGEAR_ENDPOINT=https://your-app.authanyid.com
VITE_AUTHGEAR_CLIENT_ID=your-authgear-client-id
VITE_AUTHGEAR_REDIRECT_URL=http://localhost:5173/auth-redirect
VITE_AUTHGEAR_LOGOUT_REDIRECT_URL=http://localhost:5173/
配置 Supabase 客户端
现在我们可以设置 Supabase JS 客户端,让它使用我们的 Authany 令牌。新建一个文件 my-app/src/lib/supabase.js,添加以下内容:
// my-app/src/lib/supabase.js
import { createClient } from '@supabase/supabase-js';
import authgear from '@authgear/web';
const SUPABASE_URL = import.meta.env.VITE_SUPABASE_URL;
const SUPABASE_ANON_KEY = import.meta.env.VITE_SUPABASE_PUBLISHABLE_KEY;
// Optional: caches to avoid redundant token exchanges
let cachedSupabaseJwt = null;
let cachedAuthgearToken = null;
// Function to exchange Authgear token for Supabase token
async function exchangeToken(authgearToken) {
// Return cached token if the Authgear token hasn’t changed
if (cachedAuthgearToken === authgearToken && cachedSupabaseJwt) {
return cachedSupabaseJwt;
}
// Call the Supabase Edge Function
const res = await fetch(`${SUPABASE_URL}/functions/v1/exchange-jwt`, {
headers: { Authorization: `Bearer ${authgearToken}` }
});
if (!res.ok) {
throw new Error(`Token exchange failed with status ${res.status}`);
}
const { supabaseJwt } = await res.json();
// Update cache and return the new JWT
cachedAuthgearToken = authgearToken;
cachedSupabaseJwt = supabaseJwt;
return supabaseJwt;
}
// Create Supabase client with custom auth settings
export const supabase = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, {
auth: {
autoRefreshToken: false, // Don't use Supabase’s token refresh
persistSession: false, // Don't store Supabase session (Authgear will handle session)
},
accessToken: async () => {
// This function runs before every Supabase request that needs auth
await authgear.refreshAccessTokenIfNeeded();
const authgearToken = authgear.accessToken;
if (!authgearToken) return null;
// Exchange Authgear token for a Supabase token:contentReference[oaicite:12]{index=12}:contentReference[oaicite:13]{index=13}
return exchangeToken(authgearToken);
},
});
在这个配置中:
- 我们禁用了 Supabase 客户端中的
autoRefreshToken和persistSession,这样 Authany 就是会话状态的唯一权威来源。 - 我们提供了一个异步的
accessToken函数。每当 Supabase 客户端需要用到 JWT 进行已验证身份的请求时,都会调用这个函数。在我们的例子中,就是每次对instruments表的查询。 - 在
accessToken()中,我们首先通过调用authgear.refreshAccessTokenIfNeeded()来确保 Authany 的令牌是最新的。 - 然后我们获取
authgear.accessToken——这是来自 Authany SDK 的当前 JWT。 - 如果令牌存在,我们会调用
exchangeToken()辅助函数,该函数会把 Authany 令牌传给 Edge Function 的端点,并接收返回的 Supabase JWT。
构建 CRUD 函数
登录后,使用 Supabase 客户端来获取和操作数据。例如,要加载当前用户的乐器列表,可以这样调用:
要添加一件新乐器,可以调用
类似地,要更新或删除,也可以在 Supabase 客户端中调用带有相应过滤条件的 .update(...) 或 .delete(...)
6. 运行并测试应用¶
现在可以测试整个系统了。启动开发服务器:
在浏览器中打开 http://localhost:5173,测试集成效果。
到此就完成了¶
您已经成功将 Authany 用于身份验证、Supabase 用于安全的按用户数据访问(借助行级安全)集成到了一起。
有了这样的配置,您就可以充分利用 Authany 强大的身份验证功能,例如社交登录、双重验证和安全的会话管理,同时借助 Supabase 可扩展的数据库和内置 RLS,让用户数据保持隔离和受保护。这种令牌交换方式让 Authany(或任何其他基于 JWT 的身份提供方)与 Supabase 搭配使用变得非常简单。