Skip to main content

将 Clerk 与 Supabase 集成用于身份验证、安全和数据同步

⚠️注意,clerk 生产环境和开发环境的登录行为不同,默认是开发环境,生产环境下需要配置与 oauth 类似的 client。

reference:

  1. https://clerk.com/blog/sync-clerk-user-data-to-supabase
  2. https://clerk.com/docs/guides/development/integrations/databases/supabase
  3. https://clerk.com/blog/nextjs-supabase-clerk

在现代 Web 开发中,将专业化的服务结合起来可以极大地提高开发效率和应用程序的健壮性。Clerk 提供了顶级的用户身份验证和管理解决方案,拥有美观的预构建组件和强大的 API。而 Supabase 则是一个开源的 Firebase 替代品,提供了强大的数据库、行级安全性 (RLS)、即时 API 和边缘函数等功能。

将这两者结合,您可以构建一个既拥有流畅用户体验又具备企业级安全性的应用程序。

本教程将分为两个核心部分:

  1. 安全的数据访问:我们将首先设置 Clerk 作为 Supabase 的身份验证提供商,并利用 Supabase 强大的行级安全性 (RLS) 来确保用户只能访问他们自己的数据。这是集成的基础。
  2. 用户数据同步:然后,我们将深入探讨为什么以及如何使用 Clerk Webhooks 和 Supabase 边缘函数,将 Clerk 中的用户数据(如姓名、头像)同步到您的 Supabase 数据库中,以实现更复杂的查询和更优的性能。

第一部分:使用 Clerk 和 Supabase RLS 实现安全数据访问

目标:让用户通过 Clerk 登录,并能安全地在 Supabase 数据库中创建和读取属于他们自己的数据,而无需担心数据泄露。

第 1 步:将 Clerk 设置为 Supabase 的身份验证提供商

💡 为什么需要这一步? 为了让 Supabase 能够信任来自 Clerk 的用户,您必须告诉 Supabase:“Clerk 是一个合法的身份验证来源”。完成此设置后,当您的应用向 Supabase 发出请求时,它会附带一个由 Clerk 签发的 JWT(JSON Web Token)。Supabase 会验证这个 JWT,并从中解析出用户ID,从而知道“是谁”在发出请求。这是实现后续所有安全策略的基础。

  1. 在 Clerk Dashboard 中获取您的 Clerk 域

    • 登录您的 Clerk 账户,进入对应的应用程序 Dashboard。
    • 在左侧导航栏中找到 Integrations > Supabase
    • 选择配置选项并点击 Activate Supabase integration。Clerk 会为您生成一个唯一的 Clerk Domain
    • 复制这个 Clerk Domain,它看起来像 https://*.clerk.accounts.dev
  2. 在 Supabase Dashboard 中添加 Clerk 提供商

    • 登录您的 Supabase 项目。
    • 导航到 Authentication > Providers
    • 在列表中找到并点击 Clerk
    • 将您刚刚复制的 Clerk Domain 粘贴到输入框中,然后保存。

至此,Supabase 已经正式“认识”并信任您的 Clerk 应用程序了。

第 2 步:创建安全的数据库表并配置 RLS

💡 什么是 RLS,为什么它如此重要? 行级安全性(Row-Level Security)是 PostgreSQL(Supabase 的核心)的一项强大功能。它允许您为数据库表定义精细的访问策略。简单来说,RLS 就像是数据库表的“保安”,它会检查每一个进来的查询(SELECT, INSERT, UPDATE, DELETE),并根据您定义的规则决定该查询是否可以访问某一行数据。这能从根本上防止数据泄露,即使您的前端代码或 API 出现漏洞,恶意用户也无法访问到不属于他们的数据。

  1. 创建 tasks 我们将创建一个简单的 tasks 表作为示例。进入 Supabase 的 SQL Editor,运行以下代码:

    -- 创建一个 "tasks" 表
    create table tasks (
    id serial primary key,
    name text not null,
    -- 这个字段将存储创建此任务的用户的 Clerk ID
    user_id text not null default auth.jwt()->>'sub'
    );

    -- 在此表上启用行级安全性 (RLS)
    alter table "tasks" enable row level security;

    代码解释

    • user_id text not null: 我们创建了一个 user_id 列,用于标识哪一个用户拥有这个任务。
    • default auth.jwt()->>'sub': 这是最关键的部分。
      • auth.jwt() 是一个 Supabase 内置函数,它可以读取当前请求中由 Clerk 提供的 JWT。
      • ->>'sub' 从 JWT 的载荷 (payload) 中提取 sub (Subject) 声明,这在 Clerk 的 JWT 中代表的就是用户的唯一 ID (例如 user_29w83sx...)。
      • default 关键字意味着,当您插入一条新任务而没有指定 user_id 时,数据库会自动将当前登录用户的 ID 填入该字段。这极大地简化了应用层代码,并确保了所有权信息的准确性。
  2. 创建 RLS 策略 现在,我们需要为 tasks 表定义“保安”的规则。继续在 SQL Editor 中运行以下策略:

    -- 策略1: 用户只能查看(SELECT)他们自己的任务
    create policy "用户可以查看自己的任务"
    on "public"."tasks"
    for select
    to authenticated
    using ( (select auth.jwt()->>'sub') = user_id );

    -- 策略2: 用户只能为自己插入(INSERT)任务
    create policy "用户必须为自己插入任务"
    on "public"."tasks"
    for insert
    to authenticated
    with check ( (select auth.jwt()->>'sub') = user_id );

    代码解释

    • for select ... using (...): 这条 SELECT 策略的 using 子句定义了一个条件。只有当 user_id 列的值 等于 发出请求的用户的 ID (auth.jwt()->>'sub') 时,该行数据才可见。
    • for insert ... with check (...): 这条 INSERT 策略的 with check 子句同样定义了一个条件。它确保了用户在尝试插入新数据时,该数据的 user_id 必须 是他们自己的 ID。这可以防止用户创建属于别人的任务。结合前面设置的 default 值,这个机制变得非常安全和自动化。

第 3 步:连接您的 Next.js 前端应用

现在,后台的准备工作已经完成,让我们在前端应用中获取数据。

  1. 安装 Supabase 客户端库 在您的项目终端中运行:

    npm install @supabase/supabase-js
  2. 设置环境变量 在您的 .env.local 文件中添加 Supabase 的 URL 和 anon key。您可以在 Supabase Dashboard 的 Project Settings > API 中找到它们。

    NEXT_PUBLIC_SUPABASE_URL=YOUR_SUPABASE_URL
    NEXT_PUBLIC_SUPABASE_KEY=YOUR_SUPABASE_ANON_KEY

    重要提示: NEXT_PUBLIC_ 前缀是 Next.js 的约定,它允许这些变量在浏览器(客户端)代码中被访问。

  3. 编写前端代码以获取数据 在您的页面组件(例如 app/page.tsx)中,您可以这样编写代码:

    'use client'
    import { useEffect, useState } from 'react'
    import { useSession, useUser } from '@clerk/nextjs'
    import { createClient } from '@supabase/supabase-js'

    // 定义 Supabase 客户端的类型,以便 TypeScript 知道你的数据库结构
    // 这不是必需的,但是一个好习惯
    import { Database } from '../database.types'

    export default function Home() {
    const { user } = useUser();
    const { session } = useSession(); // useSession 用于获取 Clerk 会话令牌
    const [tasks, setTasks] = useState<any[]>([]);

    // --- 关键部分:创建注入了 Clerk 令牌的 Supabase 客户端 ---
    // 只有在 session 对象可用时才创建客户端
    const supabase = session
    ? createClient<Database>(
    process.env.NEXT_PUBLIC_SUPABASE_URL!,
    process.env.NEXT_PUBLIC_SUPABASE_KEY!,
    {
    global: {
    // 这是魔法发生的地方
    fetch: (url, options = {}) => {
    const headers = new Headers(options.headers);
    // 将 Clerk 的 JWT 注入到每个请求的 Authorization 头中
    headers.set('Authorization', `Bearer ${session.getToken({ template: 'supabase' })}`);
    return fetch(url, { ...options, headers });
    },
    },
    }
    )
    : null;

    // 使用 useEffect 在组件加载且 supabase 客户端可用时获取数据
    useEffect(() => {
    if (!supabase) return;

    const loadTasks = async () => {
    // 看,查询多么简单!我们不需要写 'where user_id = ...'
    // 因为 RLS 已经在数据库层面为我们处理了安全问题。
    const { data, error } = await supabase.from('tasks').select();
    if (data) setTasks(data);
    if (error) console.error('Error fetching tasks:', error);
    };

    loadTasks();
    }, [supabase]); // 当 supabase 客户端准备好时触发

    // ... (此处省略了添加任务的表单和显示任务的 JSX)
    return (
    <div>
    <h1>My Tasks</h1>
    {tasks.map(task => <p key={task.id}>{task.name}</p>)}
    </div>
    );
    }

    代码解释:

    • createClient 的特殊配置: 我们没有使用标准的 createClient。相反,我们通过 global.fetch 选项进行拦截。
    • headers.set('Authorization', ...): 在每次向 Supabase 发出请求之前,我们都会动态地从 Clerk 的 session 中获取最新的 JWT,并将其放入 Authorization 请求头中。Supabase 会接收并验证这个令牌,从而执行我们之前设置的 RLS 策略。
    • 简洁的查询: 注意 supabase.from('tasks').select() 是多么的干净。我们完全不用在前端代码中担心用户 ID 的过滤,因为 RLS 在数据库端强制执行了安全访问,使得前端代码更简洁、更安全。

第二部分:同步 Clerk 用户数据至 Supabase

💡 为什么需要同步用户数据? 到目前为止,我们的 tasks 表只存储了 user_id(例如 user_29w83...)。但如果我们想在任务旁边显示用户的名字或头像呢?

您有两种选择:

  1. 实时请求 Clerk API:每次需要用户信息时,都从前端或后端向 Clerk 的 API 发出请求。这能保证数据最新,但会增加网络延迟和请求次数,可能触及 API 速率限制。
  2. 数据同步:在用户创建或更新时,将他们的信息(ID, 名字, 姓氏, 头像URL等)复制一份到我们自己的 Supabase 数据库中的 users 表。

同步的优势

  • 性能:在数据库内部进行 JOIN 查询(例如,连接 tasksusers 表)比跨网络调用外部 API 要快得多。
  • 关系数据:您可以在您的数据库中建立真正的外键关系,确保数据完整性。
  • 复杂的查询和分析:您可以直接在 Supabase 中对用户数据进行复杂的 SQL 查询和分析,而无需将数据导出。

我们将使用 Clerk Webhooks 和 Supabase 边缘函数来实现这种高效的同步。

第 1 步:创建并部署 Supabase 边缘函数

工作流程:当 Clerk 中发生事件(如用户创建)时 -> Clerk 发送一个 Webhook(一个 HTTP POST 请求)到我们指定的 URL -> Supabase 边缘函数接收此请求 -> 函数验证请求的合法性并将其中的数据写入数据库。

  1. 在您的项目中创建新函数

    npx supabase functions new clerk-webhooks

    这会在 supabase/functions/clerk-webhooks 目录下创建一个新的函数。

  2. 为函数添加 Clerk 后端依赖 导航到该目录并运行:

    cd supabase/functions/clerk-webhooks
    npm install @clerk/clerk-sdk-node svix
  3. 编写函数代码 (index.ts) 用以下代码替换 index.ts 文件的内容:

    import { createClient } from '@supabase/supabase-js'
    import { Webhook } from 'svix'
    import { WebhookEvent } from '@clerk/clerk-sdk-node'

    const WEBHOOK_SECRET = Deno.env.get('CLERK_WEBHOOK_SECRET')

    Deno.serve(async (req) => {
    if (!WEBHOOK_SECRET) {
    return new Response('Error: Webhook secret not configured.', { status: 500 })
    }

    // 1. 验证请求的合法性
    const headers = Object.fromEntries(req.headers);
    const payload = await req.json();
    const wh = new Webhook(WEBHOOK_SECRET);

    let event: WebhookEvent;
    try {
    event = wh.verify(JSON.stringify(payload), headers) as WebhookEvent;
    } catch (err) {
    console.error('Error verifying webhook:', err);
    return new Response('Error: Could not verify webhook.', { status: 400 });
    }

    // 2. 创建一个拥有管理员权限的 Supabase 客户端
    // 注意:这里使用的是 SERVICE_ROLE_KEY,它可以绕过 RLS
    const supabase = createClient(
    Deno.env.get('SUPABASE_URL')!,
    Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
    );

    // 3. 根据事件类型处理数据
    switch (event.type) {
    case 'user.created': {
    const { id, first_name, last_name, image_url, created_at, updated_at } = event.data;
    const { data, error } = await supabase
    .from('users')
    .insert([{
    id: id,
    first_name: first_name,
    last_name: last_name,
    avatar_url: image_url,
    created_at: new Date(created_at).toISOString(),
    updated_at: new Date(updated_at).toISOString(),
    }]);

    if (error) {
    console.error('Error creating user:', error);
    return new Response(JSON.stringify({ error: error.message }), { status: 500 });
    }
    break;
    }
    case 'user.updated': {
    const { id, first_name, last_name, image_url, updated_at } = event.data;
    const { data, error } = await supabase
    .from('users')
    .update({
    first_name: first_name,
    last_name: last_name,
    avatar_url: image_url,
    updated_at: new Date(updated_at).toISOString(),
    })
    .eq('id', id);

    if (error) {
    console.error('Error updating user:', error);
    return new Response(JSON.stringify({ error: error.message }), { status: 500 });
    }
    break;
    }
    // ... 您可以添加更多 case 来处理其他事件,如 'user.deleted'
    }

    return new Response('Webhook received successfully.', { status: 200 });
    })
    ``` **代码解释**:
    * **`wh.verify(...)`**: 这是至关重要的安全步骤。它使用一个共享的密钥来验证 Webhook 请求确实来自 Clerk,而不是恶意攻击者伪造的。
    * **Service Role Key**: 我们使用 `SUPABASE_SERVICE_ROLE_KEY` 来初始化 Supabase 客户端。这个密钥拥有最高权限,可以绕过所有 RLS 策略。这是必需的,因为这个函数需要代表系统写入所有用户的数据,而不是某个特定用户。
    * **`switch (event.type)`**: 这个逻辑根据 Clerk 发送的事件类型(`user.created`, `user.updated` 等)来执行不同的数据库操作(`insert`, `update`)。

  4. 部署函数 回到项目根目录并运行:

    npx supabase functions deploy clerk-webhooks --no-verify-jwt
    ``` **`--no-verify-jwt`** 标志非常重要。它告诉 Supabase 不要尝试验证这个函数的调用者是否提供了用户 JWT,因为我们的验证机制是基于 Webhook 密钥的。

第 2 步:配置 Clerk Webhook

  1. 在 Clerk Dashboard 中创建 Webhook 端点

    • 导航到 Webhooks,点击 Add Endpoint
    • Endpoint URL 字段中,粘贴您刚刚部署的 Supabase 边缘函数的 URL(您可以在 Supabase Dashboard 的 Edge Functions 页面找到它)。
    • Messages 部分,选择您关心的事件,例如 user.createduser.updated
    • 点击 Create
  2. 配置密钥

    • 创建端点后,Clerk 会显示一个 Signing Secret。点击复制它。
    • 回到您的 Supabase 项目,导航到 Project Settings > Edge Functions > Secrets
    • 创建一个新的密钥,名称为 CLERK_WEBHOOK_SECRET,值为您刚刚从 Clerk 复制的密钥。同时,也请确保 SUPABASE_URLSUPABASE_SERVICE_ROLE_KEY 已经作为 Secrets 添加。

第 3 步:测试同步

在 Clerk 的 Webhook 配置页面,您可以选择一个事件并点击 Send Example。然后立即去 Supabase 的数据表编辑器中查看您的 users 表,您应该会看到一条新的用户记录被成功创建了。

结论与总结

通过本教程,您已经构建了一个安全、高效且可扩展的系统:

  1. Clerk 负责身份验证,提供了优秀的用户体验和管理功能。
  2. Supabase RLS 负责数据安全,在数据库层面确保了用户数据的隔离,使您的应用坚如磐石。
  3. Clerk Webhooks 和 Supabase 边缘函数负责数据同步,通过将用户信息复制到您的本地数据库,极大地提升了性能和查询灵活性。

现在,当您需要显示一个任务及其创建者的名字时,您只需在 Supabase 中执行一个简单的 JOIN 查询,既快速又高效,而这一切的背后都有强大的安全策略在保驾护航。

技术 | Clerk+Next.js 从开发到生产配置全流程与坑点一览

来源链接:https://blog.ender-wiggin.com/posts/note-clerk-and-next/

https://clerk.com/ 号称是最开箱即用的登录鉴权 serverless 服务,确实可以帮助开发者避免前期重复无聊的鉴权逻辑,从而快速开发一些小玩具。但是这里面也有不少坑。下面汇总从开发到生产的配置流程,以及其中的坑点与避坑建议。

0. 技术栈选择

本文使用 Next.js 作为前端框架,结合 Clerk 实现登录功能。如果想要实现全面的白嫖方案,搭建功能强大(但没有太多使用量)的网站,建议增加如下配置:

  • https://neon.tech/:继 PlanetScale 背刺收费之后的第二个 serverless 数据库界赛博活菩萨。另外的选择还有 https://supabase.com/,同样基于 Postgres,但在国内访问较慢,不太建议使用。
  • 域名 & Cloudflare:域名可以自己买一个喜欢的,或去白嫖一个不太好看的免费域名使用;然后通过 Cloudflare 绑定 SSL 证书。

在这种全副武装之下,这网站可以说是白嫖界的天花板了:有前端、有边缘函数还有后端。抛开用户量暴涨带来的额外计费的可能性,限制这网站能力边界的就只有自己的编码能力了。

1. Hello World! 项目搭建

首先需要明确的一点是:不论是 Clerk,还是其它鉴权方案(NextAuth/Supabase Auth/Firebase Auth),都是封装了底层鉴权基础知识的技术黑盒。如果还不太理解鉴权原理,建议先学习基础鉴权知识(如 JWT、OAuth、加密算法),这样在选择这些服务时不至于乱花渐欲迷人眼。

创建新项目的方式有很多种:

  1. (推荐)找一个 Next + Clerk 的 starter,例如 https://github.com/clerk/clerk-nextjs-demo-app-router,直接克隆。这种方式适合想快速体验 Clerk 功能的人。
  2. 从其它 starter 开始搭建,然后整合 Clerk。这种方式更灵活。最原始的是从 https://nextjs.org/docs/getting-started/installation 开始创建,随后按 https://clerk.com/docs/quickstarts/nextjs 的指引配置。

2. 配置 Clerk 开发环境

Clerk 中最重要的两个概念是中间件和开发/生产环境。一般实践是:本地开发使用 Clerk 开发环境;项目正式上线后必须配置生产环境。在代码侧区分环境使用的是环境变量。

配置环境变量的步骤如下(也可参考 https://clerk.com/docs/quickstarts/setup-clerk):

  1. 访问 https://dashboard.clerk.com/sign-up 注册或登录。
  2. 点击 Create application 创建一个应用,也就是当前开发的项目。
  3. 配置鉴权选项(如第三方登录提供商)。注意:第三方登录提供商在开发环境可以随意增加,但在生产环境每一个都需要额外配置,务必量力而行。
  4. 进入 https://dashboard.clerk.com/last-active?path=api-keys 找到 API keys(一个公钥、一个私钥),将其复制到项目的环境变量里。

补充:环境变量文件通常是项目根目录下的 .env(如果没有可手动创建,或参考 starter 的 .env.example.env.template 等)。内容大致如下:

NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=需要粘贴的内容
CLERK_SECRET_KEY=需要粘贴的内容
NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in
NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up
# NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/dashboard
# NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/dashboard

切记:不要将 .env 上传到公共网络(如 GitHub)。

至此,如果是从 starter 克隆的项目,通常已经可以运行。如果是手动创建,还需配置中间件和 <ClerkProvider>,可参考 https://clerk.com/docs/quickstarts/nextjs

3. 中间件 Middleware 配置

这是 Clerk 的第一个坑。Clerk 有两种不同版本的鉴权中间件,务必注意区别:

  • 旧版 authMiddleware():虽然可以使用,但已 deprecated,见 https://clerk.com/docs/references/nextjs/auth-middleware。该中间件会默认拦截整个项目所有页面,必须显式声明未登录状态下允许访问的页面。稍有不慎,就可能出现所有页面 404。建议弃用。
  • 新版 clerkMiddleware():默认不保护任何路由,更符合开发规范,不会禁用原本可正常使用的功能。推荐使用。

4. 组件配置

接下来是在项目中加入注册/登录按钮,这也是 Clerk 最省心的一步。很多场景下,一个 <SignedIn> 就能实现登录态控制。

步骤如下:

  1. 在项目根布局中配置 <ClerkProvider>(Next.js 的 Pages Router 或 App Router 根布局)。由于 Next.js 路由模式差异,细节略有不同,这里不展开。
  2. 在需要的地方加入注册/登录/用户信息组件。建议参考官网与 starter,一步步照做即可。

如果使用官方 starter,这一步通常不用改动就可以直接使用。

5. 走向生产环境

从用户体验看,测试环境与生产环境几乎无感;除了跳转第三方登录时会显示一个 Clerk 开发环境域名外,差异并不大,似乎不上生产也能用。

但是!Clerk 开发环境有一个非常坑的点:Google Search Console 无法访问,会直接报 401,也就是完全没有 SEO!这个问题我很晚才发现,直到这帖提供线索:https://stackoverflow.com/questions/76816776/getting-unauthorised-401-for-my-nextjs-13-app-on-search-console

因此,只要你的网站是要上线的,就要配置生产环境。步骤如下(也可参考 https://clerk.com/docs/deployments/overview#deploy-your-clerk-app-to-production):

  1. 创建生产实例。
  2. 更新环境变量。注意:只要在本地环境使用生产环境的 API key,就会报错。解决方案是本地用开发环境变量,部署到云端后再改用生产环境变量,严格隔离。
  3. 配置第三方鉴权和路径。生产实例和开发实例几乎完全不一样,所有配置项都要重新配置。
  4. 准备并绑定自己的域名,生产环境有域名是合理且必要的。
  5. 每个 OAuth 证书都需要去对应平台自己配置。建议按 https://clerk.com/docs/authentication/social-connections/overview 慢慢走。大多数 OAuth 的配置方案类似,第一次会比较累,但一劳永逸。
  6. 配置 DNS 记录。生产环境需要使用 Clerk 的一些服务来实现会话管理和邮件管理,需要把子域名和 Clerk 绑在一起。验证通常较慢(可能一天);并且在 Cloudflare 需要把该子域名设置为 DNS only。具体操作参考 https://clerk.com/docs/deployments/overview#deploy-your-clerk-app-to-production

等一切就绪后,就可以开始验证:一方面自己登录网站看看能否正常登录;另一方面访问 Google Search Console 看看能否成功登记自己的子域名且不再出现 401。