认证

为 SvelteKit 配置服务端认证


设置服务器端认证以在SvelteKit中使用基于Cookie的身份验证。

1

安装Supabase包

安装 @supabase/supabase-js 包和辅助包 @supabase/ssr

1
npm install @supabase/supabase-js @supabase/ssr
2

设置环境变量

在项目根目录创建 .env.local 文件。

填写您的 PUBLIC_SUPABASE_URLPUBLIC_SUPABASE_ANON_KEY

Project URL
Anon key
1
2
PUBLIC_SUPABASE_URL=<your_supabase_project_url>PUBLIC_SUPABASE_ANON_KEY=<your_supabase_anon_key>
3

设置服务器端钩子

src/hooks.server.ts 中设置服务器端钩子。这些钩子用于:

  • 创建特定于请求的Supabase客户端,使用来自请求Cookie的用户凭证(该客户端仅用于服务器端代码)
  • 检查用户认证状态
  • 保护受保护页面
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import { 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)
4

创建TypeScript类型定义

为防止TypeScript错误,为新的 event.locals 属性添加类型定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import 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 {}
5

在根布局中创建Supabase客户端

在根 +layout.ts 中创建Supabase客户端。该客户端可用于从客户端或服务器访问Supabase。为了在服务器上获取Auth令牌,使用 +layout.server.ts 文件从 event.locals 传入会话。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { 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 }}
6

监听Auth事件

在客户端设置Auth事件监听器,以处理会话刷新和登出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<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()}
7

创建第一个页面

创建您的第一个页面。此示例页面从服务器调用Supabase以从数据库获取国家列表。

这是一个使用公开可读数据的公共页面示例。

要填充数据库,请从仪表板运行colors quickstart

1
2
3
4
5
6
import 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 ?? [] }}
8

更改Auth确认路径

如果您启用了电子邮件确认(默认启用),新用户注册后将收到一封确认邮件。

更改电子邮件模板以支持服务器端认证流程。

转到仪表板中的Auth templates页面。在Confirm signup模板中,将{{ .ConfirmationURL }}更改为{{ .SiteURL }}/auth/confirm?token_hash={{ .TokenHash }}&type=email

9

创建登录页面

接下来,创建一个登录页面让用户注册和登录。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import { 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') } },}
10

创建注册确认路由

通过创建处理电子邮件验证的API路由来完成注册流程。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import 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)}
11

创建私有路由

创建只能由认证用户访问的私有路由。private目录中的路由由hooks.server.ts中的路由守卫保护。

为确保hooks.server.ts对每个嵌套路径运行,在private目录中放置一个+layout.server.ts文件。该文件可以为空,但必须存在以保护没有自己的+layout|page.server.ts的路由。

1
2
3
4
5
/** * 此文件对于保护`private`目录中的所有路由是必要的。 * 它使该目录中的路由成为动态路由,会发送服务器请求, * 从而触发`hooks.server.ts`。 **/