NextJS - 使用 next-auth 配置 JWT token
Nextjs 中有很多身份验证选项,例如 Supabase、Firebase、Userbase 等等。 我们将重点关注 NextAuth.js 以及通过凭证提供程序在现有 Django 后端和 Next.js 之间实现 JWT 会话的打字稿。 我们将尽力专注于我们的用例以节省时间,因此我们将省略所有未使用的选项和功能。
为什么选择 NextAuth.js
这个全栈库可以帮助您与每个主要的 OAuth 提供商集成,也可以仅与电子邮件身份验证集成(推荐这样做,因为安全性更高)。 如果需要,您还可以创建自定义 OAuth 或凭据提供程序。 这就是我们喜欢这个库的原因,它为您提供了通用流程和默认处理程序,但您可以轻松重写每个步骤以满足您的需求。
最简配置
通过命令 yarn add next-auth
安装软件包后,或者如果您更喜欢 npm install next-auth
,则必须创建配置 [...nextauth].ts
文件,该文件将位于 API 路由 /api/auth/[... nextauth].ts
。
这意味着所有到达 /api/auth/*
的请求都将由 NextAuth.js 处理。 在此文件中,我们将导出处理程序函数,其中将包含我们的配置。 您可以在此处找到有关配置的更多详细信息。
// pages/api/auth/[...nextauth].ts
export default async function auth(
req: NextApiRequest,
res: NextApiResponse
) {
return await NextAuth(req, res, {
providers: [ ... ],
session: {
strategy: "jwt",
},
cookies: cookies,
callbacks: { ... },
});
}
一般情况下,当您想要使用 JWT 会话时,必须将 session.strategy
设置为 jwt
并指定用于加密令牌的密钥。
我们建议您通过环境变量 NEXTAUTH_SECRET
设置秘密。 此外,如果您不部署到 Vercel,则需要将站点的规范 URL 作为 NEXTAUTH_URL。
在我们的示例中,我们将使用 session-token
、callback-url
和 csrf-token
。 它们分别用于存储 JWT 令牌、登录/退出后将重定向的默认回调以及最后的 CSRF 令牌。
要使我们的令牌在所有子域中可用,您必须将 cookie 的域选项设置为有效域,例如 如果您的域是 account.example.com 和 example.com,则必须将域选项设置为 example.com。
// pages/api/auth/[...nextauth].ts
const cookies: Partial<CookiesOptions> = {
sessionToken: {
name: `next-auth.session-token`,
options: {
httpOnly: true,
sameSite: "none",
path: "/",
domain: process.env.NEXT_PUBLIC_DOMAIN,
secure: true,
},
},
callbackUrl: {
name: `next-auth.callback-url`,
options: {
...
},
},
csrfToken: {
name: "next-auth.csrf-token",
options: {
...
},
},
};
类型安全
NextAuth.js 提供内置类型,但由于我们需要在会话对象中存储更多信息,因此我们必须重写 Session
、User
和 JWT
对象的类型。
在文档中阅读有关 cookie 及其选项的更多信息。
https://next-auth.js.org/configuration/options#cookies
// types/next-auth.d.ts
declare module "next-auth" {
/**
* Returned by `useSession`, `getSession` and received as
* a prop on the `SessionProvider` React Context
*/
interface Session {
refreshTokenExpires?: number;
accessTokenExpires?: string;
refreshToken?: string;
token?: string;
error?: string;
user?: User;
}
interface User {
firstName?: string;
lastName?: string;
email?: string | null;
id?: string;
contactAddress?: {
id?: string;
};
}
}
declare module "next-auth/jwt" {
/** Returned by the `jwt` callback and `getToken`, when using JWT sessions */
interface JWT {
refreshTokenExpires?: number;
accessTokenExpires?: number;
refreshToken?: string;
token: string;
exp?: number;
iat?: number;
jti?: string;
}
}
认证流程
凭证提供者(Credentials provider)
我们终于可以将凭据提供程序添加到我们的配置中。 需要提供者的name
和id
来区分不同的提供者。 credentials
对象表示登录表单的字段,该对象结构将作为authorize
函数的第一个参数传递。
// pages/api/auth/[...nextauth].ts
providers: [
Providers.Credentials({
name: 'credentials',
id: 'credentials',
credentials: {
username: { },
password: { }
},
async authorize(credentials, req) {
// ...
}
})
],
authorize
函数负责从自定义后端实现中获取用户,该实现应该为我们提供 JWT 令牌。
// pages/api/auth/[...nextauth].ts
async authorize(credentials) {
const response = await fetch('...', {
...
variables: {
email: credentials?.email,
password: credentials?.password,
},
});
const data = await response.json();
if (response.ok && data?.token) {
return data;
}
return Promise.reject(new Error(data?.errors));
};
回调 (Callbacks)
我们流程中的第一个回调是 jwt
。 每当客户端创建或访问 JSON Web Token 时都会调用此回调。 这是实施代币轮换的正确位置。 freshAccessToken
函数的目的是使用存储在 token
对象中的刷新令牌,并使用它来获取具有更新的过期时间的新访问令牌。 请注意,我们的后端为我们提供了以秒为单位的过期时间,而 Date.now()
的输出以毫秒为单位,这就是为什么我们需要将其除以 1000。
// pages/api/auth/[...nextauth].ts
export const jwt = async ({ token, user }: { token: JWT; user?: User }) => {
// first call of jwt function just user object is provided
if (user?.email) {
return { ...token, ...user };
}
// on subsequent calls, token is provided and we need to check if it's expired
if (token?.accessTokenExpires) {
if (Date.now() / 1000 < token?.accessTokenExpires) return { ...token, ...user };
} else if (token?.refreshToken) return refreshAccessToken(token);
return { ...token, ...user };
};
jwt
的输出作为 token
传递到 session
回调中。 这是将附加数据传递到 session
对象的好地方。
在我们的例子中,我们将解析后端在 token
内向我们提供的所有数据,并将其作为 Web 客户端的 user
对象传递。 此外,我们还可以检查访问令牌和刷新令牌是否已过期并引发错误。
// pages/api/auth/[...nextauth].ts
export const session = ({ session, token }: { session: Session; token: JWT })
: Promise<Session> => {
if (Date.now() / 1000 > token?.accessTokenExpires &&
token?.refreshTokenExpires && Date.now() / 1000 > token?.refreshTokenExpires) {
return Promise.reject({
error: new Error("Refresh token has expired. Please log in again to get a new refresh token."),
});
}
const accessTokenData = JSON.parse(atob(token.token.split(".")?.at(1)));
session.user = accessTokenData;
token.accessTokenExpires = accessTokenData.exp;
session.token = token?.token;
return Promise.resolve(session);
};
这是我们的身份验证流程的可视化。
客户端使用
正如我们之前提到的,我们使用自定义登录页面,以便在客户端验证后我们可以调用 signIn
。 将重定向设置为 false
后,我们可以手动处理错误和成功。
// pages/login.tsx
signIn("credentials", {
username: data?.username,
password: data?.password,
redirect: false,
}).then((response) => {
if (response?.error) {
// show notification for user
} else {
// redirect to destination page
}
});
如果我们不需要验证响应,我们可以将重定向设置为 true
并提供回调 URL,以便在用户注销后他将被重定向到指定页面。
// pages/logout.tsx
signOut({
redirect: true,
callbackUrl: `${process?.env.NEXT_PUBLIC_LOGIN_PAGE}/login`,
});
当您需要访问 session
数据或访问客户端中的 token
令牌时,可以使用 useSession() 钩子。 在我们的例子中,我们将使用自定义属性获取session
类型。
中间件 (Middleware)
如果您使用 Next.js 12 或更高版本,则可以在中间件 middleware
中使用 NextAuth.js。 在基本用法中,我们只需导出一个 matcher
对象,其中包含我们想要保护的路径名数组。
// middleware.ts
export { default } from "next-auth/middleware"
export const config = { matcher: [ ... ] }
如果您需要一些高级逻辑,您可以使用自定义 middleware
中间件实现。 我们可以通过 getToken()
访问和解码中间件中的令牌数据。
例如,如果用户没有管理员访问权限,我们可以重定向用户。
// middleware.ts
export async function middleware(request: NextRequest) {
const token = await getToken({
req: request,
secret: process?.env?.NEXTAUTH_SECRET,
cookieName: ACCESS_TOKEN, // next-auth.session-token
});
// redirect user without access to login
if (token?.token && Date.now() / 1000 < token?.accessTokenExpires) {
return NextResponse.redirect("/login");
}
// redirect user without admin access to login
if (!token?.isAdmin) {
return NextResponse.redirect("/login");
}
return NextResponse.next();
}
子域名设置
当您只有一个域名时,到目前为止的所有内容都对您有效。 恭喜你,你已经完成了。 其余的已成功完成负责身份验证的域的实施。 在撰写本文时,NextAuth.js 文档尚未提供官方解决方案。 我们可以发现这里只是我们需要设置一个自定义的cookie策略。
我们需要在所有子域之间共享除 authorize
授权功能之外的所有内容。 这是有道理的,因为我们永远不会从子域调用 signIn
。
// pages/api/auth/[...nextauth].ts
export default NextAuth({
providers: [
CredentialsProvider({
name: "id",
name: "credentials",
credentials: {},
async authorize(_credentials, _req) {
return null;
},
}),
],
session: {
strategy: "jwt",
},
cookies: cookies,
callbacks: {
session,
jwt,
},
});
因此,我们需要共享 cookie
、session
和 jwt
回调的设置,以同样地访问和刷新令牌。 这就是为什么我们建议您将这些功能移至共享模块中,以便您可以从两个站点访问它们。 不要忘记使用相同的 secret
,因为 token
令牌的解密将会失败。