Next.js 14(App Router)Axios拦截器401时JWT存Cookie问题
当下端返回401状态码时,若存在refresh token,需要获取新的JWT access token并重发原请求。但在Next.js 14中,设置Cookie必须通过Server Action或路由处理,而令牌刷新逻辑在客户端Axios拦截器里,导致刷新后的access token无法存入Cookie,同一页面多次请求会重复触发令牌刷新,体验很差。
下面是几个可行的解决方案,按需选择:
方案1:改造Server Action,让它同时完成令牌刷新与Cookie设置
直接修改现有的refreshTokens Server Action,在获取到新令牌后直接设置Cookie,客户端调用这个Server Action时,Cookie会自动同步到浏览器。
修改refreshTokens.ts
'use server' import LoginDto from '@/models/dto/auth/LoginDto' import axiosInstance from '@/utils/axiosInstance' import axios, { AxiosError } from 'axios' import { cookies } from 'next/headers' import { cookieOptions } from '@/configs/auth.config' export interface RefreshTokenState { status: 200 | 400 | 401 | 500 message: string tokens?: LoginDto } export default async function refreshTokens(): Promise<RefreshTokenState> { try { const res = await axiosInstance.post<LoginDto>('/auth/refresh-token') if (res.status === 200) { const { access_token, refresh_token } = res.data // 在Server Action中直接设置Cookie const cookieStore = cookies() cookieStore.set('token', access_token, cookieOptions('access')) if (refresh_token) { cookieStore.set('refresh_token', refresh_token, cookieOptions('refresh')) } return { status: 200, message: 'Token refreshed successfully.', tokens: res.data, } } return { status: 500, message: 'There was a problem refreshing your token.', } } catch (error) { if (axios.isAxiosError(error)) { const axiosError = error as AxiosError if (axiosError.response) { const status = axiosError.response.status if (status === 401) { return { status: 401, message: 'Token is invalid.', } } if (status === 400) { return { status: 400, message: 'Refresh token was not present.', } } } } return { status: 500, message: 'There was a problem refreshing your token.', } } }
调整Axios拦截器(axiosInstance.ts)
不需要手动处理Cookie,调用Server Action后Cookie会自动更新,只需要更新请求头的Authorization即可:
// ... 其他代码不变 try { const res = await refreshToken() if (res.status !== 200 || !res.tokens) { return Promise.reject(error) } originalRequest.headers['Authorization'] = `Bearer ${res.tokens?.access_token}` return axiosInstance(originalRequest) } catch (refreshError) { console.log(refreshError) return Promise.reject(refreshError) } // ... 其他代码不变
方案2:创建API路由处理令牌刷新,通过响应头设置Cookie
如果不想用Server Action,可以新建一个本地API路由,在路由里处理令牌刷新并通过响应头设置Cookie,客户端拦截器调用这个路由即可。
新建/app/api/auth/refresh/route.ts
import { NextRequest, NextResponse } from 'next/server' import axiosInstance from '@/utils/axiosInstance' import { cookieOptions } from '@/configs/auth.config' import axios from 'axios' export async function POST(request: NextRequest) { try { const res = await axiosInstance.post('/auth/refresh-token') const { access_token, refresh_token } = res.data const response = NextResponse.json({ status: 200, message: 'Token refreshed successfully', tokens: res.data }) // 通过响应头设置Cookie response.cookies.set('token', access_token, cookieOptions('access')) if (refresh_token) { response.cookies.set('refresh_token', refresh_token, cookieOptions('refresh')) } return response } catch (error) { if (axios.isAxiosError(error)) { const status = error.response?.status || 500 return NextResponse.json({ status, message: error.response?.data?.message || 'Failed to refresh token' }, { status }) } return NextResponse.json({ status: 500, message: 'Failed to refresh token' }, { status: 500 }) } }
修改Axios拦截器(axiosInstance.ts)
把调用Server Action改成调用这个本地API路由:
// ... 其他代码不变 try { // 调用本地API路由 const res = await axios.post('/api/auth/refresh') if (res.data.status !== 200 || !res.data.tokens) { return Promise.reject(error) } originalRequest.headers['Authorization'] = `Bearer ${res.data.tokens?.access_token}` return axiosInstance(originalRequest) } catch (refreshError) { console.log(refreshError) return Promise.reject(refreshError) } // ... 其他代码不变
方案3:客户端缓存令牌+定时刷新(优化体验)
为避免短时间内重复触发刷新,可以在客户端用React Context缓存access token,拦截器优先使用缓存值,同时定时调用Server Action刷新令牌并同步Cookie和缓存。
示例:创建Auth Context
'use client' import { createContext, useContext, useState, useEffect } from 'react' import refreshTokens from '@/api/auth/refreshTokens' interface AuthContextType { accessToken: string | null refreshAccessToken: () => Promise<void> } const AuthContext = createContext<AuthContextType | undefined>(undefined) export function AuthProvider({ children }: { children: React.ReactNode }) { const [accessToken, setAccessToken] = useState<string | null>(null) // 初始化从Cookie读取令牌 useEffect(() => { const token = document.cookie.split('; ').find(row => row.startsWith('token='))?.split('=')[1] setAccessToken(token || null) }, []) const refreshAccessToken = async () => { const res = await refreshTokens() if (res.status === 200 && res.tokens) { setAccessToken(res.tokens.access_token) // Cookie由Server Action自动更新,这里只同步客户端缓存 } } // 提前5分钟刷新令牌,避免过期 useEffect(() => { if (!accessToken) return const payload = JSON.parse(atob(accessToken.split('.')[1])) const expiresIn = payload.exp * 1000 - Date.now() - 5 * 60 * 1000 const timer = setTimeout(refreshAccessToken, expiresIn) return () => clearTimeout(timer) }, [accessToken]) return ( <AuthContext.Provider value={{ accessToken, refreshAccessToken }}> {children} </AuthContext.Provider> ) } export const useAuth = () => { const context = useContext(AuthContext) if (!context) { throw new Error('useAuth must be used within an AuthProvider') } return context }
调整Axios拦截器(axiosInstance.ts)
确保文件标记为客户端模块,优先使用Context中的缓存令牌:
'use client' import { useAuth } from '@/contexts/AuthContext' import axios, { AxiosError } from 'axios' import { cookies } from 'next/headers' const axiosInstance = axios.create({ baseURL: `${process.env.NEXT_PUBLIC_BACKEND_URL}/api/v1`, }) // 请求拦截器优先使用客户端缓存的令牌 axiosInstance.interceptors.request.use(config => { const { accessToken } = useAuth() const cookieStore = cookies() const accessCookie = cookieStore.get('token') if (accessToken) { config.headers.Authorization = `Bearer ${accessToken}` } else if (accessCookie) { config.headers.Authorization = `Bearer ${accessCookie.value}` } // 处理refresh-token请求的逻辑不变 if (config.url?.startsWith('/auth/refresh-token')) { const refreshCookie = cookieStore.get('refresh_token') if (refreshCookie) { config.headers.Authorization = `Bearer ${refreshCookie.value}` } } return config }) // 响应拦截器逻辑不变 axiosInstance.interceptors.response.use( response => response, async (error: AxiosError) => { // ... 原有逻辑 } ) export default axiosInstance
内容的提问来源于stack exchange,提问作者Fran Zlatar
相关产品推荐
相关产品推荐

