Next.js 身份验证 - 使用 NextAuth.js 进行 JWT 刷新令牌轮换
最近,我正在一个 Next.js 应用中实现身份验证。在权衡了多个方案之后,我最终选择了 NextAuth.js,因为它是为 Next.js 量身定制的,并且支持多种身份验证服务提供商。
虽然只使用访问令牌,但身份验证流程实现起来相当简单。当我添加刷新令牌并尝试静默验证用户身份时,问题出现了。
在撰写本文时,尚无关于如何在 NextAuth.js 中实现令牌轮换的官方最佳实践。未来可能会有内置的 JWT 轮换解决方案,因此最好先查看文档。这里有一个教程,可能足以满足您的用例。
本简短教程是我对这个问题的理解,希望对大家有所帮助。我使用的是 Next.js 12.1.0、NextAuth.js 4.2.1 以及一个带有独立后端的凭证提供程序,该后端负责签发令牌。不过,文中提出的概念也适用于其他提供程序。
身份验证流程
当用户输入其凭证时,后端会验证它们并返回accessToken
、accessTokenExpiry
和refreshToken
。
- 它们
accessToken
的寿命应该相对较短,比如说 24 小时。 - 另一方面
refreshToken
, 应该是长期有效的,有效期比如说 30 天。它将用于获取新的accessTokens
。 - 是
accessTokenExpiry
令牌失效的时间戳。它也可以嵌入到令牌accessToken
本身中,稍后解码以获取到期时间戳。
我们可以设置refetchInterval
,定期向后端请求新令牌。该调用应在accessToken
过期之前进行,以确保用户保持身份验证状态。如果调用发生在accessToken
过期之后,只要 仍然有效,我们仍然有机会刷新令牌refreshToken
。但是,如果调用发生在refreshToken
过期之后,我们应该注销用户。
服务器端
我们创建一个文件,pages/api/auth/[…nextauth].js
用于存放令牌轮换的后端逻辑。文件中包含一个CredentialsProvider
,用于实现凭证身份验证。返回的对象将传递给jwt
回调函数。
jwt
回调是我们决定令牌是否可以刷新的地方。回调是我们用或session
指定客户端上可用内容的地方。useSession()
getSession()
关于刷新时间,我决定在令牌过期前为其设置一个刷新窗口。如果我们直接这样做token.accessTokenExpiry - Date.now() > 0
,要么会refetchInterval
调用会话刷新过早,导致下一个时间间隔内令牌无法刷新;要么会调用会话刷新过晚,导致用户在一段时间内无法获得有效的身份验证令牌。
[…nextauth].js
import axios from 'axios';
import NextAuth from 'next-auth';
import CredentialsProvider from "next-auth/providers/credentials";
async function refreshAccessToken(tokenObject) {
try {
// Get a new set of tokens with a refreshToken
const tokenResponse = await axios.post(YOUR_API_URL + 'auth/refreshToken', {
token: tokenObject.refreshToken
});
return {
...tokenObject,
accessToken: tokenResponse.data.accessToken,
accessTokenExpiry: tokenResponse.data.accessTokenExpiry,
refreshToken: tokenResponse.data.refreshToken
}
} catch (error) {
return {
...tokenObject,
error: "RefreshAccessTokenError",
}
}
}
const providers = [
CredentialsProvider({
name: 'Credentials',
authorize: async (credentials) => {
try {
// Authenticate user with credentials
const user = await axios.post(YOUR_API_URL + 'auth/login', {
password: credentials.password,
email: credentials.email
});
if (user.data.accessToken) {
return user.data;
}
return null;
} catch (e) {
throw new Error(e);
}
}
})
]
const callbacks = {
jwt: async ({ token, user }) => {
if (user) {
// This will only be executed at login. Each next invocation will skip this part.
token.accessToken = user.data.accessToken;
token.accessTokenExpiry = user.data.accessTokenExpiry;
token.refreshToken = user.data.refreshToken;
}
// If accessTokenExpiry is 24 hours, we have to refresh token before 24 hours pass.
const shouldRefreshTime = Math.round((token.accessTokenExpiry - 60 * 60 * 1000) - Date.now());
// If the token is still valid, just return it.
if (shouldRefreshTime > 0) {
return Promise.resolve(token);
}
// If the call arrives after 23 hours have passed, we allow to refresh the token.
token = refreshAccessToken(token);
return Promise.resolve(token);
},
session: async ({ session, token }) => {
// Here we pass accessToken to the client to be used in authentication with your API
session.accessToken = token.accessToken;
session.accessTokenExpiry = token.accessTokenExpiry;
session.error = token.error;
return Promise.resolve(session);
},
}
export const options = {
providers,
callbacks,
pages: {},
secret: 'your_secret'
}
const Auth = (req, res) => NextAuth(req, res, options)
export default Auth;
客户端
_app.js
我们用 包装我们的应用。然后<SessionProvider>
我们将 设置refetchInterval
为以秒为单位的特定值。这里的问题是,如果设置一个常量值,每次用户刷新页面时,计数器都会重新启动。因此,如果我们将 设置为refetchInterval
23 小时 30 分钟,当用户离开页面并在 12 小时后返回时,计数器会重新启动。因此,在Date.now()
+12 小时到Date.now()
+23 小时 30 分钟之间,我们得到的是无效令牌。
_app.js
import { SessionProvider } from 'next-auth/react';
import { useState } from 'react';
import RefreshTokenHandler from '../components/refreshTokenHandler';
function MyApp({ Component, pageProps }) {
const [interval, setInterval] = useState(0);
return (
<SessionProvider session={pageProps.session} refetchInterval={interval}>
<Component {...pageProps} />
<RefreshTokenHandler setInterval={setInterval} />
</SessionProvider>
)
}
export default MyApp;
为了解决这个问题,我创建了一个RefreshTokenHandler
组件,它必须放置在 内部,<SessionProvider>
以便我们能够访问useSession
钩子,从而获取访问令牌的到期时间。然后,我们计算距离到期的剩余时间,减去 30 分钟的余量。现在,每次用户刷新页面时,间隔都会设置为正确的剩余时间。
refreshTokenHandler.js
import { useSession } from "next-auth/react";
import { useEffect } from "react";
const RefreshTokenHandler = (props) => {
const { data: session } = useSession();
useEffect(() => {
if(!!session) {
// We did set the token to be ready to refresh after 23 hours, here we set interval of 23 hours 30 minutes.
const timeRemaining = Math.round((((session.accessTokenExpiry - 30 * 60 * 1000) - Date.now()) / 1000));
props.setInterval(timeRemaining > 0 ? timeRemaining : 0);
}
}, [session]);
return null;
}
export default RefreshTokenHandler;
令牌刷新现在应该可以正常工作了。我们可以创建一个钩子,当刷新令牌过期时,它会将用户注销。如果需要,我们可以进行重定向,并保存用户是否已通过身份验证的状态。
useAuth.js
import { signOut, useSession } from "next-auth/react";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
export default function useAuth(shouldRedirect) {
const { data: session } = useSession();
const router = useRouter();
const [isAuthenticated, setIsAuthenticated] = useState(false);
useEffect(() => {
if (session?.error === "RefreshAccessTokenError") {
signOut({ callbackUrl: '/login', redirect: shouldRedirect });
}
if (session === null) {
if (router.route !== '/login') {
router.replace('/login');
}
setIsAuthenticated(false);
} else if (session !== undefined) {
if (router.route === '/login') {
router.replace('/');
}
setIsAuthenticated(true);
}
}, [session]);
return isAuthenticated;
}
我们可以在我们的页面中使用这个钩子,如果用户未经身份验证则显示一条消息,或者让应用程序将用户重定向到登录页面。
const isAuthenticated = useAuth(true);
差不多就是这样了。现在我们可以用中的signIn()
和signOut()
方法测试这个机制了index.js
。至于我们在这个例子中设置的时间裕度,它们相当宽裕。这是因为我发现,在某些极端情况下,refetchInterval
可能会略微滞后。
index.js
import { signIn, signOut } from 'next-auth/react';
export default function Home() {
return (
<>
<button onClick={() => signIn('credentials', { email: 'example@example.com', password: 'example' })}>
Sign in
</button>
<button onClick={() => signOut()}>
Sign out
</button>
</>
)
}
包起来
我们介绍了一种使用 NextAuth.js 实现 JWT 令牌轮换的方法。感谢您的关注,如果您有更好的 NextAuth.js 中 JWT 令牌轮换解决方案,欢迎在下方留言或在Twitter上联系我。
鏂囩珷鏉ユ簮锛�https://dev.to/mabaranowski/nextjs-authentication-jwt-refresh-token-rotation-with-nextauthjs-5696