使用SveltKit进行Supabase认证
该子模块提供了在SvelteKit中实现用户身份验证的方便助手应用程序。
安装
此库支持Node.js^16.15.0
。
1npm install @supabase/auth-helpers-sveltekit
入门
配置
设置填充环境变量。对于本地开发,您可以将其设置为.env
文件。参见示例.
# Find these in your Supabase project settings > API PUBLIC_SUPABASE_URL=https://your-project.supabase.co PUBLIC_SUPABASE_ANON_KEY=your-anon-key
设置Suabase客户端#
首先创建一个db.ts
文件,并实例化 supabaseClient
。
src/lib/db.ts1import { createClient } from '@supabase/auth-helpers-sveltekit' 2import { env } from '$env/dynamic/public' 3// or use the static env 4// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'; 5 6export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY)
要确保在服务器和客户端上初始化了客户端,请在src/hooks.server.js
中包含此文件。js和
src/hooks.client.js`:
1import '$lib/db'
同步页面存储
编辑+layout.svelte
文件并设置客户端。
src/routes/+layout.svelte1<script> 2 import { supabaseClient } from '$lib/db' 3 import { invalidate } from '$app/navigation' 4 import { onMount } from 'svelte' 5 6 onMount(() => { 7 const { 8 data: { subscription }, 9 } = supabaseClient.auth.onAuthStateChange(() => { 10 invalidate('supabase:auth') 11 }) 12 13 return () => { 14 subscription.unsubscribe() 15 } 16 }) 17</script> 18 19<slot />
调用 invalidate('supabase:auth')
时,使用getSupabase()
的每个PageLoad
或 LayoutLoad
都将更新。
如果某些数据在登录/注销时未更新,则可以返回到 invalidateAll()
。
向客户端发送会话
要使会话对UI(页面、布局)可用,请在根布局服务器加载函数中传递会话:
src/routes/+layout.server.ts1import type { LayoutServerLoad } from './$types'
2import { getServerSession } from '@supabase/auth-helpers-sveltekit'
3
4export const load: LayoutServerLoad = async (event) => {
5 return {
6 session: await getServerSession(event),
7 }
8}
此外,如果使用invalidate('supabase:auth')
,则可以创建布局加载函数:
src/routes/+layout.ts1import type { LayoutLoad } from './$types' 2import { getSupabase } from '@supabase/auth-helpers-sveltekit' 3 4export const load: LayoutLoad = async (event) => { 5 const { session } = await getSupabase(event) 6 return { session } 7}
这会减少服务器调用,因为客户端自己管理会话。
键入
为了充分利用TypeScript及其智能感知,您应该将我们的类型导入到SveltKit项目附带的app.d.ts
类型定义文件中。
src/app.d.ts1/// <reference types="@sveltejs/kit" />
2
3// See https://kit.svelte.dev/docs/types#app
4// for information about these interfaces
5// and what to do when importing types
6declare namespace App {
7 interface Supabase {
8 Database: import('./DatabaseDefinitions').Database
9 SchemaName: 'public'
10 }
11
12 // interface Locals {}
13 interface PageData {
14 session: import('@supabase/supabase-js').Session | null
15 }
16 // interface Error {}
17 // interface Platform {}
18}
基本设置
现在,您可以通过检查 $page.data
中的session
对象来确定用户是否在客户端进行了身份验证。
src/routes/+page.svelte1<script> 2 import { page } from '$app/stores' 3</script> 4 5{#if !$page.data.session} 6<h1>I am not logged in</h1> 7{:else} 8<h1>Welcome {$page.data.session.user.email}</h1> 9<p>I am logged in!</p> 10{/if}
使用RLS获取客户端数据#
为了使行级别安全在客户端获取数据时正常工作,您需要确保从 $lib/db
导入 { supabaseClient }
,并仅在 $page.data
中定义了客户端会话后才运行查询:
1<script> 2 import { supabaseClient } from '$lib/db' 3 import { page } from '$app/stores' 4 5 let loadedData = [] 6 async function loadData() { 7 const { data } = await supabaseClient.from('test').select('*').limit(20) 8 loadedData = data 9 } 10 11 $: if ($page.data.session) { 12 loadData() 13 } 14</script> 15 16{#if $page.data.session} 17<p>client-side data fetching with RLS</p> 18<pre>{JSON.stringify(loadedData, null, 2)}</pre> 19{/if}
使用RLS获取服务器端数据#
src/routes/profile/+page.svelte1<script> 2 /** @type {import('./$types').PageData} */ 3 export let data 4 $: ({ user, tableData } = data) 5</script> 6 7<div>Protected content for {user.email}</div> 8<pre>{JSON.stringify(tableData, null, 2)}</pre> 9<pre>{JSON.stringify(user, null, 2)}</pre>
要使row-level security在服务器环境中工作,您需要使用 getSupabase
帮助器来检查用户是否经过身份验证。助手需要 event
并返回 session
和 supabaseClient
:
src/routes/profile/+page.ts1import type { PageLoad } from './$types'
2import { getSupabase } from '@supabase/auth-helpers-sveltekit'
3import { redirect } from '@sveltejs/kit'
4
5export const load: PageLoad = async (event) => {
6 const { session, supabaseClient } = await getSupabase(event)
7 if (!session) {
8 throw redirect(303, '/')
9 }
10 const { data: tableData } = await supabaseClient.from('test').select('*')
11
12 return {
13 user: session.user,
14 tableData,
15 }
16}
保护API路由#
包装API路由以检查用户是否具有有效会话。如果他们未登录,则会话为 null
。
src/routes/api/protected-route/+server.ts1import type { RequestHandler } from './$types'
2import { getSupabase } from '@supabase/auth-helpers-sveltekit'
3import { json, redirect } from '@sveltejs/kit'
4
5export const GET: RequestHandler = async (event) => {
6 const { session, supabaseClient } = await getSupabase(event)
7 if (!session) {
8 throw redirect(303, '/')
9 }
10 const { data } = await supabaseClient.from('test').select('*')
11
12 return json({ data })
13}
如果您在没有有效会话cookie的情况下访问/api/protected-route
,将得到303响应。
保护措施
包装操作以检查用户是否具有有效会话。如果他们未登录,则会话为 null
。
src/routes/posts/+page.server.ts1import type { Actions } from './$types'
2import { getSupabase } from '@supabase/auth-helpers-sveltekit'
3import { error, invalid } from '@sveltejs/kit'
4
5export const actions: Actions = {
6 createPost: async (event) => {
7 const { request } = event
8 const { session, supabaseClient } = await getSupabase(event)
9 if (!session) {
10 // the user is not signed in
11 throw error(403, { message: 'Unauthorized' })
12 }
13 // we are save, let the user create the post
14 const formData = await request.formData()
15 const content = formData.get('content')
16
17 const { error: createPostError, data: newPost } = await supabaseClient
18 .from('posts')
19 .insert({ content })
20
21 if (createPostError) {
22 return invalid(500, {
23 supabaseErrorMessage: createPostError.message,
24 })
25 }
26 return {
27 newPost,
28 }
29 },
30}
如果您尝试提交带有操作的表单 ?/createPost
如果没有有效的会话cookie,您将得到403错误响应。
保存和删除会话
1import type { Actions } from './$types'
2import { invalid, redirect } from '@sveltejs/kit'
3import { getSupabase } from '@supabase/auth-helpers-sveltekit'
4
5export const actions: Actions = {
6 signin: async (event) => {
7 const { request, cookies, url } = event
8 const { session, supabaseClient } = await getSupabase(event)
9 const formData = await request.formData()
10
11 const email = formData.get('email') as string
12 const password = formData.get('password') as string
13
14 const { error } = await supabaseClient.auth.signInWithPassword({
15 email,
16 password,
17 })
18
19 if (error) {
20 if (error instanceof AuthApiError && error.status === 400) {
21 return invalid(400, {
22 error: 'Invalid credentials.',
23 values: {
24 email,
25 },
26 })
27 }
28 return invalid(500, {
29 error: 'Server error. Try again later.',
30 values: {
31 email,
32 },
33 })
34 }
35
36 throw redirect(303, '/dashboard')
37 },
38
39 signout: async (event) => {
40 const { supabaseClient } = await getSupabase(event)
41 await supabaseClient.auth.signOut()
42 throw redirect(303, '/')
43 },
44}
保护多条路线
为了避免在每条路由中写入相同的身份验证逻辑,可以使用句柄钩子同时保护多条路由。
src/hooks.server.ts1import type { RequestHandler } from './$types'
2import { getSupabase } from '@supabase/auth-helpers-sveltekit'
3import { redirect, error } from '@sveltejs/kit'
4
5export const handle: Handle = async ({ event, resolve }) => {
6 // protect requests to all routes that start with /protected-routes
7 if (event.url.pathname.startsWith('/protected-routes')) {
8 const { session, supabaseClient } = await getSupabase(event)
9
10 if (!session) {
11 throw redirect(303, '/')
12 }
13 }
14
15 // protect POST requests to all routes that start with /protected-posts
16 if (event.url.pathname.startsWith('/protected-posts') && event.request.method === 'POST') {
17 const { session, supabaseClient } = await getSupabase(event)
18
19 if (!session) {
20 throw error(303, '/')
21 }
22 }
23
24 return resolve(event)
25}
从0.7.x迁移到0.8 {#migration}#
设置Supabase客户端 {#migration-set-up-supabase-client}#
src/lib/db.ts1import { createClient } from '@supabase/supabase-js'
2import { setupSupabaseHelpers } from '@supabase/auth-helpers-sveltekit'
3import { dev } from '$app/environment'
4import { env } from '$env/dynamic/public'
5// or use the static env
6
7// import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public';
8
9export const supabaseClient = createClient(env.PUBLIC_SUPABASE_URL, env.PUBLIC_SUPABASE_ANON_KEY, {
10 persistSession: false,
11 autoRefreshToken: false,
12})
13
14setupSupabaseHelpers({
15 supabaseClient,
16 cookieOptions: {
17 secure: !dev,
18 },
19})
初始化客户端 {#migration-initialize-client}#
src/routes/+layout.svelte1<script lang="ts"> 2 // make sure the supabase instance is initialized on the client 3 import '$lib/db' 4 import { startSupabaseSessionSync } from '@supabase/auth-helpers-sveltekit' 5 import { page } from '$app/stores' 6 import { invalidateAll } from '$app/navigation' 7 8 // this sets up automatic token refreshing 9 startSupabaseSessionSync({ 10 page, 11 handleRefresh: () => invalidateAll(), 12 }) 13</script> 14 15<slot />
设置挂钩 {#migration-set-up-hooks}#
src/hooks.server.ts1// make sure the supabase instance is initialized on the server 2import '$lib/db' 3import { dev } from '$app/environment' 4import { auth } from '@supabase/auth-helpers-sveltekit/server' 5 6export const handle = auth()
使用其他句柄方法的可选if
src/hooks.server.ts1// make sure the supabase instance is initialized on the server 2import '$lib/db' 3import { dev } from '$app/environment' 4import { auth } from '@supabase/auth-helpers-sveltekit/server' 5import { sequence } from '@sveltejs/kit/hooks' 6 7export const handle = sequence(auth(), yourHandler)
键入 {#migration-typings}#
src/app.d.ts1/// <reference types="@sveltejs/kit" />
2
3// See https://kit.svelte.dev/docs/types#app
4// for information about these interfaces
5// and what to do when importing types
6declare namespace App {
7 interface Locals {
8 session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
9 }
10
11 interface PageData {
12 session: import('@supabase/auth-helpers-sveltekit').SupabaseSession
13 }
14
15 // interface Error {}
16 // interface Platform {}
17}
withPageAuth {#migration-with-page-auth}#
src/routes/protected-route/+page.svelte1<script lang="ts"> 2 import type { PageData } from './$types' 3 4 export let data: PageData 5 $: ({ tableData, user } = data) 6</script> 7 8<div>Protected content for {user.email}</div> 9<p>server-side fetched data with RLS:</p> 10<pre>{JSON.stringify(tableData, null, 2)}</pre> 11<p>user:</p> 12<pre>{JSON.stringify(user, null, 2)}</pre>
src/routes/protected-route/+page.ts1import { withAuth } from '@supabase/auth-helpers-sveltekit'
2import { redirect } from '@sveltejs/kit'
3import type { PageLoad } from './$types'
4
5export const load: PageLoad = withAuth(async ({ session, getSupabaseClient }) => {
6 if (!session.user) {
7 throw redirect(303, '/')
8 }
9
10 const { data: tableData } = await getSupabaseClient().from('test').select('*')
11 return { tableData, user: session.user }
12})
withApiAuth {#migration-with-api-auth}#
src/routes/api/protected-route/+server.ts1import type { RequestHandler } from './$types'
2import { withAuth } from '@supabase/auth-helpers-sveltekit'
3import { json, redirect } from '@sveltejs/kit'
4
5interface TestTable {
6 id: string
7 created_at: string
8}
9
10export const GET: RequestHandler = withAuth(async ({ session, getSupabaseClient }) => {
11 if (!session.user) {
12 throw redirect(303, '/')
13 }
14
15 const { data } = await getSupabaseClient().from<TestTable>('test').select('*')
16
17 return json({ data })
18})
从0.6.11及以下迁移到0.7.0 {#migration-0-7}#
此库的最新0.7.0版本中有许多突破性的更改。
环境变量前缀
环境变量前缀现在是PUBLIC_
而不是VITE_
(例如,VITE_SUPABASE_URL
现在是BUBLIC_SUPABASE_URL
)。
设置Supabase客户端 {#migration-set-up-supabase-client-0-7}#
src/lib/db.ts1import { createSupabaseClient } from '@supabase/auth-helpers-sveltekit'; 2 3const { supabaseClient } = createSupabaseClient( 4 import.meta.env.VITE_SUPABASE_URL as string, 5 import.meta.env.VITE_SUPABASE_ANON_KEY as string 6); 7 8export { supabaseClient };
初始化客户端 {#migration-initialize-client-0-7}#
src/routes/__layout.svelte1<script> 2 import { session } from '$app/stores' 3 import { supabaseClient } from '$lib/db' 4 import { SupaAuthHelper } from '@supabase/auth-helpers-svelte' 5</script> 6 7<SupaAuthHelper {supabaseClient} {session}> 8 <slot /> 9</SupaAuthHelper>
设置挂钩 {#migration-set-up-hooks-0-7}#
src/hooks.ts1import { handleAuth } from '@supabase/auth-helpers-sveltekit' 2import type { GetSession, Handle } from '@sveltejs/kit' 3import { sequence } from '@sveltejs/kit/hooks' 4 5export const handle: Handle = sequence(...handleAuth()) 6 7export const getSession: GetSession = async (event) => { 8 const { user, accessToken, error } = event.locals 9 return { 10 user, 11 accessToken, 12 error, 13 } 14}
键入 {#migration-typings-0-7}#
src/app.d.ts1/// <reference types="@sveltejs/kit" />
2// See https://kit.svelte.dev/docs/types#app
3// for information about these interfaces
4declare namespace App {
5 interface UserSession {
6 user: import('@supabase/supabase-js').User
7 accessToken?: string
8 }
9
10 interface Locals extends UserSession {
11 error: import('@supabase/supabase-js').ApiError
12 }
13
14 interface Session extends UserSession {}
15
16 // interface Platform {}
17 // interface Stuff {}
18}
检查客户端上的用户
src/routes/index.svelte1<script> 2 import { session } from '$app/stores' 3</script> 4 5{#if !$session.user} 6<h1>I am not logged in</h1> 7{:else} 8<h1>Welcome {$session.user.email}</h1> 9<p>I am logged in!</p> 10{/if}
withPageAuth#
src/routes/protected-route.svelte1<script lang="ts" context="module"> 2 import { supabaseServerClient, withPageAuth } from '@supabase/auth-helpers-sveltekit' 3 import type { Load } from './__types/protected-page' 4 5 export const load: Load = async ({ session }) => 6 withPageAuth( 7 { 8 redirectTo: '/', 9 user: session.user, 10 }, 11 async () => { 12 const { data } = await supabaseServerClient(session.accessToken).from('test').select('*') 13 return { props: { data, user: session.user } } 14 } 15 ) 16</script> 17 18<script> 19 export let data 20 export let user 21</script> 22 23<div>Protected content for {user.email}</div> 24<p>server-side fetched data with RLS:</p> 25<pre>{JSON.stringify(data, null, 2)}</pre> 26<p>user:</p> 27<pre>{JSON.stringify(user, null, 2)}</pre>
withApiAuth#
src/routes/api/protected-route.ts1import { supabaseServerClient, withApiAuth } from '@supabase/auth-helpers-sveltekit'
2import type { RequestHandler } from './__types/protected-route'
3
4interface TestTable {
5 id: string
6 created_at: string
7}
8
9interface GetOutput {
10 data: TestTable[]
11}
12
13export const GET: RequestHandler<GetOutput> = async ({ locals, request }) =>
14 withApiAuth({ user: locals.user }, async () => {
15 // Run queries with RLS on the server
16 const { data } = await supabaseServerClient(request).from('test').select('*')
17
18 return {
19 status: 200,
20 body: { data },
21 }
22 })