Supabase 自定义第三方认证

Jan 24, 2026 · 3865 字

Supabase 本身支持 OAuth 和基于 SAML2.0 的 SSO 登录,但有时我们需要集成一些自定义的第三方认证服务,比如公司内部的认证系统,此时就需要自己实现认证流程并接入到 Supabase 中。本文将介绍如何在 Nuxt 项目中集成自定义第三方认证,并将认证结果同步到 Supabase 的用户系统中。

准备工作

这里我们使用了一个由 Nuxt 和 Supabase 组成的项目,假设你已经完成了 Supabase 项目的创建,并在 Nuxt 中集成了 @nuxtjs/supabase。如果还没有,可以参考上一篇文章进行配置。

要接入的认证系统是标准的 SSO CAS 系统,拥有以下 API 端点:

  • /cas/login?service=:用户登录页面
  • /cas/validate?service=&ticket=:验证登录票据,返回用户名

为了集中统一配置,可以将以上内容写入到 nuxt.config.tsappConfig 中:

export default defineNuxtConfig({
  appConfig: {
    casBaseUrl: process.env.CAS_BASE_URL ?? "https://example.com/cas",
    casServiceUrl:
      process.env.CAS_SERVICE_URL ??
      encodeURIComponent("http://localhost:3000/api/cas"),
  },
});

@nuxtjs/supabase 默认会自动跳转所有调用了 useSupabaseUser() 的页面到 /login,在某些只是需要判断用户是否登录的页面,这个行为并不适合,所以我们需要在 nuxt.config.ts 中准确指定哪些页面需要登录:

export default defineNuxtConfig({
  supabase: {
    redirectOptions: {
      include: ["/admin(/*)?"],
      login: "/login",
      callback: "/",
    },
  },
});

实现认证流程

接下来我们需要实现认证流程,主要原理是先手动处理 CAS 流程,然后通过 Supabase 生成魔法链接登录用户。

首先创建一个 API 路由 server/api/cas.ts 来处理 CAS 的回调:

import { serverSupabaseServiceRole } from "#supabase/server";

export default defineEventHandler(async (event) => {
  const { casBaseUrl, casServiceUrl } = useAppConfig();
  const { ticket } = getQuery(event);
  const supabase = serverSupabaseServiceRole(event);

  const rawXml = await $fetch<string>(
    `${casBaseUrl}/serviceValidate?service=${casServiceUrl}&ticket=${ticket}`,
  );
  const name = rawXml.match(/<cas:user>(\d+)<\/cas:user>/)?.[1];

  if (!name) {
    throw createError({ statusCode: 400, statusMessage: "Invalid CAS ticket" });
  }

  // 如果 CAS 系统返回的字段有邮箱可以直接使用,否则需要构造一个内部邮箱
  const email = `${name}@id.internal`;

  // TODO
});

然后我们需要判断 Supabase 中是否已经存在该用户, Supabase 内部没有直接通过 email 查询用户是否存在的接口,可以通过查询 public.profiles 表来实现:

let { data: user } = await supabase
  .from("profiles")
  .select("id")
  .eq("name", name)
  .single();

或者你也可以创建一个数据库函数:

CREATE OR REPLACE FUNCTION get_user_id_by_email(email TEXT)
RETURNS TABLE (id uuid)
SECURITY DEFINER
AS $$
BEGIN
RETURN QUERY SELECT id FROM auth.users WHERE email = $1;
END;
$$ LANGUAGE plpgsql;

然后调用:

let { data: user } = await supabase.rpc("get_user_id_by_email", { email });

之后我们就可以根据用户是否存在来创建用户,并生成魔法链接发送给用户:

if (!user) {
  const { data } = await supabase.auth.admin.createUser({
    email,
  });
  user = data.user!;
  await supabase.from("profiles").insert({ id: user.id, name });
}

const { data } = await supabase.auth.admin.generateLink({
  type: "magiclink",
  email,
});
const action_link = data.properties?.action_link;
if (action_link) {
  sendRedirect(event, action_link);
}
throw createError({
  statusCode: 500,
  statusMessage: "Failed to generate magic link",
});

如果你启用了 SSR

显然 Nuxt 和 @nuxtjs/supabase 都是默认开启 SSR 的,所以你很有可能会遇到在前端成功跳转认证,并获得 access_token 后,但用户仍然没有登录的情况。这是因为在 SSR 模式下,Supabase 使用的是 PKCE 流,而不是传统 implicit 流。如果你在前端尝试手动初始化:

const supabase = useSupabaseClient();
console.log(supabase.auth.initialize());

你会在控制台看到如下问题:

AuthPKCEGrantCodeExchangeError: Not a valid PKCE flow url.

这个问题在 Supabase 文档的 Signing in with Magic Link 部分有具体写到。但是文档只给出了在邮件模板中插入 hashToken 供 PKCE 使用的方法,后端 API 并没有直接生成带 hashToken 的魔法链接的接口。

所以我们需要自己实现这个功能,在上文我们配置了 callback/,所以我们可以在首页处理这个逻辑,你也可以将其写到 app.vue 中:

watchEffect(() => {
  if (route.hash) {
    const url = new URLSearchParams(route.hash.slice(1));
    const accessToken = url.get("access_token") || "";
    supabase.auth.setSession({
      access_token: accessToken,
      refresh_token: accessToken,
    });
  }
});

现在你就可以愉快地使用 Supabase 自定义第三方认证了!

粤ICP备2025414119号 粤公网安备44030002006951号

© 2026 Saurlax · Powered by Astro