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
相关产品推荐
相关产品推荐

