Supabase Next.js 全栈认证入门包:基于 @supabase/ssr 的 Cookie 会话与 Proxy 拦截实现

发布时间:2026/9/7 1:37:19
Supabase Next.js 全栈认证入门包:基于 @supabase/ssr 的 Cookie 会话与 Proxy 拦截实现 Supabase Next.js 全栈认证入门包基于 supabase/ssr 的 Cookie 会话与 Proxy 拦截实现【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase本文基于 Supabase 官方仓库中的 Next.js 认证入门包examples/auth/nextjs-full完整讲解如何用它搭建一个覆盖 App Router、Server Components、Server Actions、Route Handlers 与 Proxy 的 Next.js Supabase Auth 应用包含本地五步运行流程、NEXT_PUBLIC_SUPABASE_URL/NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY两个核心环境变量、登录/注册 Server Action、邮箱 OTP 确认路由以及 Proxy 中updateSession刷新会话的底层机制。读完后你可以直接复现该入门包并理解它在 Next.js 全栈各层之间保持会话同步的实现原理。一、这个入门包能做什么examples/auth/nextjs-full是 Supabase 仓库中面向 Next.js 的“全栈”认证示例其 README 明确列出了它的能力边界覆盖整个 Next.js 技术栈App Router、Pages Router、Proxy、Client Components、Server Components 全部可用README 原文称之为 “It just works!”基于supabase-ssr即supabase/ssr包配置 Cookie 化认证使用户会话在整个应用中可用——无论是 Client Components、Server Components、Route Handlers 还是 Server Actions使用 Tailwind CSS 做样式可选地通过 Supabase Vercel Integration 部署到 Vercel部署时环境变量会自动注入 Vercel 项目。从 package.json 可以看到其核心依赖supabase/ssrlatest、supabase/supabase-js^2、nextlatest、react18.2.0、tailwindcss3.4.1与typescript5.3.3。脚本部分只有dev/build/start三个标准命令。整个示例的目录结构如下examples/auth/nextjs-full/ ├── app/ │ ├── auth/confirm/route.ts # 邮箱 OTP 确认的 Route Handler │ ├── error/page.tsx # 错误提示页 │ ├── login/ # 登录/注册页含 Server Actions 与提交按钮 │ ├── private/page.tsx # 需要登录才能访问的受保护页面 │ ├── layout.tsx │ └── page.tsx # 首页含连接 Supabase 的交互式引导 ├── components/ # 教程步骤组件与认证按钮 ├── lib/supabase/ │ ├── client.ts # 浏览器端客户端 │ ├── proxy.ts # Proxy 中使用的会话刷新逻辑 │ └── server.ts # 服务端客户端Server Components/Actions ├── proxy.ts # Next.js Proxy 入口 └── supabase/ # 本地 Supabase 配置与种子脚本这套三文件客户端拆分client.ts/server.ts/proxy.ts正是supabase/ssr的推荐模式下文将逐一走读。二、三个 Supabase 客户端浏览器、服务端与 Proxy浏览器客户端lib/supabase/client.tsclient.ts 只有 8 行是供 Client Components 使用的浏览器客户端import { createBrowserClient } from supabase/ssr export function createClient() { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY! ) }注意两点它只使用NEXT_PUBLIC_*前缀的公开变量因为 Client Component 会被打包进浏览器 bundle任何服务端专用密钥都不应出现在这里createBrowserClient内部默认以 Cookie 作为会话存储因此浏览器与服务端共享同一套会话 Cookie无需额外的 localStorage 管理。服务端客户端lib/supabase/server.tsserver.ts 供 Server Components 和 Server Actions 使用关键在于它显式把 Next.js 的 Cookie Store 接入了supabase/ssr的 Cookie 接口import { createServerClient } from supabase/ssr import { cookies } from next/headers export async function createClient() { const cookieStore await cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, { cookies: { getAll() { return cookieStore.getAll() }, setAll(cookiesToSet, _headers) { try { cookiesToSet.forEach(({ name, value, options }) cookieStore.set(name, value, options) ) } catch { // The setAll method was called from a Server Component. // This can be ignored if you have proxy refreshing // user sessions. } }, }, } ) }这里setAll中被刻意catch掉的异常值得注意在Server Component中尝试写入 Cookie 会抛错Server Component 不允许修改响应头。源码注释说明了原因——只要 Proxy 层见下文已经在为每个请求刷新用户会话这个异常就可以安全忽略。这是理解整个入门包会话模型的关键一环Cookie 的写入统一交给 Proxy服务端组件只负责读取。Proxy 中的会话刷新lib/supabase/proxy.tsproxy.tsNext.js 16 用 Proxy 取代了原先的 middleware 概念根目录的 proxy.ts 把每个匹配到的请求转交给updateSessionimport { updateSession } from /lib/supabase/proxy export async function proxy(request: NextRequest) { return await updateSession(request) } export const config { matcher: [ /((?!_next/static|_next/image|favicon.ico|.*\\.(?:svg|png|jpg|jpeg|gif|webp)$).*), ], }matcher正则排除了_next/static、_next/image、favicon.ico及各类静态资源后缀保证只有真正的动态请求才会进入会话刷新逻辑避免对静态资源做无谓的处理。核心逻辑在 lib/supabase/proxy.ts它按顺序做了四件事每个请求都新建客户端。源码中特意注释“With Fluid compute, dont put this client in a global environment variable”即不要把createServerClient的实例放进模块级全局变量必须逐请求创建否则在弹性计算环境下会串会话Cookie 接线的差异与server.ts不同这里的getAll从request.cookies读取setAll则把新 Cookie 写回一个新的NextResponse.next({ request })响应对象上并同步转发headers强制调用supabase.auth.getClaims()。源码注释用大字强调了这条不变量createServerClient与getClaims()之间不能插入任何代码如果删掉getClaims()而仍在使用服务端渲染用户会被随机登出。getClaims()的调用过程会触发supabase/ssr内部的 JWT 解析与过期自动刷新正是它保证了刷新令牌轮换后 Cookie 中的 access token 被及时更新未登录重定向若取不到 claims、且路径不是/login或/auth开头则 302 重定向到/loginconst { data } await supabase.auth.getClaims() const user data?.claims if ( !user !request.nextUrl.pathname.startsWith(/login) !request.nextUrl.pathname.startsWith(/auth) ) { const url request.nextUrl.clone() url.pathname /login return NextResponse.redirect(url) }文件末尾还有一段必须遵守的响应返回约束supabaseResponse必须原样返回如果要自己构造新响应必须传入request、把supabaseResponse的 cookies 完整拷贝过去且不能改动 Cookie——否则浏览器与服务端会话状态会不同步导致会话被过早终止。三、登录与注册Server Actions 的实现登录页 app/login/page.tsx 是一个接收searchParams.message的 Server Component表单里放了两个SubmitButton分别绑定signIn与signUp两个 Server Action并通过searchParams.message展示错误/提示文案。真正的认证逻辑在 app/login/actions.ts文件头部的use server声明使其中的函数可作为表单action直接在服务端执行use server import { revalidatePath } from next/cache import { redirect } from next/navigation import { createClient } from /lib/supabase/server export async function signIn(formData: FormData) { const supabase await createClient() // type-casting here for convenience // in practice, you should validate your inputs const data { email: formData.get(email) as string, password: formData.get(password) as string, } const { error } await supabase.auth.signInWithPassword(data) if (error) { redirect(/error) } revalidatePath(/, layout) redirect(/) }signUp的结构完全对称只是调用supabase.auth.signUp(data)。几个实现细节输入在这里直接做了类型断言源码注释明确提醒“生产环境应自行校验输入”认证失败走redirect(/error)对应 app/error/page.tsx成功后先revalidatePath(/, layout)使首页按最新登录态重新渲染再redirect(/)回首页。由于登录写入的会话 Cookie 是通过server.ts中cookieStore.set落到响应上的登录完成后 Proxy 层后续请求即可正常读取到 claims无需客户端额外操作。四、受保护页面与邮箱 OTP 确认受保护页面app/private/page.tsxapp/private/page.tsx 演示了 Server Component 中的标准鉴权写法const supabase await createClient() const { data } await supabase.auth.getClaims() const claims data?.claims if (!claims) { return redirect(/login) }即使忘记在路由层做拦截页面自身也会二次校验 claims 并跳转/login与 Proxy 的全局重定向形成双保险。邮箱确认路由app/auth/confirm/route.ts当 Supabase 发送的确认邮件被点击后会带着token_hash、type、next参数回到应用。app/auth/confirm/route.ts 是一个标准的 GET Route Handlerexport async function GET(request: NextRequest) { const { searchParams } new URL(request.url) const token_hash searchParams.get(token_hash) const type searchParams.get(type) as EmailOtpType | null const next searchParams.get(next) ?? / if (token_hash type) { const supabase await createClient() const { error } await supabase.auth.verifyOtp({ type, token_hash }) if (!error) { redirect(next) } } redirect(/error) }注意type被断言为supabase/supabase-js导出的EmailOtpType验证成功按next参数回跳缺省为/任何一步失败都落到/error页。这也解释了为什么 Proxy 的重定向白名单里要放行/auth前缀——确认回调本身就是一个未登录请求。五、本地运行从建项目到启动开发服务器README 的 “Clone and run locally” 一节给出了完整可复现的五步流程这里结合仓库中的实际文件把它补齐。第 1 步准备一个 Supabase 项目通过 Supabase Dashboarddatabase.new创建一个项目。创建完成后在项目的 API settings 页面可以找到后面要填的两个值项目 URL 与 publishable key即 anon key。第 2 步用 create-next-app 的 Supabase 模板创建应用npx create-next-app -e with-supabase-e with-supabase表示使用该模板示例仓库中的examples/auth/nextjs-full就是这个模板在 Supabase 仓库内的对应实现。第 3 步进入应用目录cd name-of-new-app第 4 步配置环境变量把.env.example重命名为.env.local并填入NEXT_PUBLIC_SUPABASE_URL[INSERT SUPABASE PROJECT URL] NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY[INSERT SUPABASE PROJECT API ANON KEY]这两个变量与三个客户端文件一一对应client.ts、server.ts、proxy.ts里全部以非空断言process.env.NEXT_PUBLIC_SUPABASE_URL!读取它们这也是首页page.tsx中canInitSupabaseClient()的判定依据——该函数通过try { createClient() } catch检测环境变量是否就绪从而决定首页展示“连接 Supabase”引导ConnectSupabaseSteps还是“注册用户”引导SignUpUserSteps见 app/page.tsx。第 5 步启动开发服务器npm run dev启动后应用运行在localhost:3000。README 同时建议如果需要本地运行 Supabase 本身数据库、Auth、Storage 等全套服务可以参考 Supabase 文档中的 Local Development 指南使用supabase start系列命令。本仓库内对应的本地配置就在 supabase/config.toml。本地 Supabase 配置要点supabase/config.toml该文件是supabase start的完整配置与认证最相关的部分包括[auth]enabled truesite_url http://127.0.0.1:3000作为重定向白名单基础地址与邮件 URL 构造基础additional_redirect_urls [https://127.0.0.1:3000]jwt_expiry 36001 小时最大可设 1 周enable_refresh_token_rotation true且refresh_token_reuse_interval 10秒——这正是 Proxy 中getClaims()自动刷新逻辑所依赖的令牌轮换配置[auth.email]enable_signup trueenable_confirmations false默认不开启邮箱确认即注册即可登录若开启确认app/auth/confirm/route.ts这条路由才会被实际用到[inbucket]本地邮件测试服务器端口 54324本地开发时发出的邮件不会真正投递而是可以在其 Web 界面查看[api]本地 API 端口 54321max_rows 1000[db]数据库端口 54322、Postgresmajor_version 15[studio]端口 54323[storage]单文件上限50MiB。supabase/seed.sql 为空文件说明该示例不依赖种子数据注册流程完全依赖 Supabase Auth 本身。六、部署到 VercelREADME 的 “Deploy to Vercel” 一节说明Vercel 部署流程会引导你创建 Supabase 账号与项目安装 Supabase Vercel Integration 后所有相关环境变量会被自动注入 Vercel 项目使部署后的应用立即可用。该按钮还会把入门包克隆到你的 GitHub你可以再把它拉到本地继续开发。从环境变量命名的角度理解为什么注入后“fully functioning”整个示例只依赖NEXT_PUBLIC_SUPABASE_URL与NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY两个公开变量三个客户端文件均未引用其他密钥因此只要这两个变量到位浏览器、服务端、Proxy 三层客户端就都能初始化无需额外的服务端 Secret。七、小结会话同步的三条不变量回到 README 的核心承诺——“session available throughout the entire Next.js app”。结合源码这个承诺由三条不变量支撑也是你在自己的项目里移植这套模式时最需要保持的三客户端同构接线client.ts/server.ts/proxy.ts都以同一对NEXT_PUBLIC_*变量初始化且都使用supabase/ssr的 Cookie 适配器保证浏览器、服务端、请求层读到的是同一份会话Proxy 必须调用getClaims()并原样返回supabaseResponse它承担 access token 的静默刷新是防止“用户被随机登出”的关键见 lib/supabase/proxy.ts 的注释鉴权双保险Proxy 的全局重定向 受保护页面内的getClaims()二次校验缺一不可。如果你想进一步参考同仓库内更轻量的版本无受保护页面、无 OTP 路由可以对比 examples/auth/nextjs 的 README更深入的教程组件如注册后获取用户数据的步骤则位于 components/tutorial/FetchDataSteps.tsx 等文件中可作为扩展起点。【免费下载链接】supabaseThe Postgres development platform. Supabase gives you a dedicated Postgres database to build your web, mobile, and AI applications.项目地址: https://gitcode.com/GitHub_Trending/supa/supabase创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考