发布于 2026-01-06 1 阅读
0

使用 SvelteKit 实现 Supabase SSR 身份验证

使用 SvelteKit 实现 Supabase SSR 身份验证

编辑:本教程最初发布于 @supabase/supabase-auth-helpers 包,后来重写以适用于 @supabase/ssr 包的 beta 版本。Supabase 仍在对 SSR 包进行一些重大更改,以推进 1.0 版本的发布。您可以在此处关注更新。我会尽力保持教程的及时更新。本教程基于@supabase/ssr: 0.4.0[@supabase/supabase-js: 2.44.2此处应填写相关链接] @sveltejs/kit: 2.0.0

注意:Supabase 添加了一个臭名昭著的警告日志。本教程尽可能避免使用它,但即使是标准的 Supabase 内部方法也会auth.updateUser触发此警告日志。希望 Supabase 能尽快解决这个问题。

Supabase @supabase/ssr 包

Supabase 最近推出了 @supabase/ssr 包,取代了之前的 @supabase/supabase-auth-helpers 包。Supabase 通常建议使用新的 @supabase/ssr 包,该包继承了 Auth Helpers 包的核心概念,并将其提供给任何服务器框架。由于 Supabase Auth Helpers 包已停止维护,因此未来可能会被弃用。

@supabase/ssr 包旨在方便地与任何包含后端框架(例如 Next.js、SvelteKit、Astro、Remix 或 Express)一起使用。这正是 Supabase 的主要理念:提供一个更易于维护的通用包,而 Auth Helpers 则无法做到这一点。

本教程将引导您完成如何在 Sveltekit 中使用 @supabase/ssr 包的过程。实现过程非常简单、流畅且相当直接。

创建 SvelteKit 项目

创建 SvelteKit 应用,并将其命名为例如“my-sk-app-with-sb-ssr-auth”。

npm create svelte@latest my-sk-app-with-sb-ssr-auth
cd my-sk-app-with-sb-ssr-auth
npm install
Enter fullscreen mode Exit fullscreen mode

现在安装相关的Supabase软件包:

npm install @supabase/ssr @supabase/supabase-js
Enter fullscreen mode Exit fullscreen mode

创建 Supabase 项目

如果您还没有 Supabase 项目,请创建一个新的。只需按照https://supabase.com/上的说明操作,即可创建新项目。在“项目设置”仪表板的“API 详细信息”部分,复制 SUPABASE_URL 和 SUPABASE_ANON_KEY 键,这两个键将用于您的应用程序前端。

SUPABASE_URL 和 SUPABASE_ANON_KEY 键

公共变量

在 SvelteKit 项目根目录中创建一个 .env.local 文件。使用您刚刚从 Supabase 项目控制面板复制的 SUPABASE_URL 和 SUPABASE_ANON_KEY 密钥。

# .env.local
PUBLIC_SUPABASE_URL=your_supabase_project_url
PUBLIC_SUPABASE_ANON_KEY=your_supabase_anon_key
Enter fullscreen mode Exit fullscreen mode

在 Hooks 中创建 Supabase createServerClient

在 SvelteKit 项目根目录下创建 hooks.server.js 文件。此文件中,我们将使用导入的环境变量设置 Supabase 服务器客户端。使用 ssr 包创建 Supabase 客户端会自动配置其使用 Cookie。

您可能好奇为什么我们要用 supabase.auth.getUser() 获取的用户对象重新填充通过 supabase.auth.getSession() 获取的会话中的用户。这是为了避免Supabase 在您使用 supabase.auth.getSession() 返回的用户对象时记录的那些臭名昭著的警告。当 Supabase 的警告日志问题得到解决后,就不再需要这种 session.user 的变通方法了。

你甚至可以像下面这段代码块中看到的那样,将警告静音,if 'suppressGetSessionWarning但这相当麻烦。

还有一种解决方案是使用 JWT secrete 和 jose 库来验证会话。更多详情请访问此仓库

// hooks.server.js
import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
import { createServerClient } from '@supabase/ssr'

export const handle = async ({ event, resolve }) => {
  event.locals.supabaseServerClient = createServerClient(PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY, {
      cookies: {
          getAll() {
              return event.cookies.getAll()
          },
          setAll(cookiesToSet) {
              cookiesToSet.forEach(({ name, value, options }) =>
                  event.cookies.set(name, value, { ...options,path: '/' })
              )
          },
      },
  })

// if you want to silence the warnings https://github.com/supabase/auth-js/issues/873
  if ('suppressGetSessionWarning' in event.locals.supabaseServerClient.auth) {
    // @ts-expect-error - suppressGetSessionWarning is not part of the official API
    event.locals.supabaseServerClient.auth.suppressGetSessionWarning = true;
  } else {
    console.warn(
      'SupabaseAuthClient#suppressGetSessionWarning was removed. See https://github.com/supabase/auth-js/issues/888.',
    );
  }

  const getSessionAndUser = async () => {
      const { data: { session } } = await event.locals.supabaseServerClient.auth.getSession()
      if (!session) {
          return {
              session: null,
              user: null
          }
      }

      const { data: { user }, error } = await event.locals.supabaseServerClient.auth.getUser()
      if (error) {
          // JWT validation has failed
          return {
              session: null,
              user: null
          }
      }

      delete session.user
      const sessionWithUserFromUser = { ...session, user: {...user} }

      return { session: sessionWithUserFromUser, user }
  }

  const { session, user } = await getSessionAndUser()

  event.locals.session = session
  event.locals.user = user

  return resolve(event, {
      filterSerializedResponseHeaders(name) {
          return name === 'content-range' || name === 'x-supabase-api-version'
      },
  })
}
Enter fullscreen mode Exit fullscreen mode

从根服务器布局返回会话

在 routes 目录下创建 +layout.server.js 文件。该文件将传递与相应用户关联的会话。

// src/routes/+layout.server.js
export const load = async (event) => {
// clearing the cookie from a browser if the user logs out or was deleted from the database
  if (event.locals.session == null) {
  event.cookies.delete(event.locals.supabaseServerClient.storageKey, { path: '/' });
}

  return {
      session: event.locals.session,
      user: event.locals.user
  };
};
Enter fullscreen mode Exit fullscreen mode

在根布局加载中创建 Supabase createBrowserClient

现在我们将在 routes 目录下创建 +layout.js 文件。在这个文件中,我们将设置 Supabase 浏览器客户端。页面组件可以通过此加载函数从数据对象访问 Supabase 客户端。

请注意,如果您在服务器端完成所有操作,则可能不需要 Supabase 浏览器客户端。

这段代码的关键在于使用depends('supabase:auth')`supabase:auth`。`supabase:auth` 只是一个标识符,您可以随意命名。如果之后我们需要重新加载这个加载函数(尤其是在用户身份验证状态改变时),我们可以在状态改变后使用 `invalidate` 函数,并将 `supabase:auth` 作为参数传递给它(例如invalidate('supabase:auth'))。稍后您将在 `src/routes/+layout.svelte` 文件中看到我们使用此功能。

// src/routes/+layout.js
import { PUBLIC_SUPABASE_ANON_KEY, PUBLIC_SUPABASE_URL } from '$env/static/public'
import { createBrowserClient, createServerClient, isBrowser } from '@supabase/ssr'

export const load = async ({ fetch, data, depends }) => {
  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
          },
        },
      })

      const session = isBrowser()
      ? (await supabase.auth.getSession()).data.session 
      : data.session

  return {
    supabase,
    session,
    user: data.user
  }
}
Enter fullscreen mode Exit fullscreen mode

路线布局页面

使用 SvelteKit 布局页面作为登录/注销导航栏似乎是个不错的做法。我只添加了一些 CSS 代码,将登录/注销导航栏移动到了右上角。

重要的是添加 onMout 并监听 Supabase 的 onAuthStateChange(主要是为了监听用户登录或注销时的事件)。

如果发生此类变更,我们将使所有相关的加载功能失效。

一种方法是在加载函数中添加依赖函数。在这个依赖函数中,我们可以指定它依赖于哪个失效操作。所以在我们的例子中,它依赖于“supabase:auth”(即)depends('supabase:auth')。正如你上面看到的,我们已经在根布局文件 layout.js 的加载函数中完成了这一步。然后在需要运行失效操作的 Svelte 文件中,我们可以调用命名失效操作(即invalidate('supabase:auth'))。你应该仔细决定哪些操作相互关联。根布局文件 +layout.js 和 +layout.svelte 应该已经足够顶级了。

另一种更通用、更可靠的方法是使用 `@Load` invalidateAll()。当代码发生某些更改时,您可以将其添加到 Svelte 文件中,从而重新加载应用程序的所有加载函数。这样就无需费心猜测应该在哪个加载函数中添加依赖于此类更改的依赖项。

由于身份验证非常敏感,我建议调用更通用的invalidateAll()函数,以确保所有加载函数都重新运行。

在下面的示例代码中,我甚至同时使用了两种失效机制。

无论如何,失效操作都会同步服务器端的 Supabase 客户端和浏览器端的 Supabase 客户端,因为加载函数会重新运行。因此,您不仅会更新/同步会话状态,还会更新/同步所有运行该应用程序的浏览器标签页中的会话状态。

该布局包含已登录用户的注销表单,并调用相应的注销路由/操作。/logout 路由中的相关端点在“带有注销逻辑的路由”部分中进行了描述。

// src/routes/+layout.svelte
<script>
    import { enhance } from '$app/forms';
    import { invalidate, invalidateAll, goto } from '$app/navigation';
    import { onMount } from 'svelte';

    export let data;

    $: ({ supabase } = data);

    onMount(async () => {
        const {
            data: { subscription }
        } = supabase.auth.onAuthStateChange((event, _session) => {
// If you want to fain grain which routes should rerun their load function 
// when onAuthStateChange changges
// use invalidate('supabase:auth') 
// which is linked to +layout.js depends('supabase:auth'). 
// This should mainly concern all routes 
//that should be accesible only for logged in user. 
// Otherwise use invalidateAll() 
// which will rerun every load function of you app.
            invalidate('supabase:auth');
            invalidateAll();
        });
        return () => subscription.unsubscribe();
    });

    const submitLogout = async ({ cancel }) => {
        const { error } = await data.supabase.auth.signOut();
        if (error) {
            console.log(error);
        }
        cancel();
        await goto('/');
    };
</script>

<a href="/">Home</a>
<a href="/subscription">Subscriptions</a>

<span id="auth_header">
    {#if !data.session}
        <a href="/login">login</a> / <a href="/register">signup</a>
    {:else}
        <a href="/user_profile">User profile</a>
        <form action="/logout?/logout" method="POST" use:enhance={submitLogout}>
            <button type="submit">Logout</button>
        </form>
    {/if}
</span>
<slot />

<style>
    #auth_header {
        float: right;
    }
    form {
        display: inline;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

使用 PKCE 流程进行 SSR 的电子邮件身份验证

我们将使用电子邮件身份验证。为了使用更新后的电子邮件链接,我们需要设置一个端点来验证 token_hash,以及将 token_hash 交换为用户会话信息的类型。该会话信息将以 cookie 的形式存储在 Supabase 中,用于后续向 Supabase 发出的请求。此端点主要用于身份验证电子邮件确认。

在 src/routes/auth/confirm/+server.js 目录下创建一个新文件,并填充以下内容:

// src/routes/auth/confirm/+server.js
import { redirect } from '@sveltejs/kit';

export const GET = async (event) => {
    const {
        url,
        locals: { supabaseServerClient }
    } = event;
    const token_hash = url.searchParams.get('token_hash');
    const type = url.searchParams.get('type');
    const next = url.searchParams.get('next') ?? '/';

  if (token_hash && type) {
    const { error } = await supabaseServerClient.auth.verifyOtp({ token_hash, type });
    if (!error) {
      redirect(303, `/${next.slice(1)}`);
    }
  }

  // return the user to an error page with some instructions
  redirect(303, '/auth/auth-code-error');
};
Enter fullscreen mode Exit fullscreen mode

为了方便起见,我们可能会提供上面提到的授权码错误页面。

// src/routes/auth/auth-code-error/+page.svelte
There was some logging error. 
Enter fullscreen mode Exit fullscreen mode

简洁的首页

创建一个简单的首页着陆页。

// src/routes/+page.svelte
<h1>  Welcome to this website ...</h1>
Enter fullscreen mode Exit fullscreen mode

注册路线

最后,我们将实现一些日志记录逻辑。首先,我们将创建注册页面以及相关的页面服务器文件。请注意,如果表单提交无效且返回 4xx 客户端错误 HTTP 响应状态码,我们还会根据情况显示错误消息。

// src/routes/register/+page.svelte
<script>
    import { enhance } from '$app/forms';
    export let form;
</script>

<h2>Sign Up</h2>
<form action="?/register" method="POST" use:enhance>
    <label for="email">email</label>
    <input name="email" type="email" value={form?.email ?? ''} required />
    <label for="password">password</label>
    <input name="password" required />
    <button type="submit">Sign up</button>
</form>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}
Enter fullscreen mode Exit fullscreen mode

对应的 +page.server.js 文件逻辑如下。简而言之,这里我们只是调用函数supabase.auth.signUp()并提供电子邮件地址和密码,处理可能出现的错误,最终将用户重定向到电子邮件检查页面。

// src/routes/register/+page.server.js
import { fail, redirect } from "@sveltejs/kit"
import { AuthApiError } from '@supabase/supabase-js'

export const actions = {
    register: async (event) => {
        const { request, locals } = event
            const formData = await request.formData()
            const email = formData.get('email')
            const password = formData.get('password')

            const { data, error: err } = await locals.supabaseServerClient.auth.signUp({
                email: email,
                password: password
            })

            if (err) {
                if (err instanceof AuthApiError && err.status >= 400 && err.status < 500) {
                    return fail(400, {
                        error: "invalidCredentials", email: email, invalid: true, message: err.message
                    })
                }
                return fail(500, {
                    error: "Server error. Please try again later.",
                })
            }
            // signup for existing user returns an obfuscated/fake user object without identities https://supabase.com/docs/reference/javascript/auth-signup
            if (!err && !!data.user && !data.user.identities.length ) {
                return fail(409, {
                    error: "User already exists", email: email, invalid: true, message: "User already exists"
                })
            }
            redirect(303, "/check_email");
    }
}


  export const load = async ({ locals }) => {
    // if the user is already logged in redirect to the home page 
    if (locals.session) {
        redirect(303, '/');
    }
}
Enter fullscreen mode Exit fullscreen mode

您还需要更新 Supabase 身份验证电子邮件模板。

Supabase 身份验证电子邮件模板

前往您的 Supabase 项目控制面板网站,在身份验证部分,按如下方式更新确认注册电子邮件模板。

<h2>Confirm your signup</h2>

<p>Follow this link to confirm your user:</p>
<p>
  <a href="http://localhost:5173/auth/confirm?token_hash={{ .TokenHash }}&type=email"
    >Confirm your email</a>
</p>
Enter fullscreen mode Exit fullscreen mode

注意:当您最终托管您的应用程序时,请务必将所有 Supabase 电子邮件模板中的“ http://localhost:5173 ”更改为您的应用程序网站地址。

检查电子邮件路由

创建包含简单 +page.svelte 文件的 check_email 路由。

// src/routes/check_email/+page.svelte
<p>Check your email to confirm.</p>
Enter fullscreen mode Exit fullscreen mode

带有登录逻辑的路由

创建登录路由,使用户能够登录。

// src/routes/login/+page.svelte
<script>
    import { enhance } from '$app/forms';
    export let form;
</script>

<h2>Log in</h2>
<form action="?/login" method="POST" use:enhance>
    <label for="email">email</label>
    <input name="email" type="email" value={form?.email ?? ''} required />
    <label for="password">password</label>
    <input name="password" required />
    <button type="submit">Login</button>
</form>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}

<p>Forgot your password? <a href="/reset_password">Reset password</a></p>
Enter fullscreen mode Exit fullscreen mode

相应的 +page.server.js 文件包含登录操作。简单来说,登录操作就是调用supabase.auth.signInWithPassword()该程序,传入邮箱和密码,处理可能出现的错误,并在必要时重定向用户。

// src/routes/login/+page.server.js
import { fail, redirect } from '@sveltejs/kit';
import { AuthApiError } from '@supabase/supabase-js';

export const actions = {
    login: async (event) => {
        const { request, url, locals } = event;
        const formData = await request.formData();
        const email = formData.get('email');
        const password = formData.get('password');

        const { data, error: err } = await locals.supabaseServerClient.auth.signInWithPassword({
            email: email,
            password: password
        });

        if (err) {
            if (err instanceof AuthApiError && err.status === 400) {
                return fail(400, {
                    error: 'Invalid credentials',
                    email: email,
                    invalid: true,
                    message: err.message
                });
            }
            return fail(500, {
                message: 'Server error. Try again later.'
            });
        }

        redirect(307, '/');
    },
}

 export const load = async ({ locals }) => {
    // if there is a user's session redirect back to the home page
    if (locals.session) {
        redirect(303, '/');
    }
}
Enter fullscreen mode Exit fullscreen mode

带有注销逻辑的路由

如果用户注销supabase.auth.signOut(),我们会调用 `getlogout()` 方法,该方法会自动删除用户 cookie 并重定向到首页。目前只有一个 `+page.server.js` 文件,所以我添加了无条件重定向,而没有添加 `+page.svelte` 文件。如上所述,注销的客户端表单已经位于 `+layout.svelte` 文件中。

// src/routes/logout/+page.server.js
import { redirect } from '@sveltejs/kit';

export const actions = {
    logout: async ({ locals }) => {
        await locals.supabaseServerClient.auth.signOut()    
        redirect(303, '/');
    }
}

// no one should visit this page
export async function load() {
        redirect(303, '/');
}
Enter fullscreen mode Exit fullscreen mode

重置密码

创建一个名为 reset_password 的密码重置路由,同时添加一个 +page.svelte 文件和一个 +page.server.js 文件。

// src/routes/reset_password/+page.svelte
<script>
    import { enhance } from '$app/forms';
    export let form
</script>

<h2>Where should we send you a link for password reset?</h2>
<form action="?/reset_password" method="POST" use:enhance>
    <label for="email">email</label>
    <input type="email" name="email" placeholder="name@domain.com" required />
    <button type="submit">Get password</button>
</form>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}
Enter fullscreen mode Exit fullscreen mode

简而言之,在 reset_password/+page.server.js 中,我们只是调用supabase.auth.resetPasswordForEmail()它并提供电子邮件和密码更新页面的路由,处理可能的错误并最终重定向用户。

// src/routes/reset_password/+page.server.js
import { fail, redirect } from "@sveltejs/kit"
import { AuthApiError } from "@supabase/supabase-js"

export const actions = {
    reset_password: async ({ request, locals }) => {
        const formData = await request.formData()
        const email = formData.get('email')

        const { data, error: err } = await locals.supabaseServerClient.auth.resetPasswordForEmail(
            email, 
            {redirectTo: '/update_password'}
        )

        if (err) {
            if (err instanceof AuthApiError && err.status === 400) {
                return fail(400, {
                    error: "invalidCredentials", email: email, invalid: true, message: err.message
                })
            }
            return fail(500, {
                error: "Server error. Please try again later.",
            })
        }

        redirect(303, "/check_email");
    },
}

export const load = async ({ locals }) => {
    // if the user is already logged in redirect to the home page 
    if (locals.session) {
        redirect(303, '/');
    }
}
Enter fullscreen mode Exit fullscreen mode

Supabase 的重置密码邮件模板如下所示。点击链接会将用户引导至我们应用程序的 /update_password 页面。

<h2>Reset Password</h2>

<p>Follow this link to reset the password for your user:</p>
<p>
  <a
    href="http://localhost:5173/auth/confirm?token_hash={{ .TokenHash }}&type=recovery&next=/update_password"
    >Reset Password</a
  >
</p>
Enter fullscreen mode Exit fullscreen mode

更新密码路由

如前所述,重置密码需要通过 update_password 路由,用户可以在该路由中输入新密码。让我们创建这个 update_password 路由。

// src/routes/update_password/+page.svelte
<script>
    import { enhance } from '$app/forms';
    export let form
</script>

<h2>Change your password</h2>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}
<form action="?/update_password" method="POST" use:enhance>
    <label for="new_password"> New password </label>
    <input name="new_password" required/>   
    <label for="password_confirm">Confirm new password</label>
    <input name="password_confirm" required/>       
    <button>Update password</button>
</form>
Enter fullscreen mode Exit fullscreen mode

简单来说,我们在这里只是supabase.auth.updateUser()提供新密码,处理可能出现的错误,并最终将用户重定向到其个人资料页面。

// src/routes/update_password/+page.server.js
import { AuthApiError } from "@supabase/supabase-js"
import { fail, redirect } from "@sveltejs/kit"

export const actions = {
    update_password: async ({ request, locals }) => {
        const formData = await request.formData()
        const password = formData.get('new_password')

        const { data, error: err } = await locals.supabaseServerClient.auth.updateUser({
            password
        })

        if (err) {
            if (err instanceof AuthApiError && err.status >= 400 && err.status < 500) {
                return fail(400, {
                    error: "invalidCredentials", invalid: true, message: err.message
                })
            }
            return fail(500, {
                error: "Server error. Please try again later.",
            })
        }

        redirect(303, "/user_profile");
    },
}

export const load = async ({ locals }) => {
    // if there is no user's session redirect back to the home page
    if (!locals.session) {
        redirect(303, '/');
    }
  }
Enter fullscreen mode Exit fullscreen mode

更新电子邮件路由

用户可能需要更新电子邮件地址,因此可以使用 update_email 路由来完成此操作。请记住,需要提供新旧两个电子邮件地址的确认信息。

// src/routes/update_email/+page.svelte
<script>
    import { enhance } from '$app/forms';
    export let form;
</script>

<h2>Change your email</h2>
<form action="?/update_email" method="POST" use:enhance>
    <label for="email"> new email </label>
    <input type="email" name="email" required />
    <button>Change email</button>
</form>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}
Enter fullscreen mode Exit fullscreen mode

简而言之,我们supabase.auth.updateUser()这次只是提供新的电子邮件地址,处理可能出现的错误,并最终将用户重定向到其个人资料页面。

// src/routes/update_email/+page.server.js
import { AuthApiError } from "@supabase/supabase-js"
import { fail, redirect } from "@sveltejs/kit"

export const actions = {
    update_email: async ({ request, locals }) => {
        const formData = await request.formData()
        const email = formData.get('email')

        const { data, error: err } = await locals.supabaseServerClient.auth.updateUser({
            email
         })

          if (err) {
            if (err instanceof AuthApiError && err.status >= 400 && err.status < 500) {
                return fail(400, {
                    error: "invalidCredentials", invalid: true, message: err.message
                })
            }
            return fail(500, {
                error: "Server error. Please try again later.",
            })
        }

        redirect(303, "/check_email");
    },
}

export const load = async ({ locals }) => {
    // if there is no user's session redirect back to the home page
    if (!locals.session) {
        redirect(303, '/');
    }
}
Enter fullscreen mode Exit fullscreen mode

Supabase 的“更改电子邮件地址”电子邮件模板如下所示。

<h2>Confirm Change of Email</h2>

<p>Follow this link to confirm the update of your email from {{ .Email }} to {{ .NewEmail }}:</p>
<p>
  <a href="http://localhost:5173/auth/confirm?token_hash={{ .TokenHash }}&type=email_change">
    Change Email
  </a>
</p>
Enter fullscreen mode Exit fullscreen mode

用户个人资料路线

我们仍然缺少用户个人资料路由,用户可以通过该路由管理帐户。让我们创建 user_profile 路由并添加相应的文件。

// src/routes/user_profile/+page.svelte
<script>
    export let data;
</script>

<h2>User profile</h2>
<p>{data.session.user.email}</p>
<p><a href="/update_email">Change your email</a></p>
<p><a href="/update_password">Change password</a></p>
<p><a href="/delete_user">Delete my account</a></p>
Enter fullscreen mode Exit fullscreen mode

我想这个页面应该只有登录用户才能访问。

// src/routes/user_profile/+page.server.js
import { redirect } from "@sveltejs/kit"

export const load = async ({ locals }) => {
    // redirect if there is no user's session
    if (!locals.session) {
        redirect(303, '/');
    }

    return {
        session: locals.session,
        user: locals.user
    }
}
Enter fullscreen mode Exit fullscreen mode

删除用户帐户路由

对于是否应该允许用户删除自己的账户,大家的意见可能不尽相同。但由于这在 Supabase 中可能比较棘手,所以这里提供一种解决方案:创建 delete_user 路由。

// src/routes/delete_user/+page.svelte
<script>
    import { enhance } from '$app/forms';
    export let form
</script>

<h2>Delete your user account</h2>
<form action="?/delete_user" method="POST" use:enhance>
    <button type="submit">Delete my user account</button>
</form>
{#if form?.invalid}<mark>{form?.message}!</mark>{/if}
Enter fullscreen mode Exit fullscreen mode

Supbase 使用一个特殊的、由秘密服务角色密钥创建的身份验证客户端来删除用户。但是,使用该服务角色密钥创建的客户端拥有极高的超级用户/管理员权限。如果您不想使用这个强大的密钥,这里有一个技巧。

在 Supabase 控制面板中,转到 SQL 编辑器。

Supabase SQL 编辑器

在 SQL 编辑器中粘贴并运行此函数。

    CREATE or replace function delete_user()
    returns void
    LANGUAGE SQL SECURITY DEFINER
    AS $$
    --delete from public.profiles where id = auth.uid();
    delete from auth.users where id = auth.uid();
    $$;
Enter fullscreen mode Exit fullscreen mode

现在,您可以使用delete_user()supabase.rpc 方法从服务器使用此 Supabase 数据库函数,并将 delete_user 数据库函数的名称作为参数,如下所示。

// src/routes/delete_user/+page.server.js
import { PRIVATE_RPC_DELETE_USER } from '$env/static/private'
import { redirect } from "@sveltejs/kit"

export const actions = {
    delete_user: async ({ locals, request,  cookies }) => {
    const storageKey = locals.supabaseServerClient.storageKey
    await locals.supabaseServerClient.rpc('delete_user');
    cookies.delete(storageKey, { path: '/' });
    redirect(303, "/");
    }
}

  export const load = async ({ locals }) => {
    // if there is no user's session redirect back to the home page
    if (!locals.session) {
        redirect(303, '/');
    }
}
Enter fullscreen mode Exit fullscreen mode

删除用户 cookie 也非常重要。cookie 的名称可以在 [supabase.storageKey此处](链接) 中找到。删除 cookie 后,应用程序会将用户从所有请求会话的页面中移除。

项目结构概述

以下是包含所有 +page.svelte 和 +page.server.js 文件的项目结构截图。(其中也包含 Stripe 订阅的路由,所以不要混淆)

SvelteKit Supabase SSR 项目结构

感谢阅读

好了,就这些。如果有什么地方对你不起作用,欢迎留言。如果你觉得这篇教程哪怕有一点点用,请点个赞。

您可以将受保护的路由逻辑移到一个地方(可能在钩子中),例如,这样就不会在相关的 +page.server.js 文件中重复编写。

我希望尽快发布一些内容,今年一直忙于一个 SvelteKit 项目,但现在它快要完成了,所以有更多的时间写博客了。

2023年12月11日更新:
新增了现有用户注册/登录的使用场景。
新增了用户从数据库中删除时清除cookie的使用场景。

2023年12月12日更新:
正如一些用户所注意到的,代码中有一个 `addUserprofileToUser()` 辅助函数。为了避免混淆,我已删除这部分代码。`addUserprofileToUser()` 实用函数用于丰富用户会话中的用户数据。原因是 Supabase 不允许您向身份验证表中添加任何数据。为此,您必须创建一个新表(例如,创建一个名为 `user_profile` 的表,其主键(id)引用身份验证表的 id)。

我的 addUserprofileToUser() 函数如下所示:

export default async function addUserprofileToUser (session, supabase, user) {
    if (session) {
      let { data, error } = await supabase
        .from('user_profile')
        .select("*")
        .eq('id', user.id)
        .single()
      user.user_profile = data
    }
  }
Enter fullscreen mode Exit fullscreen mode

使用 addUserprofileToUser 的 hooks.server.js 文件将如下所示。

import { PUBLIC_SUPABASE_URL, PUBLIC_SUPABASE_ANON_KEY } from '$env/static/public'
import { createServerClient } from '@supabase/ssr'
import addUserprofileToUser from './utils/addUserprofileToUser'

...

      delete session.user
      await addUserprofileToUser(session, event.locals.supabaseServerClient, user)
      const sessionWithUserFromUser = { ...session, user: {...user} }

      return { session: sessionWithUserFromUser, user }
...
Enter fullscreen mode Exit fullscreen mode

`addUserprofileToUser` 函数的作用是从 `user_profile` 表中获取特定用户的数据,并丰富其会话信息。我await addUserprofileToUser(session,event.locals.supabase)在 `hooks.server.js` 文件中调用此函数。

个人资料表中包含用户的 Stripe ID、订阅计划等信息。但 Stripe SaaS 的这些功能需要全新的教程 :-) 也许下次吧。

2024 年 2 月 7 日更新:
更新以帮助迁移到 SvelteKit v2

2024年2月11日更新:
我已经更新了教程,使其完全实现invalidate('supabase:auth');。同时添加了单独的注销路由。因此,应用程序的客户端部分会在需要时正确刷新相关的加载函数,所有内容都能保持同步,包括在多个浏览器标签页中打开的应用程序。

2024年7月9日更新:
由于 Supabase SSR 软件包更新而进行更新。

更新于 2024 年 10 月 11 日: Supabase 引入了非对称 JWT 和 API 密钥,这些变更具有重大影响。您可以在我的文章《Supabase 引入非对称 JWT 和 API 密钥身份验证,这些变更具有重大影响》
中了解更多详情

文章来源:https://dev.to/kvetoslavnovak/supabase-ssr-auth-48j4