发布于 2026-01-05 8 阅读
0

🔑 Passwordless Authentication with Next.js, Prisma, and next-auth Passwordless Authentication with Next.js, Prisma, and next-auth DEV's Worldwide Show and Tell Challenge Presented by Mux: Pitch Your Projects!

🔑 使用 Next.js、Prisma 和 next-auth 实现无密码身份验证

使用 Next.js、Prisma 和 next-auth 实现无密码身份验证

由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!

使用 Next.js、Prisma 和 next-auth 实现无密码身份验证

本文将介绍如何使用Prismanext-auth为Next.js应用添加无密码身份验证。完成本教程后,用户将能够使用 GitHub 帐户或直接发送到其邮箱的Slack 式“魔法链接”登录您的应用。

如果你想跟着做,请克隆这个仓库并切换到start-here相应的分支!😃

原始 `next-auth` 结束绘制 OAuth 演示
快来看看这流畅的身份验证流程!


如果你想观看本教程的实时编码演示版本,请查看下面的录像!👇


步骤 0:依赖项和数据库设置

在开始之前,让我们先将 Prisma 安装next-auth到 Next.js 项目中。



npm i next-auth

npm i -D @prisma/cli @types/next-auth


Enter fullscreen mode Exit fullscreen mode

本教程中使用的是 TypeScript,因此我也会安装相应的类型定义。next-auth

您还需要一个 PostgreSQL 数据库来存储所有用户数据和活动令牌。

如果你还没有数据库,Heroku 可以免费托管 PostgreSQL 数据库,非常方便!你可以看看Nikolas Burk这篇文章,了解如何设置。

如果你是 Docker 的粉丝,并且希望在开发过程中将所有内容都放在本地,你也可以看看我制作的这个关于如何使用 Docker Compose 实现此操作的视频。

在进行下一步之前,请确保您拥有以下格式的 PostgreSQL URI:



postgresql://<USER>:<PASSWORD>@<HOST_NAME>:<PORT>/<DB_NAME>


Enter fullscreen mode Exit fullscreen mode

步骤 1:初始化 Prisma

太棒了!让我们生成一个Prisma 初始模式和一个@prisma/client模块到项目中。



npx prisma init


Enter fullscreen mode Exit fullscreen mode

请注意,您的项目下会创建一个新目录prisma。所有数据库操作都在这里进行。🧙‍♂️

现在,将虚拟数据库 URI 替换/prisma/.env为您自己的 URI。

运行原始 `npx prisma init` 命令后的项目结构
项目运行后的结构npx prisma init

步骤 2:定义用于身份验证的数据库模式

next-auth为了使其无缝运行,我们需要在数据库中包含特定的表。在我们的项目中,模式文件位于/prisma/schema.prisma……

我们暂时使用默认模式,但要知道,您始终可以自行扩展或自定义数据模型。

注意:如果您已有数据库,在替换虚拟数据库 URI 后,您可以运行命令生成数据库npx prisma introspect配置文件schema.prisma,并在此基础上进行操作。然后,您应该将以下数据模型添加到生成的配置schema.prisma文件中。



datasource db {
  provider = "postgresql"
  url      = env("DATABASE_URL")
}

generator client {
  provider = "prisma-client-js"
}

model Account {
  id                 Int       @default(autoincrement()) @id
  compoundId         String    @unique @map(name: "compound_id")
  userId             Int       @map(name: "user_id")
  providerType       String    @map(name: "provider_type")
  providerId         String    @map(name: "provider_id")
  providerAccountId  String    @map(name: "provider_account_id")
  refreshToken       String?   @map(name: "refresh_token")
  accessToken        String?   @map(name: "access_token")
  accessTokenExpires DateTime? @map(name: "access_token_expires")
  createdAt          DateTime  @default(now()) @map(name: "created_at")
  updatedAt          DateTime  @default(now()) @map(name: "updated_at")

  @@index([providerAccountId], name: "providerAccountId")
  @@index([providerId], name: "providerId")
  @@index([userId], name: "userId")

  @@map(name: "accounts")
}

model Session {
  id           Int      @default(autoincrement()) @id
  userId       Int      @map(name: "user_id")
  expires      DateTime
  sessionToken String   @unique @map(name: "session_token")
  accessToken  String   @unique @map(name: "access_token")
  createdAt    DateTime @default(now()) @map(name: "created_at")
  updatedAt    DateTime @default(now()) @map(name: "updated_at")

  @@map(name: "sessions")
}

model User {
  id            Int       @default(autoincrement()) @id
  name          String?
  email         String?   @unique
  emailVerified DateTime? @map(name: "email_verified")
  image         String?
  createdAt     DateTime  @default(now()) @map(name: "created_at")
  updatedAt     DateTime  @default(now()) @map(name: "updated_at")

  @@map(name: "users")
}

model VerificationRequest {
  id         Int      @default(autoincrement()) @id
  identifier String
  token      String   @unique
  expires    DateTime
  createdAt  DateTime  @default(now()) @map(name: "created_at")
  updatedAt  DateTime  @default(now()) @map(name: "updated_at")

  @@map(name: "verification_requests")
}


Enter fullscreen mode Exit fullscreen mode

让我们来详细分析一下:

在模式文件中,我们定义了 4 个数据模型—— 、、Account模型用于存储用户信息,模型用于管理用户的活动会话,而用于存储为魔法链接电子邮件登录生成的有效令牌。SessionUserVerificationRequestUserAccountSessionVerificationRequest

@map属性用于将 Prisma 字段名称映射到数据库列名称,例如compoundId映射到compound_id,这是next-auth使其正常工作所必需的。

snake_case常用于数据库环境的命名约定,但驼峰命名法才是 JavaScript 和 TypeScript 中我们通常使用的命名方式。虽然用snake_case命名 Prisma 字段完全没问题,但看起来不太美观。:)

接下来,让我们运行这些命令,将我们需要的表填充到数据库中。



npx prisma migrate save --experimental
npx prisma migrate up --experimental


Enter fullscreen mode Exit fullscreen mode

然后,运行此命令以生成针对数据库架构定制的 Prisma 客户端。



npx prisma generate


Enter fullscreen mode Exit fullscreen mode

现在,如果您使用以下命令打开Prisma Studio,您将能够检查我们刚刚在数据库中创建的所有表。



npx prisma studio


Enter fullscreen mode Exit fullscreen mode

Prisma工作室截图
Prisma Studio 型号选择

步骤 3:配置next-auth

在开始配置之前next-auth,让我们.env在项目根目录中创建另一个文件来存储将要使用的密钥next-auth.env.example如果您克隆了教程仓库,则可以从模板重命名该文件)。



SECRET=RAMDOM_STRING
SMTP_HOST=YOUR_SMTP_HOST
SMTP_PORT=YOUR_SMTP_PORT
SMTP_USER=YOUR_SMTP_USERNAME
SMTP_PASSWORD=YOUR_SMTP_PASSWORD
SMTP_FROM=YOUR_REPLY_TO_EMAIL_ADDRESS
GITHUB_SECRET=YOUR_GITHUB_API_CLIENT_SECRET
GITHUB_ID=YOUR_GITHUB_API_CLIENT_ID
DATABASE_URL=postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public


Enter fullscreen mode Exit fullscreen mode

现在,让我们创建一个新文件,作为发送到(例如)的所有请求的/pages/api/auth/[...nextauth].ts“兜底” Next.js API 路由your-app-url-root/api/authlocalhost:3000/api/auth

在文件中,首先从 `<module>` 导入必要的模块next-auth,然后定义一个 API 处理程序,该程序将请求传递给一个NextAuth函数,该函数会返回一个响应,该响应可以是完全生成的登录表单页面,也可以是回调重定向。要使用next-authPrisma 连接到数据库,您还需要导入PrismaClient并初始化一个Prisma 客户端实例。



import { NextApiHandler } from "next";
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
import Adapters from "next-auth/adapters";

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

// we will define `options` up next
const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options);
export default authHandler;


Enter fullscreen mode Exit fullscreen mode

现在我们来创建options对象。这里,您可以从多种内置身份验证提供程序中进行选择。在本教程中,我们将使用 GitHub OAuth 和“魔法链接”电子邮件来验证访问者的身份。

步骤 3.1:设置 GitHub OAuth

对于像 GitHub 这样的内置 OAuth 提供程序,您需要一个clientId和一个clientSecret,这两个都可以通过在 Github 上注册一个新的 OAuth 应用来获得。

首先,登录您的 GitHub 帐户,转到“设置”,然后导航到“开发者设置”,然后切换到“OAuth 应用”

GitHub OAuth 应用
GitHub OAuth 应用

点击“注册新应用”按钮将跳转到注册表单,填写一些应用信息。授权回调 URL/api/auth应该是我们之前定义的Next.js路由( http://localhost:3000/api/auth)。

需要注意的是,授权回调 URL字段仅支持一个 URL,这与 Auth0 不同,Auth0 允许您添加多个回调 URL,并用逗号分隔。这意味着如果您以后想使用生产环境 URL 部署应用,则需要创建一个新的 GitHub OAuth 应用。

注册 OAuth 应用
注册 OAuth 应用

点击“注册应用程序”按钮,然后您将看到新生成的客户端 ID 和客户端密钥。将这些信息复制到.env根目录下的文件中。

获取 OAuth 客户端 ID 和客户端密钥
获取 OAuth 客户端 ID 和客户端密钥

现在,让我们回到/api/auth/[...nextauth].ts并创建一个名为 的新对象options,并像下面这样获取 GitHub OAuth 凭据。



const options = {
  providers: [
    Providers.GitHub({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
  ],
};


Enter fullscreen mode Exit fullscreen mode

OAuth 提供程序通常工作方式相同,因此,如果您选择的提供程序受支持next-auth,您可以像我们在这里配置 GitHub 一样进行配置。如果没有内置支持,您仍然可以定义自定义提供程序

步骤 3.2:设置无密码电子邮件身份验证

要允许用户通过魔法链接邮件进行身份验证,您需要拥有 SMTP 服务器的访问权限。这类邮件属于事务性邮件。如果您没有自己的 SMTP 服务器,或者您的邮件服务提供商对发送邮件有严格的限制,我建议您使用SendGrid,或者也可以选择Amazon SESMailgun其他服务

准备好 SMTP 凭据后,您可以将该信息放入.env文件中,将其添加Providers.Email({})到提供商列表中,并按如下方式加载环境变量。



const options = {
  providers: [
    // Providers.GitHub ...
    Providers.Email({
      server: {
        host: process.env.SMTP_HOST,
        port: Number(process.env.SMTP_PORT),
        auth: {
          user: process.env.SMTP_USER,
          pass: process.env.SMTP_PASSWORD,
        },
      },
      from: process.env.SMTP_FROM, // The "from" address that you want to use
    }),
  ],
};


Enter fullscreen mode Exit fullscreen mode

步骤 3.3:连接 Prisma

设置的最后一步next-auth是指定使用 Prisma 与数据库通信。为此,我们将使用 Prisma 适配器并将其添加到对象中。此外,为了确保安全,options我们还需要一个密钥来签名和加密令牌及 cookie——该密钥也应从环境变量中获取。next-auth



const options = {
  providers: [
    // ...
  ],
  adapter: Adapters.Prisma.Adapter({ prisma }),
  secret: process.env.SECRET,
};


Enter fullscreen mode Exit fullscreen mode

总而言之,你的pages/api/auth/[...nextauth].ts文件应该如下所示:



import { NextApiHandler } from "next";
import NextAuth from "next-auth";
import Providers from "next-auth/providers";
import Adapters from "next-auth/adapters";

import { PrismaClient } from "@prisma/client";

const prisma = new PrismaClient();

const authHandler: NextApiHandler = (req, res) => NextAuth(req, res, options);
export default authHandler;

const options = {
  providers: [
    Providers.GitHub({
      clientId: process.env.GITHUB_ID,
      clientSecret: process.env.GITHUB_SECRET,
    }),
    Providers.Email({
      server: {
        host: process.env.SMTP_HOST,
        port: Number(process.env.SMTP_PORT),
        auth: {
          user: process.env.SMTP_USER,
          pass: process.env.SMTP_PASSWORD,
        },
      },
      from: process.env.SMTP_FROM,
    }),
  ],
  adapter: Adapters.Prisma.Adapter({
    prisma,
  }),

  secret: process.env.SECRET,
};


Enter fullscreen mode Exit fullscreen mode

第四步:在前端实现身份验证

在应用程序中,您可以使用此功能next-auth检查访客是否拥有与有效会话对应的 cookie/令牌。如果找不到会话,则表示用户未登录。

使用next-auth,您有两种方法可以检查会话 - 可以在 React 组件中使用 hook 完成useSession(),也可以在后端(getServerSideProps或在 API 路由中)使用辅助函数完成getSession()

我们来看看它是如何运作的。

useSession()步骤 4.1:使用钩子检查用户会话

要使用这个钩子,你需要将组件包裹在一个next-auth 提供程序中。为了让身份验证流程在整个 Next.js 应用中的任何位置都能正常工作,请创建一个名为 `.js.js` 的新文件/pages/_app.tsx



import { Provider } from "next-auth/client";
import { AppProps } from "next/app";

const App = ({ Component, pageProps }: AppProps) => {
  return (
    <Provider session={pageProps.session}>
      <Component {...pageProps} />
    </Provider>
  );
};

export default App;


Enter fullscreen mode Exit fullscreen mode

现在,您可以前往/pages/index.tsx并导入模块useSession中的钩子next-auth/client。您还需要 ` signInand`signOut函数来实现身份验证交互。该signIn函数会将用户重定向到登录表单,该表单由自动生成next-auth



import { signIn, signOut, useSession } from "next-auth/client";


Enter fullscreen mode Exit fullscreen mode

useSession()钩子返回一个数组,其中第一个元素是用户会话,第二个元素是一个布尔值,表示加载状态。



// ...
const IndexPage = () => {
  const [session, loading] = useSession();

  if (loading) {
    return <div>Loading...</div>;
  }
};


Enter fullscreen mode Exit fullscreen mode

如果session对象为空null,则表示用户未登录。此外,我们可以从中获取用户信息session.user



// ...
if (session) {
  return (
    <div>
      Hello, {session.user.email ?? session.user.name} <br />
      <button onClick={() => signOut()}>Sign out</button>
    </div>
  );
} else {
  return (
    <div>
      You are not logged in! <br />
      <button onClick={() => signIn()}>Sign in</button>
    </div>
  );
}


Enter fullscreen mode Exit fullscreen mode

最终/pages/index.tsx文件应如下所示。



import { signIn, signOut, useSession } from "next-auth/client";

const IndexPage = () => {
  const [session, loading] = useSession();

  if (loading) {
    return <div>Loading...</div>;
  }

  if (session) {
    return (
      <div>
        Hello, {session.user.email ?? session.user.name} <br />
        <button onClick={() => signOut()}>Sign out</button>
      </div>
    );
  } else {
    return (
      <div>
        You are not logged in! <br />
        <button onClick={() => signIn()}>Sign in</button>
      </div>
    );
  }
};

export default IndexPage;


Enter fullscreen mode Exit fullscreen mode

现在,您可以使用以下命令启动 Next.js 开发服务器npm run dev,并体验身份验证流程!

getSession()步骤 4.2:在后端检查用户会话

要从后端代码获取用户会话,无论getServerSideProps()是在 API 请求处理程序中,都需要使用getSession()异步函数。

现在我们创建一个新/pages/api/secret.ts文件,如下所示。前端的原则同样适用于这里——如果用户没有有效的会话,则表示他们未登录,在这种情况下,我们将返回一个状态码为 403 的消息。



import { NextApiHandler } from "next";
import { getSession } from "next-auth/client";

const secretHandler: NextApiHandler = async (req, res) => {
  const session = await getSession({ req });
  if (session) {
    res.end(
      `Welcome to the VIP club, ${session.user.name || session.user.email}!`
    );
  } else {
    res.statusCode = 403;
    res.end("Hold on, you're not allowed in here!");
  }
};

export default secretHandler;


Enter fullscreen mode Exit fullscreen mode

localhost:3000/api/secret无需登录即可访问,您将看到类似下图的内容。

如果用户未登录,则返回 403 错误。
如果用户未登录,则返回 403 错误。

结论

就是这样,有了它,身份验证就容易多了next-auth

击掌 gif

希望您喜欢这篇教程,并学到了一些有用的东西!您可以在此 GitHub 仓库中找到初始代码和完整的项目。

此外,还可以查看Awesome Prisma 列表,获取更多 Prisma 生态系统中的教程和入门项目!

文章来源:https://dev.to/prisma/passwordless-authentication-with-next-js-prisma-and-next-auth-5g8g