在 Astro 中使用 Supabase 认证
Learn how to configure Supabase Auth for Astro with server-side rendering.
快速入门 #
🌐 Quickstart
去 database.new 创建一个新的 Supabase 项目。
你的新数据库有一个用于存储用户的表。你可以通过在 SQL 编辑器 中运行一些 SQL 来看到这个表目前是空的。
1select * 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.
探索组件1npm create astro@latest my-app2cd my-app安装 @supabase/supabase-js 客户端库,@supabase/ssr 用于服务器端认证,以及 @astrojs/node 适配器以启用服务器端渲染。
1npm install @supabase/supabase-js @supabase/ssr @astrojs/node更新你的 astro.config.mjs 来启用使用 Node 适配器的服务端渲染。
1import { defineConfig } from "astro/config";2import node from "@astrojs/node";34export default defineConfig({5 output: "server",6 adapter: node({7 mode: "standalone",8 }),9});创建一个 .env.local 文件,并用你的 Supabase 连接变量填充:
1PUBLIC_SUPABASE_URL=your-project-url2PUBLIC_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.
阅读 API 密钥文档 以全面了解所有密钥类型、用途以及在哪里可以找到它们。
创建一个工具文件来初始化带有 SSR 支持的 Supabase 客户端:
1import { createServerClient, parseCookieHeader } from "@supabase/ssr";2import type { AstroCookies } from "astro";34const supabaseUrl = import.meta.env.PUBLIC_SUPABASE_URL5const supabasePublishableKey = import.meta.env.PUBLIC_SUPABASE_PUBLISHABLE_KEY67export function createClient({8 request,9 cookies,10}: {11 request: Request;12 cookies: AstroCookies;13}) {14 return createServerClient(15 supabaseUrl,16 supabasePublishableKey,17 {18 cookies: {19 getAll() {20 return parseCookieHeader(21 request.headers.get("Cookie") ?? ""22 );23 },24 setAll(cookiesToSet) {25 cookiesToSet.forEach(({ name, value, options }) =>26 cookies.set(name, value, options)27 );28 },29 },30 }31 );32}在 src/actions/index.ts 创建一个新文件,用于定义服务器端的注册、登录和登出操作:
1import { defineAction } from "astro:actions";2import { z } from "astro/zod";3import { createClient } from "../lib/supabase";45export const server = {6 signUp: defineAction({7 accept: "form",8 input: z.object({9 email: z.string().email(),10 password: z.string().min(6),11 }),12 handler: async (input, context) => {13 try {14 const supabase = createClient({15 request: context.request,16 cookies: context.cookies,17 });1819 const { error } = await supabase.auth.signUp({20 email: input.email,21 password: input.password,22 options: {23 emailRedirectTo: "http://localhost:4321/auth/callback",24 },25 });2627 if (error) {28 return {29 success: false,30 message: error.message,31 };32 }3334 return {35 success: true,36 message: "Check your email to confirm your account",37 };38 } catch (err) {39 return {40 success: false,41 message: "Unexpected error",42 };43 }44 },45 }),46 signIn: defineAction({47 accept: "form",48 input: z.object({49 email: z.string().email(),50 password: z.string(),51 }),52 handler: async (input, context) => {53 try {54 const supabase = createClient({55 request: context.request,56 cookies: context.cookies,57 });5859 const { error } = await supabase.auth.signInWithPassword({60 email: input.email,61 password: input.password,62 });6364 if (error) {65 return {66 success: false,67 message: error.message,68 };69 }7071 return {72 success: true,73 message: "Signed in successfully",74 };75 } catch (err) {76 return {77 success: false,78 message: "Unexpected error",79 };80 }81 },82 }),83 signOut: defineAction({84 handler: async (_, context) => {85 try {86 const supabase = createClient({87 request: context.request,88 cookies: context.cookies,89 });9091 await supabase.auth.signOut();9293 return {94 success: true,95 };96 } catch (err) {97 return {98 success: false,99 message: "Failed to sign out",100 };101 }102 },103 }),104};在用户确认他们的邮箱之前,先更新 Supabase 邮件模板,将 token 哈希发送到你的回调 URL。
在你的 Supabase 项目控制面板 中:
- 去 认证 > 邮件模板
- 选择 确认注册 模板
- 把
{{ .ConfirmationURL }}改成{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=email。 - 把你的网站链接改成
http://localhost:4321
1{{ .SiteURL }}/auth/callback?token_hash={{ .TokenHash }}&type=email在 src/pages/auth/callback.astro 创建一个新文件来处理邮箱确认回调。从 URL 中提取令牌,并用 Supabase 验证它:
1---2import { createClient } from "../../lib/supabase";3import type { EmailOtpType } from "@supabase/supabase-js";45const supabase = createClient({6 request: Astro.request,7 cookies: Astro.cookies,8});910const requestUrl = new URL(Astro.request.url);11const token_hash = requestUrl.searchParams.get('token_hash');12const type = requestUrl.searchParams.get('type') as EmailOtpType | null;1314if (token_hash && type) {15 const { error } = await supabase.auth.verifyOtp({16 token_hash,17 type,18 });1920 if (!error) {21 return Astro.redirect("/dashboard");22 }23}2425return Astro.redirect("/auth/signin");26---2728<html>29 <head>30 <title>Email Confirmation</title>31 </head>32 <body>33 <p>Confirming your email...</p>34 </body>35</html>在 src/pages/auth/signup.astro 创建一个新文件,里面放一个注册表单。使用客户端事件监听器来处理表单提交:
1---2import { createClient } from "../../lib/supabase";34const supabase = createClient({5 request: Astro.request,6 cookies: Astro.cookies,7});89const { data } = await supabase.auth.getUser();1011if (data?.user) {12 return Astro.redirect("/dashboard");13}14---1516<html>17 <head>18 <title>Sign Up</title>19 </head>20 <body>21 <h1>Sign Up</h1>2223 <div id="message"></div>2425 <form id="signup-form">26 <div>27 <label for="email">Email</label>28 <input29 id="email"30 type="email"31 name="email"32 placeholder="your@email.com"33 required34 />35 </div>36 <div>37 <label for="password">Password</label>38 <input39 id="password"40 type="password"41 name="password"42 placeholder="At least 6 characters"43 required44 />45 </div>46 <button type="submit" id="signup-btn">Sign Up</button>47 </form>48 <p>49 Already have an account? <a href="/auth/signin">Sign in</a>50 </p>51 </body>52</html>5354<script>55 import { actions } from "astro:actions";5657 const form = document.querySelector("#signup-form") as HTMLFormElement;58 const btn = document.getElementById("signup-btn") as HTMLButtonElement;59 const messageEl = document.getElementById("message") as HTMLDivElement;6061 form?.addEventListener("submit", async (e) => {62 e.preventDefault();63 btn.disabled = true;64 btn.textContent = "Signing up...";65 messageEl.textContent = "";6667 try {68 const formData = new FormData(form);69 const result = await actions.signUp(formData);7071 if (!result.data?.success) {72 btn.disabled = false;73 btn.textContent = "Sign Up";74 messageEl.textContent = result.data?.message || "Sign up failed";75 messageEl.style.color = "red";76 return;77 }7879 messageEl.textContent = result.data.message;80 messageEl.style.color = "green";81 btn.textContent = "Sign Up";82 } catch (error) {83 btn.disabled = false;84 btn.textContent = "Sign Up";85 messageEl.textContent = "An error occurred. Please try again.";86 messageEl.style.color = "red";87 console.error(error);88 }89 });90</script>在 src/pages/auth/signin.astro 创建一个新文件,里面放一个登录表单。使用客户端事件监听器来处理表单提交:
1---2import { createClient } from "../../lib/supabase";34const supabase = createClient({5 request: Astro.request,6 cookies: Astro.cookies,7});89const { data } = await supabase.auth.getUser();1011if (data?.user) {12 return Astro.redirect("/dashboard");13}14---1516<html>17 <head>18 <title>Sign In</title>19 </head>20 <body>21 <h1>Sign In</h1>2223 <div id="message"></div>2425 <form id="signin-form">26 <div>27 <label for="email">Email</label>28 <input29 id="email"30 type="email"31 name="email"32 placeholder="your@email.com"33 required34 />35 </div>36 <div>37 <label for="password">Password</label>38 <input39 id="password"40 type="password"41 name="password"42 placeholder="Your password"43 required44 />45 </div>46 <button type="submit" id="signin-btn">Sign In</button>47 </form>48 <p>49 Don't have an account? <a href="/auth/signup">Sign up</a>50 </p>51 </body>52</html>5354<script>55 import { actions } from "astro:actions";5657 const form = document.querySelector("#signin-form") as HTMLFormElement;58 const btn = document.getElementById("signin-btn") as HTMLButtonElement;59 const messageEl = document.getElementById("message") as HTMLDivElement;6061 form?.addEventListener("submit", async (e) => {62 e.preventDefault();63 btn.disabled = true;64 btn.textContent = "Signing in...";65 messageEl.textContent = "";6667 try {68 const formData = new FormData(form);69 const result = await actions.signIn(formData);7071 if (!result.data?.success) {72 btn.disabled = false;73 btn.textContent = "Sign In";74 messageEl.textContent = result.data?.message || "Sign in failed";75 messageEl.style.color = "red";76 return;77 }7879 // Redirect to dashboard on successful sign in80 window.location.href = "/dashboard";81 } catch (error) {82 btn.disabled = false;83 btn.textContent = "Sign In";84 messageEl.textContent = "An error occurred. Please try again.";85 messageEl.style.color = "red";86 console.error(error);87 }88 });89</script>在 src/pages/dashboard.astro 创建一个新文件来显示已认证用户的信息。对登出按钮使用客户端事件监听器:
1---2import { createClient } from "../lib/supabase";34const supabase = createClient({5 request: Astro.request,6 cookies: Astro.cookies,7});89const { data } = await supabase.auth.getUser();10const user = data?.user;1112if (!user) {13 return Astro.redirect("/auth/signin");14}15---1617<html>18 <head>19 <title>Dashboard</title>20 </head>21 <body>22 <h1>Welcome!</h1>23 <p>Email: {user.email}</p>24 <p>User ID: {user.id}</p>2526 <button id="signout-btn">Sign Out</button>27 </body>28</html>2930<script>31 import { actions } from "astro:actions";3233 const btn = document.getElementById("signout-btn") as HTMLButtonElement;3435 btn?.addEventListener("click", async (e) => {36 e.preventDefault();37 btn.disabled = true;38 btn.textContent = "Signing out...";3940 try {41 const result = await actions.signOut();4243 if (!result.data?.success) {44 btn.disabled = false;45 btn.textContent = "Sign Out";46 alert("Failed to sign out");47 return;48 }4950 // Redirect to signin page51 window.location.href = "/auth/signin";52 } catch (error) {53 btn.disabled = false;54 btn.textContent = "Sign Out";55 console.error(error);56 }57 });58</script>了解更多 #
🌐 Learn more
- Supabase 身份验证文档了解更多 Supabase 身份验证方法