在 Astro 中使用 Supabase 认证
Learn how to configure Supabase Auth for Astro with server-side rendering.
快速入门 #
🌐 Quickstart
去 database.new 创建一个新的 Supabase 项目。
你的新数据库有一个用于存储用户的表。你可以通过在 SQL 编辑器 中运行一些 SQL 来看到这个表目前是空的。
select * from auth.users;使用 npm create 命令创建一个新的 Astro 应用。
探索适用于你 Supabase 应用的即插即用 UI 组件。
基于 shadcn/ui 构建的 UI 组件,通过一个命令连接到 Supabase。
🌐 UI components built on shadcn/ui that connect to Supabase via a single command.
npm create astro@latest my-appcd my-app安装 @supabase/supabase-js 客户端库,@supabase/ssr 用于服务器端认证,以及 @astrojs/node 适配器以启用服务器端渲染。
npm install @supabase/supabase-js @supabase/ssr @astrojs/node更新你的 astro.config.mjs 来启用使用 Node 适配器的服务端渲染。
import { defineConfig } from "astro/config";import node from "@astrojs/node";export default defineConfig({ output: "server", adapter: node({ mode: "standalone", }),});创建一个 .env.local 文件,并用你的 Supabase 连接变量填充:
PUBLIC_SUPABASE_URL=your-project-urlPUBLIC_SUPABASE_PUBLISHABLE_KEY=sb_publishable_key获取 API 详情 #
🌐 Get API details
要与数据库表中的数据进行交互,你可以使用封装了自动生成的数据 API 端点的客户端库,并使用来自项目 Connect 对话框的项目 URL 和密钥进行认证。
🌐 To interact with data in database tables, you use the client libraries that wrap the auto-generated Data API endpoints, authenticating using the Project URL and key from the project Connect dialog.
创建一个工具文件来初始化带有 SSR 支持的 Supabase 客户端:
import { createServerClient, parseCookieHeader } from "@supabase/ssr";import type { AstroCookies } from "astro";const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URLconst supabasePublishableKey = import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEYexport function createClient({ request, cookies,}: { request: Request; cookies: AstroCookies;}) { return createServerClient( supabaseUrl, supabasePublishableKey, { cookies: { getAll() { return parseCookieHeader( request.headers.get("Cookie") ?? "" ); }, setAll(cookiesToSet) { cookiesToSet.forEach(({ name, value, options }) => cookies.set(name, value, options) ); }, }, } );}在 src/actions/index.ts 创建一个新文件,用于定义服务器端的注册、登录和登出操作:
import { defineAction } from "astro:actions";import { z } from "astro/zod";import { createClient } from "../lib/supabase";export const server = { signUp: defineAction({ accept: "form", input: z.object({ email: z.string().email(), password: z.string().min(6), }), handler: async (input, context) => { try { const supabase = createClient({ request: context.request, cookies: context.cookies, }); const { error } = await supabase.auth.signUp({ email: input.email, password: input.password, options: { emailRedirectTo: "http://localhost:4321/auth/callback", }, }); if (error) { return { success: false, message: error.message, }; } return { success: true, message: "Check your email to confirm your account", }; } catch (err) { return { success: false, message: "Unexpected error", }; } }, }), signIn: defineAction({ accept: "form", input: z.object({ email: z.string().email(), password: z.string(), }), handler: async (input, context) => { try { const supabase = createClient({ request: context.request, cookies: context.cookies, }); const { error } = await supabase.auth.signInWithPassword({ email: input.email, password: input.password, }); if (error) { return { success: false, message: error.message, }; } return { success: true, message: "Signed in successfully", }; } catch (err) { return { success: false, message: "Unexpected error", }; } }, }), signOut: defineAction({ handler: async (_, context) => { try { const supabase = createClient({ request: context.request, cookies: context.cookies, }); await supabase.auth.signOut(); return { success: true, }; } catch (err) { return { success: false, message: "Failed to sign out", }; } }, }),};在用户确认他们的邮箱之前,先更新 Supabase 邮件模板,将 token 哈希发送到你的回调 URL。
在你的 Supabase 项目控制面板 中:
- 去 认证 > 邮件模板
- 选择 确认注册 模板
- 把
{{ .ConfirmationURL }}改成{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=email。 - 把你的网站链接改成
http://localhost:4321
{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=email在 src/pages/auth/callback.astro 创建一个新文件来处理邮箱确认回调。从 URL 中提取令牌,并用 Supabase 验证它:
---import { createClient } from "../../lib/supabase";import type { EmailOtpType } from "@supabase/supabase-js";const supabase = createClient({ request: Astro.request, cookies: Astro.cookies,});const requestUrl = new URL(Astro.request.url);const token_hash = requestUrl.searchParams.get('token_hash');const type = requestUrl.searchParams.get('type') as EmailOtpType | null;if (token_hash && type) { const { error } = await supabase.auth.verifyOtp({ token_hash, type, }); if (!error) { return Astro.redirect("/dashboard"); }}return Astro.redirect("/auth/signin");---<html> <head> <title>Email Confirmation</title> </head> <body> <p>Confirming your email...</p> </body></html>在 src/pages/auth/signup.astro 创建一个新文件,里面放一个注册表单。使用客户端事件监听器来处理表单提交:
---import { createClient } from "../../lib/supabase";const supabase = createClient({ request: Astro.request, cookies: Astro.cookies,});const { data } = await supabase.auth.getUser();if (data?.user) { return Astro.redirect("/dashboard");}---<html> <head> <title>Sign Up</title> </head> <body> <h1>Sign Up</h1> <div id="message"></div> <form id="signup-form"> <div> <label for="email">Email</label> <input id="email" type="email" name="email" placeholder="your@email.com" required /> </div> <div> <label for="password">Password</label> <input id="password" type="password" name="password" placeholder="At least 6 characters" required /> </div> <button type="submit" id="signup-btn">Sign Up</button> </form> <p> Already have an account? <a href="/auth/signin">Sign in</a> </p> </body></html><script> import { actions } from "astro:actions"; const form = document.querySelector("#signup-form") as HTMLFormElement; const btn = document.getElementById("signup-btn") as HTMLButtonElement; const messageEl = document.getElementById("message") as HTMLDivElement; form?.addEventListener("submit", async (e) => { e.preventDefault(); btn.disabled = true; btn.textContent = "Signing up..."; messageEl.textContent = ""; try { const formData = new FormData(form); const result = await actions.signUp(formData); if (!result.data?.success) { btn.disabled = false; btn.textContent = "Sign Up"; messageEl.textContent = result.data?.message || "Sign up failed"; messageEl.style.color = "red"; return; } messageEl.textContent = result.data.message; messageEl.style.color = "green"; btn.textContent = "Sign Up"; } catch (error) { btn.disabled = false; btn.textContent = "Sign Up"; messageEl.textContent = "An error occurred. Please try again."; messageEl.style.color = "red"; console.error(error); } });</script>在 src/pages/auth/signin.astro 创建一个新文件,里面放一个登录表单。使用客户端事件监听器来处理表单提交:
---import { createClient } from "../../lib/supabase";const supabase = createClient({ request: Astro.request, cookies: Astro.cookies,});const { data } = await supabase.auth.getUser();if (data?.user) { return Astro.redirect("/dashboard");}---<html> <head> <title>Sign In</title> </head> <body> <h1>Sign In</h1> <div id="message"></div> <form id="signin-form"> <div> <label for="email">Email</label> <input id="email" type="email" name="email" placeholder="your@email.com" required /> </div> <div> <label for="password">Password</label> <input id="password" type="password" name="password" placeholder="Your password" required /> </div> <button type="submit" id="signin-btn">Sign In</button> </form> <p> Don't have an account? <a href="/auth/signup">Sign up</a> </p> </body></html><script> import { actions } from "astro:actions"; const form = document.querySelector("#signin-form") as HTMLFormElement; const btn = document.getElementById("signin-btn") as HTMLButtonElement; const messageEl = document.getElementById("message") as HTMLDivElement; form?.addEventListener("submit", async (e) => { e.preventDefault(); btn.disabled = true; btn.textContent = "Signing in..."; messageEl.textContent = ""; try { const formData = new FormData(form); const result = await actions.signIn(formData); if (!result.data?.success) { btn.disabled = false; btn.textContent = "Sign In"; messageEl.textContent = result.data?.message || "Sign in failed"; messageEl.style.color = "red"; return; } // Redirect to dashboard on successful sign in window.location.href = "/dashboard"; } catch (error) { btn.disabled = false; btn.textContent = "Sign In"; messageEl.textContent = "An error occurred. Please try again."; messageEl.style.color = "red"; console.error(error); } });</script>在 src/pages/dashboard.astro 创建一个新文件来显示已认证用户的信息。对登出按钮使用客户端事件监听器:
---import { createClient } from "../lib/supabase";const supabase = createClient({ request: Astro.request, cookies: Astro.cookies,});const { data } = await supabase.auth.getUser();const user = data?.user;if (!user) { return Astro.redirect("/auth/signin");}---<html> <head> <title>Dashboard</title> </head> <body> <h1>Welcome!</h1> <p>Email: {user.email}</p> <p>User ID: {user.id}</p> <button id="signout-btn">Sign Out</button> </body></html><script> import { actions } from "astro:actions"; const btn = document.getElementById("signout-btn") as HTMLButtonElement; btn?.addEventListener("click", async (e) => { e.preventDefault(); btn.disabled = true; btn.textContent = "Signing out..."; try { const result = await actions.signOut(); if (!result.data?.success) { btn.disabled = false; btn.textContent = "Sign Out"; alert("Failed to sign out"); return; } // Redirect to signin page window.location.href = "/auth/signin"; } catch (error) { btn.disabled = false; btn.textContent = "Sign Out"; console.error(error); } });</script>了解更多 #
🌐 Learn more
- Supabase 身份验证文档了解更多 Supabase 身份验证方法