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

Next.js:getServerSideProps中访问更新客户端存储Token的问题

服务端渲染(SSR)下JWT认证解决方案

核心问题

  1. getServerSideProps运行在Node.js环境,无法访问浏览器的localStorage,导致无法获取客户端存储的Token/Refresh Token
  2. 现有请求逻辑依赖浏览器API(localStorage、Router),直接在服务端调用会抛出错误
  3. 商品页SSR需要认证数据,但服务端无法携带客户端的认证信息发起请求

解决方案

方案一:Cookie传递Token(推荐)

将Token同时存储在Cookie和localStorage中,服务端通过请求Cookie获取Token,解决服务端认证问题。

1. 修改客户端Token存储逻辑

登录成功后,把Token和Refresh Token同时写入localStorage和Cookie:

// 登录成功回调
const handleLoginSuccess = (response) => {
  const { token, refreshToken } = response.data;
  // 存储到localStorage
  localStorage.setItem('token', token);
  localStorage.setItem('refreshToken', refreshToken);
  // 写入Cookie(可使用js-cookie库简化操作,或原生document.cookie)
  document.cookie = `token=${token}; path=/; secure; sameSite=Strict; maxAge=86400`;
  document.cookie = `refreshToken=${refreshToken}; path=/; secure; sameSite=Strict; maxAge=604800`;
};

2. 调整getServerSideProps获取Token

从请求上下文的req.cookies中提取Token,传入数据请求函数:

export async function getServerSideProps(context) {
  const { locale, params, req } = context;
  const { uuid } = params;
  const token = req.cookies.token || '';

  try {
    const data = await fetchProductData(uuid, locale, token);
    return { props: { uuid, data } };
  } catch (error) {
    // 处理401未授权,重定向到登录页
    if (error.message.includes('401')) {
      return {
        redirect: { destination: '/login', permanent: false }
      };
    }
    return { props: { uuid, data: null, error: true } };
  }
}

3. 修改请求函数支持服务端Token传入

更新fetchProductData、getData和executeRequest,区分服务端/客户端环境,避免浏览器API报错:

// fetchProductData新增token参数
export const fetchProductData = (uuid, locale, token = '') => {
  return new Promise((resolve, reject) => {
    getData(
      {
        url: `${BACKEND_URL}/v1/items/${uuid}`,
        params: { language: locale, program: "web" },
        token: token
      },
      (responseData, status) => {
        status === 200 ? resolve(responseData) : reject(new Error(`Failed to fetch: ${status}`));
      }
    );
  });
};

// getData传递token参数
export const getData = (fetchData, callback) => {
  const { url, params = null, token = '' } = fetchData;
  executeRequest({ url, method: "GET", params }, callback, token);
};

// executeRequest区分服务端/客户端逻辑
const executeRequest = async (request, callback, paramToken = "") => {
  // 服务端环境:直接使用传入的Token,不处理刷新(无法更新客户端存储)
  if (typeof window === 'undefined') {
    const token = paramToken;
    try {
      const response = await axios({
        ...request,
        headers: { Authorization: token ? `Bearer ${token}` : "" }
      });
      callback(response.data, 200);
    } catch (error) {
      callback(null, error.response?.status);
    }
    return;
  }

  // 客户端环境:保留原有Token刷新逻辑
  let token = paramToken || localStorage.getItem("token");
  let refreshTokenBlocked = false;
  try {
    const response = await axios({
      ...request,
      headers: { Authorization: token ? `Bearer ${token}` : "" }
    });
    callback(response.data, 200);
  } catch (error) {
    // 原有客户端错误处理逻辑(Token刷新、队列请求等)
    if (error.response?.status === 401) {
      if (token && !refreshInProgress) {
        refreshInProgress = true;
        try {
          const refreshToken = localStorage.getItem("refreshToken");
          const refreshResponse = await refreshTokenFunction(refreshToken);
          if (refreshResponse && refreshResponse !== "blocked") {
            refreshInProgress = false;
            token = refreshResponse;
            await Promise.all(
              requestQueue.map(async (queuedRequest) => {
                await executeRequest(queuedRequest.request, queuedRequest.callback, refreshResponse);
              })
            );
            requestQueue = [];
            requestCounter = 0;
          } else {
            refreshTokenBlocked = true;
          }
        } catch (err) {
          callback(null, err?.response?.status);
        }
      }
      requestQueue.push({ request: { ...request }, callback });
      requestCounter++;
    } else if (error.response?.status === 403) {
      store.dispatch(setIsBlocked(true));
      Router.push("/blocked");
    } else {
      callback("err", error.response?.status, error.response?.data);
    }
  } finally {
    if (!refreshTokenBlocked) {
      requestCounter--;
      if (requestCounter === 0 && requestQueue.length > 0) {
        const queuedRequest = requestQueue.shift();
        executeRequest(queuedRequest.request, queuedRequest.callback);
      }
    }
  }
};

方案二:公开数据免认证(简单场景)

如果商品信息是公开可访问的,无需用户登录即可查看,直接修改后端接口,取消商品详情接口的Token校验。这种方案无需修改前端逻辑,直接在getServerSideProps中发起无认证请求即可。

方案三:集成NextAuth.js(标准化认证)

使用NextAuth.js统一处理服务端和客户端的认证状态,它会自动将用户会话存储在HttpOnly Cookie中,服务端可通过getServerSession获取认证信息,快速对接后端接口。

关键注意事项

  • 服务端无法更新客户端的localStorage和Cookie,因此Token刷新逻辑只能在客户端执行
  • 所有依赖浏览器API的代码(如localStorage、Router)必须用typeof window !== 'undefined'做环境判断,避免服务端渲染报错
  • Cookie需设置Secure、SameSite=Strict属性,提升安全性

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.20 05:45:01