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

React Web应用请求Spotify Access Token时触发无限刷新循环

问题分析与解决方案

核心问题

你的代码出现无限刷新循环、无法正确加载Spotify授权页,主要源于以下几个问题:

  • 两个无依赖useEffect同时执行:组件挂载时两个useEffect触发,第一个立即跳转授权页,第二个同时尝试解析不存在的参数,触发无效过期判断后再次跳转,形成循环。
  • 参数解析位置错误:Spotify Implicit Grant Flow返回的access_token等参数在URL的hash(#)部分,而非search(?)部分,原代码用window.location.search根本无法获取token。
  • 第一个useEffect中clientId为空:空clientId会被Spotify直接拒绝,跳转回redirect_uri并携带错误参数,但循环跳转导致错误信息无法被捕获。

修复后的代码

import React, { useEffect } from 'react';

const generateRandomString = (length) => {
  const characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  let result = '';
  for (let i = 0; i < length; i++) {
    result += characters.charAt(Math.floor(Math.random() * characters.length));
  }
  return result;
};

const SpotifyAccessToken = ({ stateLength = 16 }) => {
  useEffect(() => {
    const clientId = 'd90891e3a05449b5992d7c531d9a8cc1'; // 确保填写正确的clientId
    const redirectUri = 'http://localhost:5173/';
    const scope = 'playlist-modify-private playlist-modify-public user-read-private';
    const responseType = 'token';

    // 先检查localStorage中是否有有效token
    const storedToken = localStorage.getItem('spotifyAccessToken');
    const storedExpiration = localStorage.getItem('spotifyTokenExpirationTime');
    
    if (storedToken && storedExpiration && parseInt(storedExpiration) > Date.now()) {
      // 已有有效token,清除URL参数后结束流程
      window.history.replaceState({}, document.title, window.location.pathname);
      return;
    }

    // 处理Spotify授权返回的hash参数
    const hashParams = new URLSearchParams(window.location.hash.slice(1)); // 去掉#号再解析
    const error = hashParams.get('error');
    const stateFromHash = hashParams.get('state');
    const storedState = sessionStorage.getItem('spotifyAuthState');

    if (error) {
      console.error('Authorization error:', error);
      sessionStorage.removeItem('spotifyAuthState');
      return;
    }

    // 验证state,防止CSRF攻击
    if (stateFromHash && stateFromHash === storedState) {
      const accessToken = hashParams.get('access_token');
      const expiresIn = parseInt(hashParams.get('expires_in'));
      
      if (accessToken && !isNaN(expiresIn)) {
        const expirationTime = Date.now() + expiresIn * 1000;
        localStorage.setItem('spotifyAccessToken', accessToken);
        localStorage.setItem('spotifyTokenExpirationTime', expirationTime);
        // 清除hash参数,避免重复处理
        window.history.replaceState({}, document.title, window.location.pathname);
        sessionStorage.removeItem('spotifyAuthState');
        return;
      }
    }

    // 无有效token且非授权返回,发起授权请求
    const state = generateRandomString(stateLength);
    sessionStorage.setItem('spotifyAuthState', state); // 存储state用于后续验证

    const queryParams = new URLSearchParams({
      client_id: clientId,
      redirect_uri: redirectUri,
      scope: scope,
      response_type: responseType,
      state: state,
    });

    const authorizeUrl = `https://accounts.spotify.com/authorize?${queryParams}`;
    window.location.href = authorizeUrl;
  }, []);

  return (
    <div>
      <p>Redirecting to Spotify for access...</p>
    </div>
  );
};

export default SpotifyAccessToken;

关键修复点

  1. 合并逻辑到单个useEffect:按「检查本地token有效性→处理授权返回→发起授权请求」的顺序执行,避免并发跳转导致的循环。
  2. 正确解析hash参数:通过window.location.hash.slice(1)获取并解析Spotify返回的token参数。
  3. 添加state验证:将生成的state存入sessionStorage,授权返回时验证一致性,符合Spotify安全规范。
  4. 统一clientId:修复原代码中两个useEffect使用不同clientId的问题。
  5. 避免重复跳转:仅在无有效token且非授权返回状态下,才跳转到Spotify授权页。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.24 19:23:27