Next.js 14中设置Cookie实现认证授权的疑难问题
我太懂你这种折腾一周的崩溃感了!Next.js 14的App Router在处理Cookie和认证时,确实因为服务器组件的无状态、可缓存特性,给我们挖了不少坑。让我一步步帮你梳理问题,找到可行的解决方案。
首先,先明确你的核心痛点:当Access Token过期时,需要用Refresh Token去获取新的Access Token并设置Cookie,但服务器组件里直接操作Cookie会报错,试了Server Action和路由处理器也没成功。下面针对这些问题逐一解决:
一、为什么你的Server Action会报错?
你在服务器组件的fetchUser函数里直接调用了getAccessToken这个Server Action,这就违反了Next.js的规则:服务器组件的渲染流程中不能执行有副作用的操作(比如设置Cookie)。因为服务器组件是用来生成静态或动态HTML的,渲染过程必须是纯函数式的,不能修改状态或Cookie,否则会破坏缓存机制。
正确的Server Action使用方式
如果一定要用Server Action来刷新Token,你需要把触发逻辑放在客户端组件里,或者通过用户交互(比如按钮点击)来触发。如果是页面加载时自动刷新Token,可以用客户端组件的useEffect来调用Server Action:
- 写一个客户端组件(比如
AuthInitializer.tsx):
'use client'; import { useEffect } from 'react'; import { getAccessToken } from './action'; export default function AuthInitializer() { useEffect(() => { // 页面加载时调用Server Action刷新Token const refreshToken = async () => { // 从客户端Cookie中提取Refresh Token const refreshToken = document.cookie.split('; ') .find(row => row.startsWith('refresh-token='))?.split('=')[1]; if (refreshToken) { // 调用接口获取新的Access Token const tokenRes = await fetch('/api/access-token', { method: 'POST', credentials: 'include', // 确保携带Cookie body: JSON.stringify({ refreshToken }) }); if (tokenRes.ok) { const { accessToken } = await tokenRes.json(); // 调用Server Action设置新的Access Token Cookie await getAccessToken(accessToken); } } }; refreshToken(); }, []); return null; // 该组件无需渲染可见内容 }
- 在你的服务器组件(page.tsx)里引入这个客户端组件:
import AuthInitializer from './AuthInitializer'; import { cookies } from 'next/headers'; export default async function Home() { // 此时可以正常获取已经设置好的Access Token const accessToken = cookies().get('access-token'); // 后续用户数据请求逻辑 const fetchUser = async () => { if (!accessToken) return undefined; const getUser = await foodyApi.get<SuccessfullResponse<{ user: User }>>( "/users/me", { headers: { Cookie: cookies().toString(), }, timeout: 10000, } ); return { user: getUser.data.rsp.user }; }; const userData = await fetchUser(); return ( <> <AuthInitializer /> {/* 页面内容渲染 */} {userData ? <div>欢迎回来,{userData.user.name}</div> : <div>请登录</div>} </> ); }
二、更优雅的方案:用Middleware自动处理Token刷新
其实,页面加载时自动刷新Token的需求,用Next.js Middleware来处理是最合适的,因为它会在每个请求到达服务器组件之前运行,可以无缝完成Token刷新,不需要客户端介入。
实现步骤:
- 在项目根目录创建
middleware.ts文件:
import { NextResponse } from 'next/server'; import type { NextRequest } from 'next/server'; export async function middleware(request: NextRequest) { // 1. 获取请求中的Cookie const accessToken = request.cookies.get('access-token'); const refreshToken = request.cookies.get('refresh-token'); // 2. 检查Access Token是否过期(示例:解码JWT验证过期时间) let isAccessTokenExpired = false; if (accessToken) { try { const decoded = JSON.parse(atob(accessToken.value.split('.')[1])); isAccessTokenExpired = Date.now() >= decoded.exp * 1000; } catch (error) { isAccessTokenExpired = true; // 解码失败视为Token无效 } } // 3. 如果Access Token过期且有Refresh Token,执行刷新逻辑 if (isAccessTokenExpired && refreshToken) { try { const response = await fetch('http://localhost:3000/api/access-token', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', body: JSON.stringify({ refreshToken: refreshToken.value }) }); if (response.ok) { const { accessToken: newAccessToken } = await response.json(); // 4. 创建新响应并设置新的Access Token Cookie const nextResponse = NextResponse.next(); nextResponse.cookies.set('access-token', newAccessToken, { path: '/', maxAge: 300, httpOnly: true, secure: process.env.NODE_ENV === 'production', sameSite: 'lax' }); return nextResponse; } } catch (error) { // 刷新失败,跳转到登录页 return NextResponse.redirect(new URL('/login', request.url)); } } // 正常请求直接放行 return NextResponse.next(); } // 指定Middleware生效的路由(排除静态资源和API路由) export const config = { matcher: ['/((?!api|_next/static|_next/image|favicon.ico).*)'], };
- 对应的
/api/access-token路由处理器:
import { NextResponse } from 'next/server'; import { cookies } from 'next/headers'; export async function POST(request: Request) { const cookieStore = cookies(); const refreshToken = cookieStore.get('refresh-token'); if (!refreshToken) { return NextResponse.json({ error: '缺少Refresh Token' }, { status: 401 }); } // 调用你的认证服务,用Refresh Token换取新的Access Token // const newAccessToken = await authService.refresh(refreshToken.value); const newAccessToken = 'your-generated-access-token'; // 替换为实际生成的Token return NextResponse.json({ accessToken: newAccessToken }); }
三、路由处理器的问题排查
你之前的/api/set-cookie没生效,可能有几个原因:
- 调用接口时未携带Refresh Token:客户端调用需添加
credentials: 'include',服务器组件调用需确保Cookie头正确传递。 - Cookie参数配置错误:比如
domain设置为localhost但请求用的是127.0.0.1,会导致Cookie不生效,建议去掉domain或匹配正确域名。 - 响应对象使用错误:路由处理器中需用
NextResponse而非普通Response.json,才能正确处理Cookie设置。
最后总结:用Middleware处理Token刷新是最优雅的方案,它能在请求到达页面之前自动完成Token更新,既不破坏服务器组件的缓存特性,也不需要客户端额外介入。如果一定要用Server Action,记得要在客户端组件里触发,不能在服务器组件的渲染流程中直接调用。
备注:内容来源于stack exchange,提问作者Diego Davincci

