使用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.ts
1import { 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中包含此文件。jssrc/hooks.client.js`:

1import '$lib/db'

同步页面存储

编辑+layout.svelte文件并设置客户端。

src/routes/+layout.svelte
1<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() 的每个PageLoadLayoutLoad都将更新。

如果某些数据在登录/注销时未更新,则可以返回到 invalidateAll()

向客户端发送会话

要使会话对UI(页面、布局)可用,请在根布局服务器加载函数中传递会话:

src/routes/+layout.server.ts
1import 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.ts
1import 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.ts
1/// <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.svelte
1<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.svelte
1<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 并返回 sessionsupabaseClient

src/routes/profile/+page.ts
1import 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.ts
1import 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.ts
1import 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.ts
1import 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.ts
1import { 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.svelte
1<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.ts
1// 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.ts
1// 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.ts
1/// <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.svelte
1<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.ts
1import { 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.ts
1import 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.ts
1import { 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.svelte
1<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.ts
1import { 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.ts
1/// <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.svelte
1<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.svelte
1<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.ts
1import { 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  })

其他链接