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

Next.js 14中设置Cookie实现认证授权的疑难问题

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:

  1. 写一个客户端组件(比如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; // 该组件无需渲染可见内容
}
  1. 在你的服务器组件(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刷新,不需要客户端介入。

实现步骤:

  1. 在项目根目录创建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).*)'],
};
  1. 对应的/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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.21 09:52:58