如何获取Spotify Web API的Refresh Token解决令牌过期问题
Spotify Web JS API 刷新令牌实现方案
你当前的代码用的是隐式授权流程,直接从URL哈希片段里取access_token,这个流程是Spotify早期给纯前端应用提供的简化授权方式,天生不会返回Refresh Token,自然没法实现无感刷新。要拿到Refresh Token,你需要把授权流程切换为官方推荐的、适配纯前端场景的PKCE授权码流程。
具体实现步骤
1. 调整授权跳转逻辑,首次授权时获取Refresh Token
你之前跳转到Spotify授权页的请求参数需要修改,否则拿不到Refresh Token:
- 把
response_type从原来的token改为code - 新增PKCE校验相关参数:提前生成随机的
code_verifier存在本地,基于它计算S256格式的code_challenge,请求时带上code_challenge和code_challenge_method=S256 - 新增参数
access_type=offline,这个参数是Spotify返回Refresh Token的必要条件 - 保持你原来需要的
scope、redirect_uri、client_id参数不变
用户同意授权后,Spotify会重定向回你的站点,URL查询参数里会带上临时授权码code,而不是直接返回access_token。
2. 用授权码换取Access Token和Refresh Token
拿到回调里的code之后,向Spotify的token接口发POST请求,带上code、redirect_uri、client_id、之前存的code_verifier、grant_type=authorization_code,接口返回的结果里会包含三个核心字段:
access_token:短期访问令牌,有效期1小时refresh_token:长期刷新令牌,用来换取新的access_tokenexpires_in:access_token的有效秒数,固定为3600
拿到这三个值之后,不要只存access_token,要把三个值都存到localStorage,同时额外存一个过期时间戳:Date.now() + expires_in * 1000,方便后续判断是否过期。
3. 实现令牌自动刷新逻辑
你提到的“每次页面加载都刷新token”的思路虽然能用,但会产生不必要的接口请求,更合理的逻辑如下:
- 页面加载时优先从localStorage读取已存储的凭证
- 如果本地没有任何凭证,直接跳转Spotify授权页走首次授权流程
- 如果本地有凭证,先对比当前时间和存储的过期时间戳:
- 令牌未过期:直接用存的access_token初始化SDK,发起后续业务请求
- 令牌已过期:拿存的refresh_token调用Spotify的token接口,参数设为
grant_type=refresh_token,带上refresh_token和client_id,拿到新的access_token和过期时间后,更新本地存储的凭证,再初始化SDK发请求
- 额外容错:如果业务接口请求返回401状态码,不管本地判断的过期状态是什么,都主动走一次刷新逻辑,刷新成功后重试原请求;如果刷新接口也报错(比如refresh_token过期失效),就清空本地存储,跳回授权页让用户重新授权。
改造后的参考代码
// 提前实现PKCE参数生成、授权页跳转逻辑,可直接复用通用的前端PKCE工具方法 useEffect(() => { const initSpotify = async () => { // 处理授权回调携带的临时授权码 const urlParams = new URLSearchParams(window.location.search); const code = urlParams.get('code'); let token = window.localStorage.getItem("token"); let refreshToken = window.localStorage.getItem("refresh_token"); let expiresAt = window.localStorage.getItem("expires_at"); // 首次授权回调场景:用code换正式凭证 if (code) { window.history.replaceState({}, document.title, window.location.pathname); // 清除URL中的敏感参数 const codeVerifier = window.localStorage.getItem("code_verifier"); const res = await fetch("https://accounts.spotify.com/api/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "authorization_code", code: code, redirect_uri: "你的应用回调地址", client_id: "你的client_id", code_verifier: codeVerifier, }), }); const data = await res.json(); token = data.access_token; refreshToken = data.refresh_token; expiresAt = Date.now() + data.expires_in * 1000; // 持久化存储凭证 window.localStorage.setItem("token", token); window.localStorage.setItem("refresh_token", refreshToken); window.localStorage.setItem("expires_at", expiresAt); window.localStorage.removeItem("code_verifier"); } // 无有效凭证场景:跳转授权 if (!token || !refreshToken) { // 此处写入生成PKCE参数、拼接授权URL、跳转Spotify授权页的逻辑 return; } // 令牌过期场景:用refresh_token换新令牌 if (Date.now() > Number(expiresAt)) { const res = await fetch("https://accounts.spotify.com/api/token", { method: "POST", headers: { "Content-Type": "application/x-www-form-urlencoded" }, body: new URLSearchParams({ grant_type: "refresh_token", refresh_token: refreshToken, client_id: "你的client_id", }), }); const data = await res.json(); token = data.access_token; // 部分场景下Spotify会返回新的refresh_token,需要同步更新 if (data.refresh_token) { refreshToken = data.refresh_token; window.localStorage.setItem("refresh_token", refreshToken); } expiresAt = Date.now() + data.expires_in * 1000; window.localStorage.setItem("token", token); window.localStorage.setItem("expires_at", expiresAt); } // 初始化SDK,发起业务请求 setToken(token); spotify.setAccessToken(token); spotify.getMyRecentlyPlayedTracks({ limit: 50 }).then((data) => { setRecentlyPlayed(data.items); }); spotify.getMySavedAlbums().then((user) => { console.log("Saved albums:", user); }); spotify.getFollowedArtists().then((user) => { setNumberArtistsFollowing(user.artists.total); }); spotify.getMyCurrentPlayingTrack().then((data) => { if (data) { setCurrentlyPlaying(data); } }); spotify.getMe().then(setUserProfile); }; initSpotify(); }, []);
注意事项
- PKCE流程不需要使用Client Secret,绝对不要把Client Secret写到前端代码里,否则会造成应用凭证泄露
- Refresh Token虽然是长期有效,但如果用户长时间不访问应用、或者主动撤销授权,Refresh Token也会失效,这时候需要引导用户重新走授权流程
- 所有和Spotify token接口的交互直接用原生
fetch实现即可,不需要额外引入SDK
内容的提问来源于stack exchange,提问作者Brad Bieselin
相关产品推荐
相关产品推荐

