为 SvelteKit 配置服务端认证
设置服务器端认证以在SvelteKit中使用基于Cookie的身份验证。
安装Supabase包
安装 @supabase/supabase-js
包和辅助包 @supabase/ssr
。
1npm install @supabase/supabase-js @supabase/ssr
设置环境变量
在项目根目录创建 .env.local
文件。
填写您的 PUBLIC_SUPABASE_URL
和 PUBLIC_SUPABASE_ANON_KEY
:
Project URL
Anon key
12PUBLIC_SUPABASE_URL=<your_supabase_project_url>PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>
设置服务器端钩子
在 src/hooks.server.ts
中设置服务器端钩子。这些钩子用于:
- 创建特定于请求的Supabase客户端,使用来自请求Cookie的用户凭证(该客户端仅用于服务器端代码)
- 检查用户认证状态
- 保护受保护页面
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879import { createServerClient } from '@supabase/ssr'import { type Handle, redirect } from '@sveltejs/kit'import { sequence } from '@sveltejs/kit/hooks'import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'const supabase: Handle = async ({ event, resolve }) => { /** * 创建特定于该服务器请求的Supabase客户端。 * * Supabase客户端从请求Cookie中获取Auth令牌。 */ event.locals.supabase = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { cookies: { getAll: () => event.cookies.getAll(), /** * SvelteKit的Cookies API要求必须在Cookie选项中显式设置`path`。 * 将`path`设置为`/`可复制之前的标准行为。 */ setAll: (cookiesToSet) => { cookiesToSet.forEach(({ name, value, options }) => { event.cookies.set(name, value, { ...options, path: '/' }) }) }, }, }) /** * 与`supabase.auth.getSession()`不同(它返回会话但不验证JWT), * 此函数还会调用`getUser()`来验证JWT,然后才会返回会话。 */ event.locals.safeGetSession = async () => { const { data: { session }, } = await event.locals.supabase.auth.getSession() if (!session) { return { session: null, user: null } } const { data: { user }, error, } = await event.locals.supabase.auth.getUser() if (error) { // JWT验证失败 return { session: null, user: null } } return { session, user } } return resolve(event, { filterSerializedResponseHeaders(name) { /** * Supabase库使用`content-range`和`x-supabase-api-version`头, * 所以我们需要告诉SvelteKit传递它们。 */ return name === 'content-range' || name === 'x-supabase-api-version' }, })}const authGuard: Handle = async ({ event, resolve }) => { const { session, user } = await event.locals.safeGetSession() event.locals.session = session event.locals.user = user if (!event.locals.session && event.url.pathname.startsWith('/private')) { redirect(303, '/auth') } if (event.locals.session && event.url.pathname === '/auth') { redirect(303, '/private') } return resolve(event)}export const handle: Handle = sequence(supabase, authGuard)
创建TypeScript类型定义
为防止TypeScript错误,为新的 event.locals
属性添加类型定义。
123456789101112131415161718192021import type { Session, SupabaseClient, User } from '@supabase/supabase-js'import type { Database } from './database.types.ts' // 导入生成的类型declare global { namespace App { // interface Error {} interface Locals { supabase: SupabaseClient<Database> safeGetSession: () => Promise<{ session: Session | null; user: User | null }> session: Session | null user: User | null } interface PageData { session: Session | null } // interface PageState {} // interface Platform {} }}export {}
在根布局中创建Supabase客户端
在根 +layout.ts
中创建Supabase客户端。该客户端可用于从客户端或服务器访问Supabase。为了在服务器上获取Auth令牌,使用 +layout.server.ts
文件从 event.locals
传入会话。
123456789101112131415161718192021222324252627282930313233343536373839404142import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'import type { LayoutLoad } from './$types'export const load: LayoutLoad = async ({ data, depends, fetch }) => { /** * 声明一个依赖项,以便布局可以失效,例如在会话刷新时。 */ depends('supabase:auth') const supabase = isBrowser() ? createBrowserClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { global: { fetch, }, }) : createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, { global: { fetch, }, cookies: { getAll() { return data.cookies }, }, }) /** * 在这里使用`getSession`是安全的,因为在客户端`getSession`是安全的, * 而在服务器端,它从`LayoutData`中读取`session`, * 后者已使用`safeGetSession`安全地检查了会话。 */ const { data: { session }, } = await supabase.auth.getSession() const { data: { user }, } = await supabase.auth.getUser() return { session, supabase, user }}
监听Auth事件
在客户端设置Auth事件监听器,以处理会话刷新和登出。
12345678910111213141516171819<script> import { invalidate } from '$app/navigation' import { onMount } from 'svelte' let { data, children } = $props() let { session, supabase } = $derived(data) onMount(() => { const { data } = supabase.auth.onAuthStateChange((_, newSession) => { if (newSession?.expires_at !== session?.expires_at) { invalidate('supabase:auth') } }) return () => data.subscription.unsubscribe() })</script>{@render children()}
创建第一个页面
创建您的第一个页面。此示例页面从服务器调用Supabase以从数据库获取国家列表。
这是一个使用公开可读数据的公共页面示例。
要填充数据库,请从仪表板运行colors quickstart。
123456import type { PageServerLoad } from './$types'export const load: PageServerLoad = async ({ locals: { supabase } }) => { const { data: colors } = await supabase.from('colors').select('name').limit(5).order('name') return { colors: colors ?? [] }}
更改Auth确认路径
如果您启用了电子邮件确认(默认启用),新用户注册后将收到一封确认邮件。
更改电子邮件模板以支持服务器端认证流程。
转到仪表板中的Auth templates页面。在Confirm signup
模板中,将{{ .ConfirmationURL }}
更改为{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email
。
创建登录页面
接下来,创建一个登录页面让用户注册和登录。
1234567891011121314151617181920212223242526272829303132import { redirect } from '@sveltejs/kit'import type { Actions } from './$types'export const actions: Actions = { signup: async ({ request, locals: { supabase } }) => { const formData = await request.formData() const email = formData.get('email') as string const password = formData.get('password') as string const { error } = await supabase.auth.signUp({ email, password }) if (error) { console.error(error) redirect(303, '/auth/error') } else { redirect(303, '/') } }, login: async ({ request, locals: { supabase } }) => { const formData = await request.formData() const email = formData.get('email') as string const password = formData.get('password') as string const { error } = await supabase.auth.signInWithPassword({ email, password }) if (error) { console.error(error) redirect(303, '/auth/error') } else { redirect(303, '/private') } },}
创建注册确认路由
通过创建处理电子邮件验证的API路由来完成注册流程。
12345678910111213141516171819202122232425262728293031import type { EmailOtpType } from '@supabase/supabase-js'import { redirect } from '@sveltejs/kit'import type { RequestHandler } from './$types'export const GET: RequestHandler = async ({ url, locals: { supabase } }) => { const token_hash = url.searchParams.get('token_hash') const type = url.searchParams.get('type') as EmailOtpType | null const next = url.searchParams.get('next') ?? '/' /** * 通过删除Auth流程参数清理重定向URL。 * * 暂时保留`next`,因为在错误情况下需要它。 */ const redirectTo = new URL(url) redirectTo.pathname = next redirectTo.searchParams.delete('token_hash') redirectTo.searchParams.delete('type') if (token_hash && type) { const { error } = await supabase.auth.verifyOtp({ type, token_hash }) if (!error) { redirectTo.searchParams.delete('next') redirect(303, redirectTo) } } redirectTo.pathname = '/auth/error' redirect(303, redirectTo)}
创建私有路由
创建只能由认证用户访问的私有路由。private
目录中的路由由hooks.server.ts
中的路由守卫保护。
为确保hooks.server.ts
对每个嵌套路径运行,在private
目录中放置一个+layout.server.ts
文件。该文件可以为空,但必须存在以保护没有自己的+layout|page.server.ts
的路由。
12345/** * 此文件对于保护`private`目录中的所有路由是必要的。 * 它使该目录中的路由成为动态路由,会发送服务器请求, * 从而触发`hooks.server.ts`。 **/