Next.js服务中利用刷新令牌逻辑处理过期访问令牌的方案
解决Next.js 15服务端API调用中过期访问令牌的自动刷新问题
问题背景
基于Next.js 15开发的项目中,后端独立处理用户认证:前端通过邮箱密码获取访问JWT和刷新JWT并存入Cookie;中间件全局处理受保护路由的令牌校验,过期则调用/refresh刷新令牌,失败跳转登录。但服务端API调用(如PersonalDataService.ts)在令牌过期时无法触发刷新逻辑,导致请求失败。
触发场景:
- 用户登录后打开受保护路由填写信息
- 填写过程中访问令牌过期,触发
savePersonalData调用 - 后端返回失败响应,未执行令牌刷新
解决方案
1. 封装可复用的令牌管理工具
创建app/lib/authUtils.ts,集中处理令牌有效性检查、刷新逻辑:
"use server"; import { cookies } from "next/headers"; import { jwtDecode } from "jwt-decode"; import { redirect } from "next/navigation"; // 校验访问令牌是否过期 const isTokenExpired = (token: string): boolean => { const decoded = jwtDecode<{ exp: number }>(token); return Date.now() >= decoded.exp * 1000; }; // 获取有效访问令牌,自动刷新过期令牌 export const getValidAccessToken = async (): Promise<string> => { const cookieStore = await cookies(); let accessToken = cookieStore.get("accessToken")?.value; const refreshToken = cookieStore.get("refreshToken")?.value; // 无访问令牌直接跳转登录 if (!accessToken) redirect("/login"); // 令牌未过期,直接返回 if (!isTokenExpired(accessToken)) return accessToken; // 无刷新令牌,跳转登录 if (!refreshToken) redirect("/login"); try { // 调用后端刷新接口 const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refreshToken }), }); if (!response.ok) { // 刷新失败,清除无效令牌并跳转登录 cookieStore.delete("accessToken"); cookieStore.delete("refreshToken"); redirect("/login"); } const { newAccessToken } = await response.json(); // 更新Cookie中的访问令牌 cookieStore.set("accessToken", newAccessToken, { httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "strict", maxAge: 15 * 60, // 与后端令牌有效期保持一致 }); return newAccessToken; } catch (error) { console.error("令牌刷新失败:", error); cookieStore.delete("accessToken"); cookieStore.delete("refreshToken"); redirect("/login"); } };
2. 修改服务端API调用逻辑
在PersonalDataService.ts中替换直接读取Cookie的逻辑,使用封装的工具函数获取有效令牌:
"use server"; import { cookies } from "next/headers"; import { jwtDecode } from "jwt-decode"; import { getValidAccessToken } from "@/app/lib/authUtils"; export const savePersonalData = async (firstName: string, lastName: string): Promise<boolean> => { try { // 获取经过校验/刷新的有效访问令牌 const accessToken = await getValidAccessToken(); const { sub } = jwtDecode<{ sub: string }>(accessToken); const response = await fetch( `${process.env.NEXT_PUBLIC_API_URL}/${process.env.NEXT_PUBLIC_API_USER}/${sub}`, { method: "PATCH", headers: { "Content-Type": "application/json", Authorization: `Bearer ${accessToken}`, }, body: JSON.stringify({ first_name: firstName.trim(), last_name: lastName.trim() }), } ); // 额外处理401情况,防止刷新后的令牌仍无效 if (response.status === 401) { (await cookies()).delete("accessToken"); (await cookies()).delete("refreshToken"); redirect("/login"); } return response.ok; } catch (error) { console.error("保存个人信息失败:", error); return false; } };
3. 关键优化点
- 统一逻辑:所有需要认证的服务端API调用都使用
getValidAccessToken,避免重复代码,确保全局一致的令牌处理 - 错误兜底:刷新失败或令牌无效时,直接清除Cookie并跳转登录,避免无效请求循环
- 性能提升:仅在令牌过期时触发刷新请求,减少不必要的后端调用;服务端本地校验令牌有效期,无需额外请求
- 与中间件互补:中间件处理路由进入前的认证,服务端API调用处理请求过程中的令牌过期,覆盖所有认证场景
内容的提问来源于stack exchange,提问作者Amr Gaafer
相关产品推荐
相关产品推荐

