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

如何获取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_token
  • expires_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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 22:01:10