集成Spotify API时交换授权码获取访问令牌遇400错误
针对你遇到的授权码交换令牌时返回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
相关产品推荐
相关产品推荐

