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

如何在NextJS中对接自定义后端实现认证令牌刷新中间件逻辑

在Next.js中对接自定义后端实现认证令牌刷新机制

问题背景

需要在Next.js中实现基于Cookie的令牌刷新机制,现有思路包括:

  • 从Cookie中解码access token和refresh token
  • 当access token过期但refresh token有效时,刷新access token
  • 将解析后的用户信息存入currentUser Cookie(当前设置无效)

现有代码存在两个核心问题:中间件无法异步调用刷新接口、currentUser Cookie设置不生效。


解决方案

1. 让中间件支持异步操作

Next.js Middleware 完全支持异步函数,只需将 middleware 和校验函数改为 async 类型,即可使用 await 调用后端刷新接口。

2. 完善令牌刷新流程

调用后端刷新接口后,需将新的accessToken(及可能更新的refreshToken)重新写入Cookie,同时更新用户信息。

3. 修复Cookie设置无效问题

req.cookies是只读的请求对象属性,必须通过NextResponse对象设置响应Cookie,同时要指定Cookie的路径、安全属性等(根据你的后端认证配置调整)。


修改后的完整代码

import jwt from 'jsonwebtoken'
import { NextRequest, NextResponse } from 'next/server'
import authService from './app/lib/auth.service'

const protectedRoutes = ['/dashboard']
const publicRoutes = ['/login', '/register', '/']

// 改为async函数支持异步操作
export async function middleware(req: NextRequest) {
    const path = req.nextUrl.pathname
    const isProtectedRoute = protectedRoutes.includes(path)
    const isPublicRoute = publicRoutes.includes(path)

    // 调用异步校验函数
    const { isAuthorized, response } = await checkIfAuthorized(req)

    // 未授权访问受保护路由:重定向到登录页
    if (isProtectedRoute && !isAuthorized) {
        return NextResponse.redirect(new URL('/login', req.nextUrl))
    }

    // 已授权访问公开路由:重定向到仪表盘
    if (isPublicRoute && isAuthorized && path !== '/') {
        return NextResponse.redirect(new URL('/dashboard', req.nextUrl))
    }

    // 返回修改后的响应(携带新Cookie)
    return response
}

// 改为async函数,返回授权状态和处理后的响应
const checkIfAuthorized = async (req: NextRequest): Promise<{ isAuthorized: boolean; response: NextResponse }> => {
    const response = NextResponse.next()
    let accessToken = req.cookies.get('accessToken')?.value
    let decodedAccessToken: any = null

    // 无accessToken直接返回未授权
    if (!accessToken) {
        return { isAuthorized: false, response }
    }

    // 解码并校验accessToken过期时间
    decodedAccessToken = jwt.decode(accessToken) as any
    if (!decodedAccessToken) {
        return { isAuthorized: false, response }
    }

    const accessExpiredAt = new Date(decodedAccessToken.exp * 1000)
    const now = new Date()

    // accessToken过期,尝试刷新
    if (accessExpiredAt < now) {
        const refreshToken = req.cookies.get('refreshToken')?.value
        if (!refreshToken) {
            return { isAuthorized: false, response }
        }

        // 校验refreshToken是否过期
        const decodedRefreshToken = jwt.decode(refreshToken) as any
        const refreshExpiredAt = new Date(decodedRefreshToken.exp * 1000)
        if (refreshExpiredAt < now) {
            return { isAuthorized: false, response }
        }

        // 调用后端刷新接口,获取新的令牌
        try {
            const refreshResult = await authService.refresh(refreshToken)
            // 假设后端返回{ accessToken: string, refreshToken?: string }
            accessToken = refreshResult.accessToken
            
            // 更新accessToken Cookie
            response.cookies.set('accessToken', accessToken, {
                httpOnly: true,
                secure: process.env.NODE_ENV === 'production',
                sameSite: 'strict',
                path: '/',
                maxAge: 15 * 60 // 15分钟,和accessToken有效期一致
            })

            // 如果后端返回新的refreshToken,也更新
            if (refreshResult.refreshToken) {
                response.cookies.set('refreshToken', refreshResult.refreshToken, {
                    httpOnly: true,
                    secure: process.env.NODE_ENV === 'production',
                    sameSite: 'strict',
                    path: '/',
                    maxAge: 7 * 24 * 60 * 60 // 7天,和refreshToken有效期一致
                })
            }

            // 解码新的accessToken获取用户信息
            decodedAccessToken = jwt.decode(accessToken) as any
        } catch (err) {
            // 刷新失败,返回未授权
            return { isAuthorized: false, response }
        }
    }

    // 设置currentUser Cookie(如果不存在)
    if (!req.cookies.get('currentUser')?.value && decodedAccessToken) {
        response.cookies.set('currentUser', JSON.stringify(decodedAccessToken), {
            secure: process.env.NODE_ENV === 'production',
            sameSite: 'strict',
            path: '/',
            maxAge: 15 * 60 // 和accessToken有效期一致
        })
    }

    return { isAuthorized: true, response }
}

// 中间件忽略静态资源和API路由
export const config = {
    matcher: ['/((?!api|_next/static|_next/image|.*\\.png$).*)'],
}

关键说明

  • 异步支持:将middleware和checkIfAuthorized改为async函数,解决了无法调用异步刷新接口的问题。
  • Cookie设置:通过response.cookies.set()设置响应Cookie,指定了httpOnly(仅后端可读取,提升安全性)、secure(生产环境下仅HTTPS传输)、sameSite(防止CSRF)等必要属性。
  • 错误处理:刷新令牌时加入try/catch,处理接口调用失败的情况,避免中间件崩溃。
  • 响应返回:始终返回处理后的response对象,确保新设置的Cookie能被客户端接收。

内容的提问来源于stack exchange,提问作者phoebus

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.25 03:18:18