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

SvelteKit中如何处理并发Token刷新请求?

解决SvelteKit中服务端并发刷新Token的问题

问题背景

在SvelteKit应用的+page.server.js的load函数中,我用自定义的customFetch调用第三方后端接口,后端采用refresh token机制:当收到401响应时,会刷新Token并重试原请求;如果刷新失败(refresh token过期)就登出用户。

但用户操作过快时会出现并发刷新请求,第一个请求之外的所有刷新都会使用已过期的refresh token,导致后续请求失败并触发登出。通用方案是用一个Promise让后续请求等待刷新完成,但在SvelteKit服务端场景下,这个Promise该存在哪里?如果改用客户端的+page.js加载函数,是不是可以用setContext/getContext实现?

现有伪代码

+page.server.js

import { customFetch } from "$./customFetch.js";

export async function load(event) {
  const fetchSomeData = async () => {
    const res = await customFetch(
      "https://someURL.com",
      {
        method: "GET",
      },
      event
    );

    const data = await res.json();
    return data.data;
  };

  return {
    news: await fetchSomeData(),
  };
}

customFetch.js

export async function customFetch(url, config = {}, event) {
  const { cookies } = event;

  // 处理默认配置
  const headers = config.headers || {};
  const method = config.method || "GET";
  const body = config.body || null;
  const session = cookies.get("session");
  
  // 原代码存在逻辑问题,需从session中拆分token
  const { accessToken, refreshToken } = session ? JSON.parse(session) : {};

  // 添加Authorization头
  if (accessToken) {
    headers["Authorization"] = `Bearer ${accessToken}`;
  }

  // 发起初始请求
  const response = await event.fetch(url, { method, headers, body });

  if (response.status === 401) {
    // 原代码用全局变量存储refreshPromise,会导致跨请求污染
    if (!refreshPromise) {
      refreshPromise = refreshAccessToken(event);
    }

    await refreshPromise;
    return customFetch(url, { method, body, headers }, event);
  }

  refreshPromise = null;
  return response;
}

async function refreshAccessToken(event) {
  try {
    // 刷新Token逻辑:调用后端刷新接口
    const { cookies } = event;
    const session = cookies.get("session");
    const { refreshToken } = session ? JSON.parse(session) : {};
    
    const refreshRes = await event.fetch("https://backend.com/refresh", {
      method: "POST",
      body: JSON.stringify({ refreshToken })
    });

    if (!refreshRes.ok) throw new Error("刷新Token失败");
    
    const newSession = await refreshRes.json();
    // 服务端可通过cookies.set更新http-only cookie
    cookies.set("session", JSON.stringify(newSession), {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "strict",
      path: "/"
    });
  } catch (error) {
    // 刷新失败,登出用户
    event.cookies.delete("session", { path: "/" });
    throw error;
  } finally {
    refreshPromise = null;
  }
}

解决方案

1. 服务端场景(+page.server.js):将refreshPromise存在event.locals中

SvelteKit的event.locals是每个请求独立的上下文对象,不会在不同用户/请求间共享,完美解决全局变量的污染问题。修改后的customFetch.js如下:

export async function customFetch(url, config = {}, event) {
  const { cookies, locals } = event;

  // 处理默认配置
  const headers = config.headers || {};
  const method = config.method || "GET";
  const body = config.body || null;
  const session = cookies.get("session");
  const { accessToken, refreshToken } = session ? JSON.parse(session) : {};

  // 添加Authorization头
  if (accessToken) {
    headers["Authorization"] = `Bearer ${accessToken}`;
  }

  // 发起初始请求
  const response = await event.fetch(url, { method, headers, body });

  if (response.status === 401) {
    // 用event.locals存储刷新Promise,每个请求独立
    if (!locals.refreshPromise) {
      locals.refreshPromise = refreshAccessToken(event);
    }

    await locals.refreshPromise;
    // 刷新后重新调用customFetch,获取新token发起请求
    return customFetch(url, { method, body, headers }, event);
  }

  // 请求成功,清除刷新Promise
  locals.refreshPromise = null;
  return response;
}

async function refreshAccessToken(event) {
  try {
    const { cookies, locals } = event;
    const session = cookies.get("session");
    const { refreshToken } = session ? JSON.parse(session) : {};
    
    const refreshRes = await event.fetch("https://backend.com/refresh", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ refreshToken })
    });

    if (!refreshRes.ok) throw new Error("刷新Token失败");
    
    const newSession = await refreshRes.json();
    cookies.set("session", JSON.stringify(newSession), {
      httpOnly: true,
      secure: process.env.NODE_ENV === "production",
      sameSite: "strict",
      path: "/"
    });
  } catch (error) {
    cookies.delete("session", { path: "/" });
    throw error;
  } finally {
    // 清除locals里的Promise
    event.locals.refreshPromise = null;
  }
}

2. 客户端场景(+page.js):用setContext/getContext管理刷新Promise

如果是在客户端的+page.js或组件中调用customFetch,可以用Svelte的上下文API存储刷新Promise,确保每个用户会话独立:

首先在根布局+layout.svelte初始化上下文:

<script>
  import { setContext } from 'svelte';

  // 初始化刷新Promise上下文,初始为null
  setContext('refreshPromise', null);
</script>

<slot />

然后修改客户端版本的customFetch.js:

import { getContext, setContext } from 'svelte';

export async function customFetch(url, config = {}) {
  const headers = config.headers || {};
  const method = config.method || "GET";
  const body = config.body || null;
  
  // 客户端从localStorage获取session(需后端配合允许客户端存储token)
  const session = localStorage.getItem("session");
  const { accessToken, refreshToken } = session ? JSON.parse(session) : {};

  if (accessToken) {
    headers["Authorization"] = `Bearer ${accessToken}`;
  }

  const response = await fetch(url, { method, headers, body });

  if (response.status === 401) {
    let refreshPromise = getContext('refreshPromise');
    
    if (!refreshPromise) {
      refreshPromise = refreshAccessToken();
      setContext('refreshPromise', refreshPromise);
    }

    await refreshPromise;
    return customFetch(url, { method, body, headers });
  }

  setContext('refreshPromise', null);
  return response;
}

async function refreshAccessToken() {
  try {
    const session = localStorage.getItem("session");
    const { refreshToken } = session ? JSON.parse(session) : {};
    
    const refreshRes = await fetch("https://backend.com/refresh", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ refreshToken })
    });

    if (!refreshRes.ok) throw new Error("刷新Token失败");
    
    const newSession = await refreshRes.json();
    localStorage.setItem("session", JSON.stringify(newSession));
  } catch (error) {
    localStorage.removeItem("session");
    // 客户端跳转登录页
    window.location.href = "/login";
    throw error;
  } finally {
    setContext('refreshPromise', null);
  }
}

关键注意点

  • 服务端必须用event.locals存储刷新Promise,绝对不能用全局变量,否则会导致不同用户的请求互相干扰。
  • 客户端用setContext/getContext时,要确保上下文作用域覆盖所有需要调用customFetch的组件,根布局是最合适的选择。
  • 刷新Token成功后,一定要更新存储token的介质(服务端是http-only cookie,客户端根据实际情况选择localStorage或sessionStorage),确保后续请求能拿到新的token。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 20:36:03