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

基于Mongoose实现Spotify OAuth2 refresh token存取与自动刷新

Spotify API OAuth2 持久化授权实现修正方案

现有代码核心问题

  • 重复初始化2次SpotifyWebApi实例,存在冗余逻辑
  • 服务启动时没有优先查询数据库中已存储的有效refresh token,无条件执行授权码授予流程,每次重启都要重复授权
  • 授权码硬编码在源码中,每次换code都要手动改代码重新部署,流程不闭环
  • 存储refresh token时每次都生成新_id插入文档,会在库中生成多条重复记录,后续查询无法定位到最新有效token
  • 没有处理refresh token失效的异常分支,token吊销后会无限刷新报错
  • 定时刷新任务没有做去重,多次触发会生成重复定时器

第一步:优化RefreshToken模型

给模型增加固定类型字段做唯一约束,保证库中仅存一条有效Spotify refresh token,避免多记录混乱:

const mongoose = require('mongoose');

const tokenSchema = mongoose.Schema({
  type: {
    type: String,
    default: 'spotify_refresh',
    unique: true // 固定标识,保证该类型文档全局唯一
  },
  token: String,
  updatedAt: {
    type: Date,
    default: Date.now
  }
});

module.exports = mongoose.model("RefreshToken", tokenSchema);

后续查询、更新token直接以type: 'spotify_refresh'为条件即可,不需要手动维护ObjectId,也不会出现重复插入问题。

第二步:重构授权流程逻辑

核心判断规则:服务启动连接数据库完成后,先查询库中是否存在有效refresh token:

  • 存在有效token:直接将token设置到API实例,立即触发一次access token刷新,启动定时刷新任务,全程跳过授权码流程
  • 不存在有效token:打印授权链接,引导手动访问完成授权,通过回调接口自动接收授权码换取token,持久化到库后再启动刷新任务
  • 不要把授权码硬编码在源码中,通过回调路由自动接收Spotify回传的code参数,省掉手动改代码重部署的操作

第三步:修正后的完整业务代码

const express = require('express');
const mongoose = require('mongoose');
const SpotifyWebApi = require('spotify-web-api-node');
// 替换成你自己的RefreshToken模型存放路径
const RefreshToken = require('./models/RefreshToken');

// 生成OAuth流程所需的随机state
const generateRandomString = function(length) {
  let text = '';
  const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
  for (let i = 0; i < length; i++) {
    text += possible.charAt(Math.floor(Math.random() * possible.length));
  }
  return text;
};

// 基础配置常量
const scopes = ['user-read-private', 'user-read-email', 'playlist-read-private', 'playlist-modify-private', 'playlist-modify-public'];
const redirectUri = '<替换为你的回调地址>';
const clientId = '<替换为你的Spotify应用clientId>';
const clientSecret = '<替换为你的Spotify应用clientSecret>';
const state = generateRandomString(16);
const SERVICE_PORT = 3000; // 替换为你自己的服务端口

// 全局仅初始化一次Spotify API实例,删除原代码中重复初始化的部分
const spotifyApi = new SpotifyWebApi({
  redirectUri: redirectUri,
  clientId: clientId,
  clientSecret: clientSecret
});

const app = express();

// 登录跳转路由
app.get('/login', function(req, res) {
  const authorizeURL = spotifyApi.createAuthorizeURL(scopes, state);
  res.redirect(authorizeURL);
});

// Spotify授权回调路由,自动接收授权码
app.get('/callback', async (req, res) => {
  const { code, error } = req.query;
  if (error) {
    return res.send(`授权失败:${error}`);
  }
  try {
    const tokenRes = await spotifyApi.authorizationCodeGrant(code);
    const { access_token, refresh_token, expires_in } = tokenRes.body;
    
    // 将获取到的token设置到API实例
    spotifyApi.setAccessToken(access_token);
    spotifyApi.setRefreshToken(refresh_token);

    // 持久化refresh token到数据库:存在则更新,不存在则新建,保证仅一条有效记录
    await RefreshToken.findOneAndUpdate(
      { type: 'spotify_refresh' },
      { token: refresh_token, updatedAt: Date.now() },
      { upsert: true, new: true }
    );

    // 启动定时刷新任务
    startTokenRefreshTask();
    res.send('授权成功,可关闭当前页面');
  } catch (err) {
    console.error('授权码换取token失败:', err);
    res.send('授权出错,请查看服务端日志');
  }
});

// access token刷新逻辑
function refreshSpotifyToken() {
  return spotifyApi.refreshAccessToken()
    .then(function(data) {
      console.log('access token刷新完成');
      spotifyApi.setAccessToken(data.body['access_token']);
      console.log('新token有效期:', data.body['expires_in'], '秒');
      return data.body['access_token'];
    })
    .catch(function(err) {
      console.error('access token刷新失败,refresh token可能已失效,请重新授权:', err);
      // 清空库中无效的旧token
      RefreshToken.deleteOne({ type: 'spotify_refresh' }).catch(console.error);
      console.log(`请访问 http://你的服务地址:${SERVICE_PORT}/login 重新完成授权`);
    });
}

let refreshTimer = null;
// 统一的定时任务启动方法,避免重复创建定时器
function startTokenRefreshTask() {
  if (refreshTimer) clearInterval(refreshTimer);
  // 启动时先刷新一次token,之后每59分钟刷新一次(Spotify access token默认有效期1小时)
  refreshSpotifyToken();
  refreshTimer = setInterval(refreshSpotifyToken, 1000 * 60 * 59);
}

// 客户端就绪后的启动逻辑
client.on('ready', async () => {
  try {
    // 连接MongoDB
    await mongoose.connect(process.env.MONGODB_SPECTRUM, {
      useNewUrlParser: true,
      useUnifiedTopology: true,
      keepAlive: true
    });
    console.log('已连接到Spectrum数据库');

    // 启动回调服务
    app.listen(SERVICE_PORT, () => {
      console.log(`授权回调服务已启动,监听端口${SERVICE_PORT}`);
    });

    // 核心判断:查询库中是否存在已存储的refresh token
    const storedRefreshToken = await RefreshToken.findOne({ type: 'spotify_refresh' });
    if (storedRefreshToken?.token) {
      console.log('检测到已持久化的refresh token,直接加载');
      spotifyApi.setRefreshToken(storedRefreshToken.token);
      // 启动刷新任务,跳过授权流程
      startTokenRefreshTask();
    } else {
      console.log('未检测到有效refresh token,请访问以下地址完成首次授权:');
      console.log(`http://你的服务地址:${SERVICE_PORT}/login`);
    }
  } catch (err) {
    console.error('服务启动失败:', err);
  }
});

关键改动说明

  • 首次部署时只需访问一次控制台打印的/login地址完成授权,后续服务重启、进程拉起时只要库中refresh token未失效,就会自动加载刷新,不需要重复操作
  • 数据库操作采用upsert模式,保证永远只存一条有效refresh token,不会出现重复数据
  • 增加了refresh token失效兜底逻辑:如果刷新接口报错(比如token被Spotify吊销、权限变更),会自动清理旧无效token,提示重新授权,不会无限循环报错
  • 定时器做了去重处理,避免多次调用生成重复的定时任务
  • 删除了冗余的重复API实例初始化代码,全局使用同一个spotifyApi实例,避免token状态不一致

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.28 14:06:30