You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Next.js 14(App Router)Axios拦截器401时JWT存Cookie问题

解决Next.js 14中Axios拦截器刷新令牌后无法设置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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.19 03:24:50