随着边缘运行时越来越受欢迎,人们自然会尝试将 Auth.js 和 next-auth
部署到这些环境中,并遇到一些目前困扰整个生态系统的基本兼容性问题。我们希望通过本文档,无论您目前对理解和经验处于什么阶段,我们都能帮助您理解挑战,并希望您能在您选择的任何运行时环境中成功运行 Auth.js!
首先,让我们消除一些背景知识。如果您熟悉这些,可以跳过本节!
定义
我们将专门讨论 Auth.js 以及它如何与如今在各种框架、托管提供商、库等中非常流行的 边缘运行时 相交。
首先,“边缘” 在此上下文中是什么?此处的边缘借鉴了网络工程人员,指的是位于网络边缘(即更靠近用户)的计算节点(即服务器)。通常,这些计算节点的功耗低于可以在数据中心核心运行大多数重要工作负载的全功能服务器。在边缘运行代码的一些优势包括更低的延迟到用户的终端设备、更好的可扩展性故事以及更具成本效益的计算。一些缺点包括更弱的硬件以及软件堆栈方面的潜在兼容性差异。
因此,当我们说 边缘运行时 时,我们的意思是 不是 Node.js 的服务器端 JavaScript 运行时,它针对这些边缘计算节点(服务器)进行了优化。这通常意味着代码更靠近您的用户在针对其他方面(如快速启动时间、低内存使用量等)进行了优化的低功耗硬件上执行。
这是一个问题,因为这些运行时通常缺少 Node.js 具有的功能,有时这些功能对您依赖的库和包的功能至关重要。当一个包说它是“边缘兼容的”或“边缘就绪的”时,他们的真实意思是他们设计了自己的软件以避免一些边缘运行时中缺失的 Node.js 功能/模块,从而使它们具有更广泛的兼容性。查看 unjs 的 兼容性矩阵 以了解哪些运行时支持哪些功能。虽然这对 Auth.js 并不重要,但现在是时候提及有一个行业组织旨在为 JavaScript 运行时提供一个空间来协作 API 互操作性 - WinterCG。
我想在此指出,这些功能/模块通常缺失是因为它们运行的底层环境没有提供它们。例如,开发人员可以投入尽可能多的时间,但如果他们的服务器端 JavaScript 运行时将在不提供他们访问文件系统的沙盒操作系统环境中运行,那么无论他们如何努力,他们都无法实现 fs
模块。
由于这种 Node.js 与其他运行时的情况目前非常分散且不稳定,许多库都在优化其工作负载以仅使用最常见的特征,例如 fetch
。例如,如果您是数据库提供商,并且您可以设计您的系统,以便您的客户端库只需要向您的后端发出 HTTP 请求即可进行通信,那么您可以将您的库宣传为“边缘兼容的”并在用户可能想要使用的任何地方运行。这与其他数据库客户端库形成对比,例如,其他数据库客户端库必须使用 Node.js 的原始 TCP 套接字与他们的后端通信。
Auth.js
Auth.js 针对边缘兼容性进行了优化。这意味着您可以在您选择的任何 JavaScript 运行时环境中运行 Auth.js 的核心功能。然而,这里的关键词是 核心功能。如果您 只 使用 Auth.js / next-auth
以及在您的 Auth.js 回调、中间件等中没有使用其他库,那么您可以在任何地方使用它!
当您想将其他库与 Auth.js 一起使用时,问题就出现了。
问题
数据库适配器
与 Auth.js 配合使用以实现完整身份验证系统的常用包是数据库客户端。数据库客户端很麻烦,因为它们通常利用 TCP 套接字直接与数据库服务器通信。其中一个常见的数据库就是 PostgreSQL。
PostgreSQL 是一个数据库,它使用基于消息的协议在客户端和服务器之间进行通信,该协议通过 TCP(或 Unix)套接字 传输。原始 TCP 套接字是那些通常无法用于边缘运行时的 Node.js 功能之一。因此,从表面上看,似乎无法从在边缘运行时运行的 JavaScript 与 PostgreSQL 数据库进行通信。许多其他数据库及其各自的通信协议也是如此。
然而,随着边缘运行时变得更加成熟和流行,人们变得更有创意,并实施了各种解决方案来解决这个问题。其中一个常见的解决方案是在数据库前面放置某种 API 服务器,其目标是将通过 HTTP 发送给它的数据库查询转换为数据库可以理解的协议。这允许客户端仅向 API 服务器发出 HTTP 请求,这是每个边缘运行时都支持的操作。
中间件
在 Next.js 和 next-auth
中,您还可以使用 Next.js 的 中间件 通过检查是否存在会话并决定下一步路由的位置来保护路由。默认情况下,在 Vercel 和其他托管提供商上,中间件代码始终在边缘运行时中运行。这意味着我们的代码将尝试执行,例如,在没有提供底层功能(即 TCP 套接字)的环境中执行 PostgreSQL 查询。因此,要使用不是明确“边缘兼容的”的数据库适配器,我们需要找到一种使用我们可用的功能来查询数据库的方法。
解决方案
Auth.js 与 数据库会话策略 和数据库适配器一起使用,在正常操作期间会多次调用数据库。无论您使用哪个框架,每个 Auth.js 客户端都可以获取当前活动会话,这通过查询数据库以检查用户的 sessionToken
是否同时在数据库中且有效(即未过期)来完成。
这意味着在您可能希望检查用户是否已通过身份验证的应用程序的每个地方都需要进行数据库调用。现在在现实生活中,Auth.js 在这方面更智能,并使用缓存和其他技巧来避免不必要的数据库请求,但您可以想象每个 auth()
调用都将触发一个数据库查询。因此,我们需要某种变通方法来在边缘运行时中将 Auth.js 与许多数据库适配器一起使用!
拆分配置
考虑到 Next.js 和 next-auth
,让我们考虑一下我们需要做什么才能使 Auth.js 能够同时在边缘运行时环境中运行部分代码,并使用数据库来存储其会话。我们需要一个没有数据库设置的 next-auth
的独立“版本”供边缘环境使用,另一个带有数据库供其他地方使用。为了实现这一点,我们可以使用 Auth.js 的 “延迟初始化” 功能来实例化一个没有适配器的独立客户端,供中间件使用,另一个供其他地方使用。
- 首先,一个供所有地方使用的通用 Auth.js 配置对象。这 不会 包括数据库适配器。
import GitHub from "next-auth/providers/github"
import type { NextAuthConfig } from "next-auth"
// Notice this is only an object, not a full Auth.js instance
export default {
providers: [GitHub],
} satisfies NextAuthConfig
- 接下来,一个单独的**实例化**的 Auth.js 实例,它导入该配置,但也添加了适配器并使用
jwt
作为 Session 策略。
import NextAuth from "next-auth"
import authConfig from "./auth.config"
import { PrismaClient } from "@prisma/client"
import { PrismaAdapter } from "@auth/prisma-adapter"
const prisma = new PrismaClient()
export const { handlers, auth, signIn, signOut } = NextAuth({
adapter: PrismaAdapter(prisma),
session: { strategy: "jwt" },
...authConfig,
})
- 我们的中间件,它将导入配置**不带数据库适配器**,并实例化自己的 Auth.js 客户端。
import NextAuth from "next-auth"
import authConfig from "./auth.config"
export const { auth: middleware } = NextAuth(authConfig)
- 最后,在其他任何地方,我们可以从主
auth.ts
配置中导入,并像往常一样使用next-auth
。有关更多示例,请参阅我们的会话管理文档。
import { auth } from "@/auth"
export default async function Page() {
const session = await auth()
if (!session) {
return <div>Not authenticated</div>
}
return (
<div className="container">
<pre>{JSON.stringify(session, null, 2)}</pre>
</div>
)
}
这里需要注意的是,我们现在已从next-auth
**在中间件中**删除了数据库功能和支持。这意味着我们无法在中间件中执行代码时获取会话或其他信息,例如用户的帐户详细信息等。这意味着您需要依赖类似于上面在/app/protected/page.tsx
文件中演示的检查,以确保您有效地保护您的路由。例如,中间件仍然用于延长会话 cookie 的过期时间。