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

React/Next.js应用跨API路由使用Access Token持续过期问题求助

Access Token跨API路由失效问题排查与解决

问题描述

开发React/Next.js应用时遇到以下问题:

  • 首次调用Spotify API加载全部专辑,能正常获取并使用Access Token;
  • 后续调用获取专辑详情的API时,始终返回Access Token过期错误,即使硬编码Token也无法解决。

相关代码

获取全部专辑文件

import { BASE_URL_MUSIC } from "@/shared/constants/urls";
import { getValidToken } from "../token/tokenManager";

export const GET = async (req: any) => {
  try {
    const data = await getAllAlbums();
    return new Response(JSON.stringify({ data }), { status: 200 });
  } catch (error) {
    console.error("获取专辑失败:", error);
    return new Response(JSON.stringify({ error: "获取专辑失败" }), {
      status: 500,
    });
  }
};

const getAllAlbums = async () => {
  const token = await getValidToken();

  const response = await fetch(
    `${BASE_URL_MUSIC}/browse/new-releases?limit=20&country=GB`,
    {
      method: "GET",
      headers: {
        Authorization: `Bearer ${token}`,
      },
    }
  );

  if (!response.ok) {
    const error = await response.json();
    throw { status: response.status, ...error };
  }

  return response.json();
};

获取专辑详情文件

import { BASE_URL_MUSIC } from "@/shared/constants/urls";
import { getValidToken } from "../token/tokenManager";

export const GET = async (req: any) => {
  const { searchParams } = new URL(req.url);
  const id = searchParams.get("id");

  try {
    const data = await getAlbumDetails(id);
    return new Response(JSON.stringify({ data }), { status: 200 });
  } catch (error: any) {
    // 总是走到这里
    console.error("获取专辑详情出错:", error);
    if (error.status === 401) {
      console.log("Token已过期,正在刷新...");

      try {
        const token = await getValidToken();
        const data = await getAlbumDetails(id, token);
        return new Response(JSON.stringify({ data }), { status: 200 });
      } catch (tokenError) {
       // 然后走到这里
        console.error("刷新Token失败:", tokenError);
        return new Response(
          JSON.stringify({
            error: "刷新Token并获取专辑详情失败",
          }),
          { status: 500 }
        );
      }
    } else {
      return new Response(
        JSON.stringify({ error: "获取专辑详情失败" }),
        { status: 500 }
      );
    }
  }
};

const getAlbumDetails = async (id: any, token?: string) => {
  if (!token) {
    token = await getValidToken();
  }

  const response = await fetch(`${BASE_URL_MUSIC}/albums/${id}`, {
    method: "GET",
    headers: {
      Authorization: `Bearer ${token}`,
    },
  });

  if (!response.ok) {
    const error = await response.json();
    throw { status: response.status, ...error };
  }

  return response.json();
};

getToken文件

interface TokenResponse {
  access_token: string;
  expires_in: number;
}

export const getToken = async (): Promise<TokenResponse> => {
  const response = await fetch("http://localhost:3000/api/music/token/");
  const data = await response.json();

  if (!data.data || !data.data.access_token || !data.data.expires_in) {
    throw new Error("无效的Token响应");
  }

  console.log("获取到Token:", data.data.access_token);

  return {
    access_token: data.data.access_token,
    expires_in: data.data.expires_in,
  };
};

tokenManager文件

import { getToken } from "./getToken";

interface TokenResponse {
  access_token: string;
  expires_in: number;
}

let cachedToken: string | null = null;
let tokenExpiry: number | null = null;

export const getValidToken = async (): Promise<string> => {
  const now = Date.now();

  if (cachedToken && tokenExpiry && tokenExpiry > now) {
    console.log("使用缓存的Token");
    return cachedToken;
  }

  console.log("正在获取新的Token...");
  const tokenResponse: TokenResponse = await getToken();
  cachedToken = tokenResponse.access_token;
  tokenExpiry = now + tokenResponse.expires_in * 1000;

  console.log("获取到新的Token", cachedToken);

  if (!cachedToken) {
    throw new Error("无法获取有效的Token");
  }

  return cachedToken;
};

排查与解决方案

1. 检查Token源接口有效性

问题核心可能出在/api/music/token/内部接口:

  • 直接用Postman或curl调用该接口,拿到返回的access_token;
  • 用这个Token手动调用Spotify的/albums/{id}接口,验证是否返回401;
  • 如果手动调用也失败,说明该接口返回的Token本身无效,需检查其向Spotify申请Token的逻辑(比如client_id、client_secret是否正确,授权流程是否符合Spotify要求)。

2. 优化Token获取逻辑,避免内部API调用

当前getToken通过fetch调用自身API路由获取Token,增加了不必要层级,还可能引入状态问题。建议直接在getToken中实现向Spotify申请Token的逻辑:

// 修改后的getToken.ts
interface TokenResponse {
  access_token: string;
  expires_in: number;
}

export const getToken = async (): Promise<TokenResponse> => {
  const clientId = process.env.SPOTIFY_CLIENT_ID;
  const clientSecret = process.env.SPOTIFY_CLIENT_SECRET;

  if (!clientId || !clientSecret) {
    throw new Error("缺少Spotify客户端凭证");
  }

  const authHeader = Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
  const response = await fetch("https://accounts.spotify.com/api/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization: `Basic ${authHeader}`,
    },
    body: new URLSearchParams({
      grant_type: "client_credentials",
    }),
  });

  if (!response.ok) {
    const error = await response.json();
    throw new Error(`申请Token失败: ${error.error_description}`);
  }

  const data = await response.json();
  console.log("获取到Spotify Token:", data.access_token);

  return {
    access_token: data.access_token,
    expires_in: data.expires_in,
  };
};

3. 验证Token缓存逻辑

  • 在getValidToken中添加日志,打印tokenExpiry和当前时间的差值,确认过期时间计算正确:
    console.log("Token过期时间:", new Date(tokenExpiry), "当前时间:", new Date(now));
    console.log("剩余有效期(秒):", (tokenExpiry - now)/1000);
    
  • 确认expires_in是Spotify返回的秒数,当前代码中tokenExpiry = now + tokenResponse.expires_in * 1000的转换逻辑正确。

4. 检查请求权限与URL

  • 确认Spotify Token权限:Client Credentials Flow获取的Token默认拥有所有公开数据访问权限,理论上可访问/albums/{id}接口;
  • 检查BASE_URL_MUSIC是否为https://api.spotify.com/v1,专辑ID是否无多余字符(如空格、编码错误)。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.20 08:58:14