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

集成Spotify API时交换授权码获取访问令牌遇400错误

Spotify PKCE授权码交换400(invalid authorization code)问题排查与解决

针对你遇到的授权码交换令牌时返回400错误(提示invalid authorization code),以下是最常见的排查点和解决方法:

1. 确保Code Verifier与Code Challenge严格匹配

这是PKCE流程中最容易出错的环节:

  • 生成code_challenge必须使用对应的code_verifier进行SHA-256哈希,再做Base64URL编码(替换+为-、/为_,移除末尾的=)。
  • 回调时使用的code_verifier必须和跳转授权页时生成的完全一致,不能有拼写错误、大小写差异或截断。

2. 检查Redirect URI的严格一致性

Spotify对redirect_uri的校验非常严格:

  • 必须与你在Spotify开发者后台配置的Redirect URI完全一致,包括协议(http/https)、端口、路径,甚至末尾的斜杠都不能多/少。
  • 授权请求和令牌请求中的redirect_uri必须完全相同,不能出现比如授权用http://localhost:3000,令牌请求用http://localhost:3000/的情况。

3. 授权码只能单次使用

Spotify的授权码是一次性的,哪怕第一次请求失败,也不能再用同一个code发起令牌交换请求。遇到错误后,必须重新走授权流程获取新的授权码。

4. 修正Code Verifier/Challenge的生成逻辑

以下是符合Spotify要求的生成逻辑,替换你代码中占位的生成步骤:

// 生成符合规范的Code Verifier(43-128位,仅包含A-Z,a-z,0-9,-,.,_,~)
const generateCodeVerifier = () => {
  const array = new Uint32Array(32);
  window.crypto.getRandomValues(array);
  return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
};

// 从Code Verifier生成Code Challenge(SHA-256 + Base64URL)
const generateCodeChallenge = async (verifier) => {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await window.crypto.subtle.digest('SHA-256', data);
  return btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
};

修正后的完整流程代码

const clientId = '[YOUR_CLIENT_ID]';
const redirectUri = 'http://localhost:3000';

const generateCodeVerifier = () => {
  const array = new Uint32Array(32);
  window.crypto.getRandomValues(array);
  return Array.from(array, dec => ('0' + dec.toString(16)).substr(-2)).join('')
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
};

const generateCodeChallenge = async (verifier) => {
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const digest = await window.crypto.subtle.digest('SHA-256', data);
  return btoa(String.fromCharCode(...new Uint8Array(digest)))
    .replace(/\+/g, '-')
    .replace(/\//g, '_')
    .replace(/=/g, '');
};

const generateAuthCode = async () => {
  const codeVerifier = generateCodeVerifier();
  // 存储verifier到localStorage,回调时取用
  localStorage.setItem('spotify_code_verifier', codeVerifier);
  
  const codeChallenge = await generateCodeChallenge(codeVerifier);
  
  const authUrl = 'https://accounts.spotify.com/authorize';
  const params = {
    response_type: 'code',
    client_id: clientId,
    scope: 'playlist-modify-public playlist-modify-private',
    code_challenge_method: 'S256',
    code_challenge: codeChallenge,
    redirect_uri: redirectUri,
  };
  const queryString = new URLSearchParams(params).toString();
  window.location.href = `${authUrl}?${queryString}`;
};

const generateAccessToken = async (code, setUserInfo) => {
  const codeVerifier = localStorage.getItem('spotify_code_verifier');
  if (!codeVerifier) {
    console.error('未找到存储的Code Verifier');
    return;
  }
  
  const url = 'https://accounts.spotify.com/api/token';
  const payload = {
    method: 'POST',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    body: new URLSearchParams({
      client_id: clientId,
      grant_type: 'authorization_code',
      code,
      redirect_uri: redirectUri,
      code_verifier: codeVerifier,
    }),
  };

  try {
    const response = await fetch(url, payload);
    if (!response.ok) {
      const errorData = await response.json();
      console.error('令牌交换失败:', errorData);
      // 若为无效授权码,清除存储的verifier并提示重新授权
      if (errorData.error === 'invalid_grant') {
        localStorage.removeItem('spotify_code_verifier');
        alert('授权码无效,请重新发起授权');
      }
      return;
    }
    const responseData = await response.json();
    setUserInfo((prevInfo) => ({
      api_key: responseData.access_token,
      userId: prevInfo.userId,
    }));
    // 用完后删除verifier,避免重复使用
    localStorage.removeItem('spotify_code_verifier');
  } catch (error) {
    console.log('请求错误:', error);
  }
};

export { generateAuthCode, generateAccessToken };

其他注意事项

  • 确认请求的Content-Type确实是application/x-www-form-urlencoded,Spotify令牌端点不接受JSON格式的请求体。
  • 检查client_id是否与Spotify开发者后台的完全一致,无多余空格或字符。
  • 授权码的有效期为10分钟,若用户授权后长时间未完成令牌交换,也会导致失效,这种情况只需重新授权即可。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 00:32:07