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

Next.js JWT认证下getServerSideProps处理Cookie的更优方案

问题描述

(编辑补充:以下是对所解决问题的更清晰总结,现有方案最终也采用了和spreadCookiesToContext(ctx, apiResponse);相同的实现思路)

核心需求与问题背景:

  • 页面渲染前需要在服务端拉取用户数据,流程第一步是验证加密存储在浏览器Cookie中的access_token,同时需要支持页面渲染前完成多个Cookie的新增、更新、删除操作
  • 用户Token验证逻辑需要复用,因此要在getServerSideProps函数作用域外的独立模块统一处理Cookie逻辑,比如getServerSideProps内部发起API请求时统一处理Cookie,这个场景实现存在较多坑
  • 实际调试发现:getServerSideProps中调用Cookie处理逻辑,和普通API端点中的调用逻辑存在差异:Postman调接口时Cookie可以正常设置,但getServerSideProps加载页面时Cookie无法写入浏览器
  • 问题根因:getServerSideProps不是浏览器像调用API路由那样直接发起的请求,因此不能用和API路由相同的方式设置浏览器Cookie,必须操作Next.js的上下文对象,不能直接操作Node.js原生request对象

目前在Next.js中使用getServerSideProps()在页面加载前从外部API拉取数据,基础拉取逻辑本身运行正常,参考代码如下:

async function getInitialValues(
    ctx: GetServerSidePropsContext,
): Promise<GetServerSidePropsResult<Props>> {
    const { query } = ctx;
    try {
        const authTestData = await axiosAuthTest(ctx);
        spreadCookiesToContext(ctx, authTestData);

        const data = await axiosFetchData(query, ctx);

        return {
            props: {
                data: data.data,
                query,
            },
        };
    } catch (e) {
        // 错误处理逻辑
    }
}

但调用需要有效access_token的受保护API路由、且同一个getServerSideProps函数中需要发起多次API调用时,必须重复编写如下代码同步Cookie:

spreadCookiesToContext(ctx, apiResponse);

该函数的定义如下:

function spreadCookiesToContext(
    ctx: GetServerSidePropsContext,
    res: AxiosResponse,
): Record<string, string> | null {
    const cookies: string[] | undefined =
        res.config.headers['set-cookie'] ?? res.headers['set-cookie'];
    if (cookies) {
        ctx.res.setHeader('Set-Cookie', cookies);
        return cookie.parse(cookies.join(';'));
    }
    return null;
}

这个函数接收getServerSideProps()上下文和Axios响应对象,把响应中的set-cookie头写入上下文响应。需要这个逻辑的原因是用户access_token有效期仅5分钟,access_token过期时Axios拦截器会自动刷新Token后重试请求,拦截器返回的响应会携带新的refresh_token和access_token,需要保存到用户Cookie中,其中refresh_token为httpOnly属性,仅能在服务端访问。

目前想到的优化方案是使用authGuard包装器包裹所有受保护路由的请求:

const testData = await authGuard<AuthTestResposne>(
    axiosAuthTest(ctx),
    ctx,
);

该函数会调用对应API接口,自动将响应中的Cookie同步到传入的getServerSideProps()上下文,无需在每个getServerSideProps函数中重复调用spreadCookiesToContext(),所有鉴权API请求都通过这个authGuard包装器调用即可,包装器代码如下:

async function authGuard<T>(
    apiFunc: Promise<AxiosResponse<T>>,
    ctx: GetServerSidePropsContext,
): Promise<AxiosResponse<T>> {
    const data = await apiFunc;
    spreadCookiesToContext(ctx, data);

    return data;
}

Axios拦截器代码

有提问提到“为什么不在拦截器中直接设置Cookie?”,目前拦截器中已经实现了Cookie设置逻辑,代码如下:

if (error.response.status === 401 && !originalRequest._retry) {
    originalRequest._retry = true;

    const config: AxiosRequestConfig | undefined = !hasWindow
        ? {
                headers: { cookie: originalRequest.headers.cookie },
                withCredentials: true,
            }
        : undefined;

    try {
        const { data, headers } = await axiosInternalRefresh(config);
        const cookieConfig = !hasWindow
            ? { 'set-cookie': headers['set-cookie'] }
            : undefined;

        const updatedRequestWithRefreshedToken = {
            ...originalRequest,
            headers: {
                ...originalRequest.headers,
                Authorization: `Bearer ${data.accessToken}`,
                ...cookieConfig,
            },
        };

        return await axiosInstance(updatedRequestWithRefreshedToken);
    } catch (err) {
        return Promise.reject(new APIError(error));
    }
}

虽然拦截器中已经做了Cookie设置,但新的access_token仍然无法保存到客户端浏览器,会导致用户被登出,鉴权API的重试逻辑本身可以正常执行并返回数据。
其中axiosInternalRefresh(config);用于调用API的Token刷新接口,返回新的access_token和refresh_token对。

待咨询问题

目前的实现需要手动封装包装器同步Cookie,拦截器刷新的Token也无法自动同步到浏览器,是否存在更简洁的方式实现上述鉴权、Cookie同步、Token自动刷新的完整功能?或者是否有完全不同的实现思路可以解决该问题?


解决方案

现有实现思路本身没有问题,拦截器没法自动同步Cookie的核心原因是:服务端运行的Axios实例拿不到当前请求的ctx对象,自然没法把刷新拿到的Cookie写到Next.js的上下文响应头里。不需要推翻现有逻辑,做两层改造就能完全去掉重复代码,实现全自动同步:

1. 改造Axios实例创建逻辑,给服务端请求绑定上下文

不要用全局单例的Axios实例处理服务端请求,改成按请求创建实例的模式,把当前请求的ctx绑定到Axios实例的配置上,这样拦截器里就能直接拿到ctx对象,不需要在外层手动传。
参考实现:

import { GetServerSidePropsContext } from 'next';
import axios, { AxiosInstance } from 'axios';

// 客户端用全局单例即可,不存在ctx隔离问题
const clientAxiosInstance = axios.create({
  withCredentials: true,
  baseURL: process.env.NEXT_PUBLIC_API_BASE_URL
});

// 服务端每次请求创建带ctx的独立实例
export const getServerAxiosInstance = (ctx: GetServerSidePropsContext): AxiosInstance => {
  const instance = axios.create({
    baseURL: process.env.API_BASE_URL, // 服务端可以走内网地址提升性能
    headers: {
      cookie: ctx.req.headers.cookie || '' // 自动把当前请求的Cookie带到上游API
    },
    withCredentials: true,
    // 把ctx挂到实例配置上,拦截器里可以直接读取
    _ctx: ctx
  } as any);

  // 响应拦截器:所有响应(包括Token刷新后的重试响应)自动同步Cookie到ctx
  instance.interceptors.response.use(
    (res) => {
      const setCookieHeader = res.headers['set-cookie'];
      if (setCookieHeader) {
        const existingCookies = ctx.res.getHeader('Set-Cookie') as string[] || [];
        // 追加而不是覆盖Set-Cookie头,避免多次请求设置的Cookie互相覆盖
        ctx.res.setHeader('Set-Cookie', [...existingCookies, ...setCookieHeader]);
      }
      return res;
    },
    async (error) => {
      const originalRequest = error.config;
      // 原有401刷新逻辑直接迁移,刷新接口返回的Set-Cookie会被上面的响应拦截器自动同步到ctx
      if (error.response?.status === 401 && !originalRequest._retry) {
        originalRequest._retry = true;
        try {
          const { data } = await instance.post('/auth/refresh');
          originalRequest.headers.Authorization = `Bearer ${data.accessToken}`;
          return instance(originalRequest);
        } catch (refreshErr) {
          // 刷新失败直接重定向到登录页,不需要上层页面再处理
          ctx.res.writeHead(302, { Location: '/login' });
          ctx.res.end();
          return Promise.reject(refreshErr);
        }
      }
      return Promise.reject(error);
    }
  );

  return instance;
};

2. 简化getServerSideProps里的调用逻辑

改造之后完全不需要authGuard包装器,也不需要手动调用spreadCookiesToContext,所有通过这个服务端Axios实例发出的请求,不管是第一次请求还是Token刷新后的重试请求,只要上游API返回了Set-Cookie头,都会被自动追加到Next.js的响应头里,最终自动写到用户浏览器。
原有的getInitialValues可以简化成这样:

async function getInitialValues(
    ctx: GetServerSidePropsContext,
): Promise<GetServerSidePropsResult<Props>> {
    const { query } = ctx;
    // 直接获取绑定了当前ctx的Axios实例
    const serverAxios = getServerAxiosInstance(ctx);
    try {
        // 所有请求直接用这个实例发送,不需要手动处理Cookie
        const authTestData = await serverAxios.get('/auth/test');
        const data = await serverAxios.get('/data', { params: query });

        return {
            props: {
                data: data.data,
                query,
            },
        };
    } catch (e) {
        // 仅处理业务错误,Token刷新失败已经在拦截器里处理重定向
    }
}

关键注意事项

  • 注意:Set-Cookie头是多值数组格式,不要直接调用setHeader覆盖原有值,要把多次请求返回的Cookie追加到已有值后面,不然同一次getServerSideProps里多次请求设置的Cookie会互相覆盖,最终只有最后一个请求的Cookie会生效
  • 注意:服务端Axios实例绝对不能做成全局单例,多用户并发请求时会出现上下文串号、Cookie写到其他用户响应里的严重安全问题,必须每个服务端请求单独创建实例,请求处理完成后实例随上下文一起销毁,做到用户级隔离
  • 如果使用Next.js 13+的App Router,逻辑完全一致,只需要把GetServerSidePropsContext换成框架提供的cookies()、headers()操作方法即可,核心逻辑还是拿到服务端响应对象后自动追加Set-Cookie头
  • 客户端侧的Axios实例不需要这套逻辑,浏览器本身会自动处理Cookie存储,拦截器只需要保留401刷新Token的逻辑即可

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.30 21:48:48