使用 Supabase 和 Next.js 实现 Magic Link 身份验证和路由控制

2025-05-24

使用 Supabase 和 Next.js 实现 Magic Link 身份验证和路由控制

点击此处观看本指南的视频教程。免责声明:Supabase 也是我的 YouTube 频道的赞助商。

虽然Supabase因其实时数据库和 API 层而广为人知,但我喜欢它的一点是它提供的大量易于设置的开箱即用的身份验证机制。

Supabase 身份验证

魔法链接

我最喜欢的一个功能是 Magic Link。你可能以前用过 Magic Link。Magic Link 会通过电子邮件向用户发送一个链接,其中包含一个通过自定义 URL 和访问令牌进行身份验证的链接。

当用户访问该 URL 时,其浏览器存储中会设置一个会话,并将用户重定向回应用程序,在此过程中对用户进行身份验证。

这正成为一种非常流行的验证用户身份的方式,因为他们不必记住另一个密码,它提供了非常好的用户体验。

Next.js

使用 Next.js,您不仅可以通过客户端授权保护路由,而且为了增加安全性,您还可以进行服务器端授权,并在getServerSideProps已设置 cookie 且在请求上下文中可用时进行重定向。

这也是 Supabase 派上用场的地方。它内置了在 SSR 和 API 路由中设置和获取已登录用户的 Cookie 的功能:

在 API 路由中设置用户

import { supabase } from '../../client'

export default function handler(req, res) {
  supabase.auth.api.setAuthCookie(req, res)
}
Enter fullscreen mode Exit fullscreen mode

通过 SSR 或 API 路由获取用户

export async function getServerSideProps({ req }) {
  const { user } = await supabase.auth.api.getUserByCookie(req)

  if (!user) {
    return {
      props: {} 
    }
  }

  /* if user is present, do something with the user data here */
  return { props: { user } }
}
Enter fullscreen mode Exit fullscreen mode

从 SEO 角度来看,服务器端重定向通常比客户端重定向更受欢迎 - 搜索引擎更难理解应如何处理客户端重定向。

您还可以使用该函数从 API 路由访问用户配置getUserByCookie文件,从而开辟一组全新的用例和功能。

借助 Next.js 和 Supabase,您可以轻松地使用 SSG、SSR 以及客户端数据提取和用户授权的组合来实现各种各样的应用程序,从而使该组合(以及提供这种功能组合的任何框架)变得极其有用和强大。

我们将要构建什么

在本文中,我们将构建一个 Next.js 应用程序,该应用程序支持导航、身份验证、授权、重定向(客户端和服务器端)和配置文件视图。

我们将要构建的项目对于任何需要处理用户身份的应用程序来说都是一个很好的起点,并且是了解用户身份如何使用像 Next.js 这样的现代混合框架在项目的所有不同位置工作和流动的好方法。

该项目的最终代码位于此处

构建应用程序

首先,您需要创建一个 Supabase 帐户和项目。

为此,请转到Supabase.io并单击“开始您的项目”。使用 GitHub 进行身份验证,然后在您的帐户中提供给您的组织下创建一个新项目。

创建 Supabase 帐户

为项目提供名称和密码,然后单击创建新项目。

创建您的项目大约需要 2 分钟。

接下来,打开终端并创建一个新的 Next.js 应用程序:

npx create-next-app supabase-next-auth

cd supabase-next-auth
Enter fullscreen mode Exit fullscreen mode

我们唯一需要的依赖项是@supabase/supabase-js包:

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

配置 Supabase 凭据

现在 Next.js 应用程序已经创建,它需要了解 Supabase 项目才能与其交互。

最好的方法是使用环境变量。Next.js 允许通过在项目根目录中创建一个名为.env.local的文件并将其存储在那里来设置环境变量。

为了向浏览器公开变量,您必须在变量前面加上NEXT_PUBLIC _。

在项目根目录创建一个名为.env.local的文件,并添加以下配置:

NEXT_PUBLIC_SUPABASE_URL=https://app-id.supabase.co
NEXT_PUBLIC_SUPABASE_ANON_KEY=your-public-api-key
Enter fullscreen mode Exit fullscreen mode

您可以在 Supabase 仪表板设置中找到 API URL 和 API 密钥的值:

Supabase 配置

创建 Supabase 客户端

现在已经设置了环境变量,我们可以创建一个 Supabase 实例,可以在需要时导入它。

在项目根目录中创建一个名为client.js的文件,内容如下:

/* client.js */
import { createClient } from '@supabase/supabase-js'

const supabase = createClient(
  process.env.NEXT_PUBLIC_SUPABASE_URL,
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY
)

export { supabase }
Enter fullscreen mode Exit fullscreen mode

更新索引

接下来,让我们更新pages/index.js 文件,使其比开箱即用的文件更简单。这只是为了用作一个基本的登录页面。

/* pages/index.js */
import styles from '../styles/Home.module.css'
export default function Home() {
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <h1 className={styles.title}>
          Hello World!
        </h1>
       </main>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

创建登录屏幕

接下来,让我们创建“登录”屏幕。这将为用户提供一个表单输入框,用于输入他们的电子邮件地址。

当用户提交表单时,他们将收到一个魔术链接来登录。这对新用户和现有用户都适用!

在pages目录中创建一个名为sign-in.js的新文件:

/* pages/sign-in.js */
import { useState } from 'react'
import styles from '../styles/Home.module.css'

import { supabase } from '../client'

export default function SignIn() {
  const [email, setEmail] = useState('')
  const [submitted, setSubmitted] = useState(false)
  async function signIn() {
    const { error, data } = await supabase.auth.signIn({
      email
    })
    if (error) {
      console.log({ error })
    } else {
      setSubmitted(true)
    }
  }
  if (submitted) {
    return (
      <div className={styles.container}>
        <h1>Please check your email to sign in</h1>
      </div>
    )
  }
  return (
    <div className={styles.container}>
      <main className={styles.main}>
        <h1 className={styles.title}>
          Sign In
        </h1>
        <input
          onChange={e => setEmail(e.target.value)}
          style={{ margin: 10 }}
        />
        <button onClick={() => signIn()}>Sign In</button>
       </main>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

这个文件中最主要的是这行代码:

const { error, data } = await supabase.auth.signIn({
  email
})
Enter fullscreen mode Exit fullscreen mode

只需提供用户的电子邮件地址,魔术链接身份验证就会自动进行。

个人资料视图

接下来,让我们创建个人资料视图。在pages目录中创建一个名为profile.js的新文件:

/* pages/profile.js */
import { useState, useEffect } from 'react';
import { supabase } from '../client'
import { useRouter } from 'next/router'

export default function Profile() {
  const [profile, setProfile] = useState(null)
  const router = useRouter()
  useEffect(() => {
    fetchProfile()
  }, [])
  async function fetchProfile() {
    const profileData = await supabase.auth.user()
    if (!profileData) {
      router.push('/sign-in')
    } else {
      setProfile(profileData)
    }
  }
  async function signOut() {
    await supabase.auth.signOut()
    router.push('/sign-in')
  }
  if (!profile) return null
  return (
    <div style={{ maxWidth: '420px', margin: '96px auto' }}>
      <h2>Hello, {profile.email}</h2>
      <p>User ID: {profile.id}</p>
      <button onClick={signOut}>Sign Out</button>
    </div>
  )
}
Enter fullscreen mode Exit fullscreen mode

为了检查当前登录的用户,我们调用supabase.auth.user()

如果用户已登录,我们将setProfile使用钩子设置的函数来设置用户信息useState

如果用户未登录,我们将使用useRouter钩子进行客户端重定向。

API 路由

pages/_app.js中,我们需要调用一个函数来设置 cookie,以便稍后在 SSR 路由中检索。

让我们继续创建 API 路由和函数。这将调用setAuthCookieSupabase 客户端提供给我们的 API。

pages/api文件夹中创建一个名为auth.js的新文件,并添加以下代码:

/* pages/api/auth.js */
import { supabase } from '../../client'

export default function handler(req, res) {
  supabase.auth.api.setAuthCookie(req, res)
}
Enter fullscreen mode Exit fullscreen mode

导航、身份验证监听器和设置会话 cookie

我们需要编写的最大块代码将在pages/app.js中。以下是我们需要在这里实现的内容:

  1. 导航
  2. 当身份验证状态改变时触发的监听器(由 Supabase 提供)
  3. 使用用户会话设置 cookie 的函数

除此之外,我们还需要跟踪用户的身份验证状态。这样做是为了能够根据用户是否登录来切换链接,显示或隐藏某些链接。

我们将在这里演示这一点,仅向未登录的用户显示登录链接,登录后则隐藏该链接。

/* pages/_app.js */
import '../styles/globals.css'
import { useState, useEffect } from 'react'
import Link from 'next/link'
import { supabase } from '../client'
import { useRouter } from 'next/router'

function MyApp({ Component, pageProps }) {
  const router = useRouter()
  const [authenticatedState, setAuthenticatedState] = useState('not-authenticated')
  useEffect(() => {
    /* fires when a user signs in or out */
    const { data: authListener } = supabase.auth.onAuthStateChange((event, session) => {
      handleAuthChange(event, session)
      if (event === 'SIGNED_IN') {
        setAuthenticatedState('authenticated')
        router.push('/profile')
      }
      if (event === 'SIGNED_OUT') {
        setAuthenticatedState('not-authenticated')
      }
    })
    checkUser()
    return () => {
      authListener.unsubscribe()
    }
  }, [])
  async function checkUser() {
    /* when the component loads, checks user to show or hide Sign In link */
    const user = await supabase.auth.user()
    if (user) {
      setAuthenticatedState('authenticated')
    }
  }
  async function handleAuthChange(event, session) {
    /* sets and removes the Supabase cookie */
    await fetch('/api/auth', {
      method: 'POST',
      headers: new Headers({ 'Content-Type': 'application/json' }),
      credentials: 'same-origin',
      body: JSON.stringify({ event, session }),
    })
  }
  return (
    <div>
      <nav style={navStyle}>
        <Link href="/">
          <a style={linkStyle}>Home</a>
        </Link>
        <Link href="/profile">
          <a style={linkStyle}>Profile</a>
        </Link>
        {
          authenticatedState === 'not-authenticated' && (
            <Link href="/sign-in">
              <a style={linkStyle}>Sign In</a>
            </Link>
          )
        }
        <Link href="/protected">
          <a style={linkStyle}>Protected</a>
        </Link>
      </nav>
      <Component {...pageProps} />
    </div>
  )
}

const navStyle = {
  margin: 20
}
const linkStyle = {
  marginRight: 10
}

export default MyApp
Enter fullscreen mode Exit fullscreen mode

我们需要实现的最后一页是演示服务器端保护和重定向的路由。

由于我们已经实现了设置 cookie,因此如果用户已登录,我们现在应该能够在服务器上读取 cookie。

正如我之前提到的,我们可以使用该getUserByCookie函数来做到这一点。

在pages目录中创建一个名为protected.js的新文件,并添加以下代码:

import { supabase } from '../client'

export default function Protected({ user }) {
  console.log({ user })
  return (
    <div style={{ maxWidth: '420px', margin: '96px auto' }}>
      <h2>Hello from protected route</h2>
    </div>
  )
}

export async function getServerSideProps({ req }) {
  /* check to see if a user is set */
  const { user } = await supabase.auth.api.getUserByCookie(req)

  /* if no user is set, redirect to the sign-in page */
  if (!user) {
    return { props: {}, redirect: { destination: '/sign-in' } }
  }

  /* if a user is set, pass it to the page via props */
  return { props: { user } }
}
Enter fullscreen mode Exit fullscreen mode

测试一下

现在应用程序已构建完毕,我们可以测试它了!

要运行该应用程序,请打开终端并运行以下命令:

npm run dev
Enter fullscreen mode Exit fullscreen mode

应用程序加载后,您应该可以注册并使用魔术链接登录。登录后,您应该能够查看个人资料页面,并查看您的用户 ID 以及电子邮件地址。

设置元数据和属性

如果您想继续构建用户的个人资料,您可以使用该update方法轻松完成。

例如,假设我们想允许用户设置他们的位置。我们可以使用以下代码来实现:

const { user, error } = await supabase.auth.update({ 
  data: {
    city: "New York"
  } 
})
Enter fullscreen mode Exit fullscreen mode

现在,当我们获取用户的数据时,我们应该能够查看他们的元数据:

用户元数据

该项目的最终代码位于此处

文章来源:https://dev.to/dabit3/magic-link-authentication-and-route-controls-with-supabase-and-next-js-leo
PREV
React State 5 种方式 Recoil MobX XState Redux Context
NEXT
如何以开发者身份进入加密货币领域