我有一个使用外部 Oauth API 的 NextJS 14 应用程序。成功登录后,我使用访问和刷新令牌设置了一些 cookie,这样我就可以在调用后端(Spring)时使用这些令牌。
当我想从中间件刷新访问令牌时遇到问题,我无法设置 cookie,因为出现以下错误:
[错误]:Cookie 只能在服务器操作或路由处理程序中修改。了解更多:https://nextjs.org/docs/app/api-reference/functions/cookies#cookiessetname-value-options
如果我尝试在中间件中覆盖 reqquest.cookies 或 response.cookies ,那么调用 cookies().get() 时获得的 cookie 似乎具有旧值。
如果我设置服务器操作来更新 cookie 并从中间件导入它,我会收到相同的错误。
我需要能够从一个集中的地方更新令牌,这样我就不必检查过期的令牌并更新它们,以防它们在每次调用后端时都过期了。
我没有任何路由处理程序,因为我直接从客户端组件调用后端。
在 /src/lib/auth.ts 中我有我的凭据提供者:
import { cookies } from "next/headers";
[... ommitted irrelevant parts ...]
export const authOptions: NextAuthOptions = {
session: {
strategy: "jwt",
},
providers: [
CredentialsProvider({
name: "credentials",
credentials: {
username: { label: "Username", type: "text" },
password: { label: "Password", type: "password" },
},
async authorize(credentials) {
const { username, password } = credentials as {
username: string;
password: string;
};
const formData = new URLSearchParams();
formData.append("email", encodeURIComponent(username));
formData.append("password", encodeURIComponent(password));
const res = await fetch(loginPath, {
method: "POST",
headers: {
"Content-Type": "application/x-www-form-urlencoded",
},
body: formData.toString(),
});
const receivedToken = await res.json();
console.log("Received token: " + receivedToken);
if (res.ok && receivedToken) {
const user = receivedToken;
const parsed = parsePayload(receivedToken.access_token);
user.name = parsed.name;
cookies().set({
name: "gat",
value: receivedToken.access_token,
httpOnly: true,
});
cookies().set({
name: "grt",
value: receivedToken.refresh_token,
httpOnly: true,
});
cookies().set({
name: "exp",
value: parsed.exp,
httpOnly: true,
});
return user;
} else {
return null;
}
},
}),
],
callbacks: {
jwt: async ({ token, user }: { token: any; user: any }) => {
if (user) token = user as unknown as { [key: string]: any };
return token;
},
session: async ({ session, token }: { session: any; token: any }) => {
session.user = { ...token };
return session;
},
},
pages: {
signIn: "/login",
},
};
我还尝试检查令牌是否过期并在此处更新会话回调中的cookie,但我得到了相同的错误。
我尝试按如下方式覆盖中间件中的cookie,但正如我之前所说,当我稍后使用 cookies().get() 访问这些值时,我得到的是旧值,而不是新值:
export async function middleware(req) {
[... ommitted irrelevant things ...]
let expTime = req.cookies.get("exp")?.value;
let grtValue = req.cookies.get("grt")?.value;
if (grtValue && expTime && new Date(+expTime * 1000) < Date.now()) {
let response = NextResponse.next();
updateToken(req, response, grtValue);
return response;
}
}
export async function updateToken(req, resp, grtValue) {
try {
const res = await fetch(refreshPath, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(new Refresh(grtValue)),
});
const receivedToken = await res.json();
console.log("Refreshed access token: " + receivedToken);
if (res.ok && receivedToken) {
let parsedPayload = parsePayload(receivedToken.access_token);
req.cookies.set({
name: "gat",
value: receivedToken.access_token,
httpOnly: true,
});
resp.cookies.set({
name: "gat",
value: receivedToken.access_token,
httpOnly: true,
});
req.cookies.set({
name: "grt",
value: receivedToken.refresh_token,
httpOnly: true,
});
resp.cookies.set({
name: "grt",
value: receivedToken.refresh_token,
httpOnly: true,
});
req.cookies.set({
name: "exp",
value: parsedPayload.exp,
httpOnly: true,
});
resp.cookies.set({
name: "exp",
value: parsedPayload.exp,
httpOnly: true,
});
} else {
console.log("Error while refreshing token " + res.statusText);
}
} catch (error) {
console.log(error);
}
}
您的主要挑战是在成功登录 oAuth 后更新存储在 cookie 中的访问和刷新令牌,特别是当这些令牌过期时。出于安全和架构原因,Next.js 将 cookie 修改限制在特定区域,这就是您在尝试在服务器操作或路由处理程序之外更新 cookie 时遇到错误的原因。
为了解决这个问题,您需要一种集中的方式来管理令牌刷新而不违反 Next.js 的限制。该策略涉及使用专用于刷新令牌和更新 cookie 的 API 路由,您可以根据需要从客户端或中间件调用。
创建用于刷新令牌的 API 路由:此路由将处理与 OAuth 提供商的通信以刷新令牌,并且能够设置 cookie。
在pages/api目录下创建一个文件,例如refresh-token.js。
在此文件中,编写调用 OAuth API 的逻辑,以使用刷新令牌刷新访问令牌。
客户端Token刷新逻辑: 在调用后端之前,请检查访问令牌是否需要刷新。如果是这样,请调用您刚刚创建的 /api/refresh-token 端点来刷新令牌。
如果您使用 Axios 进行 HTTP 请求,这可以作为实用函数或使用拦截器来实现。
中间件调整:由于直接在中间件中修改 cookie 面临限制,因此:检测令牌是否需要刷新 中间件。 不要尝试直接在中间件中刷新令牌,而是将请求重定向到 API 路由 (/api/refresh-token) 或返回指示客户端应刷新令牌的响应。 集中式令牌管理:通过为令牌刷新提供专用 API 路由,您可以集中此逻辑,从而更轻松地管理整个应用程序。这种方法尊重 Next.js 的架构限制,同时为令牌管理提供无缝体验。
// pages/api/refresh-token.js
export default async function handler(req, res) {
// Logic to refresh token and handle errors
// Use res.setHeader('Set-Cookie', ...) to set new tokens
}
if (tokenNeedsRefresh) {
// Redirect to refresh-token API route
}