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

React客户端调用Spotify API时出现401未授权问题求助

集成Spotify API的React应用认证流程问题:useAuth无法连接服务器

我正在开发首个集成Spotify API的React应用,客户端运行于localhost:5173,服务器运行于localhost:3001,但在认证流程中遇到问题——负责获取accessToken的自定义Hook useAuth无法连接到服务器。尝试过用ChatGPT解决但效果不佳,可能是我把问题复杂化了。

useAuth.js代码

import { useState, useEffect } from 'react'
import axios from "axios";

const API_URL = 'http://localhost:3001';

export default function useAuth(code) {
  const [accessToken, setAccessToken] = useState(null);
  const [refreshToken, setRefreshToken] = useState(null);
  const [expiresIn, setExpiresIn] = useState(null);

  useEffect(() => {
    if (!code) {
      console.warn('No code provided, skipping authentication.');
      return;
    }

    axios.post(`${API_URL}/login`, { code }, { withCredentials: true })
      .then(res => {
        console.log('Successfully obtained tokens:', res.data);
        setAccessToken(res.data.accessToken);
        setRefreshToken(res.data.refreshToken);
        setExpiresIn(res.data.expiresIn);
        window.history.pushState({}, null, '/');
      })
      .catch(err => {
        console.error('Error during authentication:', err.response || err.message);
        console.log(code);
        alert('Failed to authenticate. Please log in again.');
        window.location = '/';
      });
  }, [code]);

  useEffect(() => {
    if (!refreshToken || !expiresIn) return;
    const interval = setInterval(() => {

      axios
      .post(`${API_URL}/refresh`, {
        refreshToken,
      })
      .then(res => {
        console.log('Token refreshed successfully:', res.data);
        setAccessToken(res.data.accessToken);
        setExpiresIn(res.data.expiresIn);
      })
      .catch(error => {
        console.error('Error during token refresh:', error);
        alert('Session expired. Please log in again.');
        window.location = '/';
      })
    }, (expiresIn - 60) * 1000);

    return () => clearInterval(interval);
  }, [refreshToken, expiresIn])


  return accessToken;
}

server.js代码

import 'dotenv/config';
console.log('Process object:', process.env);
import express from 'express';
import SpotifyWebApi from 'spotify-web-api-node';
import cors from 'cors';
import bodyParser from 'body-parser';
import session from 'express-session';

console.log('SPOTIFY_CLIENT_ID:', process.env.SPOTIFY_CLIENT_ID);
console.log('SPOTIFY_CLIENT_SECRET:', process.env.SPOTIFY_CLIENT_SECRET);
console.log('REDIRECT_URI:', process.env.REDIRECT_URI);

const app = express();
const port = 3001;

const corsOptions = {
  origin: 'http://localhost:5173',
  credentials: true,  // Allow credentials to be sent/received
  methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],  // Allowed HTTP methods
  allowedHeaders: ['Content-Type', 'Authorization'],  // Allowed headers
};

app.use(cors(corsOptions));


app.use(bodyParser.json());

app.use(express.static('public')); // If serving static files

app.use((req, res, next) => {
  res.setHeader("Content-Security-Policy", "default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline'; img-src 'self' data:;");
  next();
});

app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: true,
  cookie: {
    secure: false, // Use 'true' in production with HTTPS
    httpOnly: true, // Helps to prevent XSS attacks by making the cookie inaccessible via JavaScript
    sameSite: 'None', // Allows cross-site requests with the cookie
  }
}));

const spotifyApi = new SpotifyWebApi({
  clientId: process.env.SPOTIFY_CLIENT_ID,
  clientSecret: process.env.SPOTIFY_CLIENT_SECRET,
  redirectUri: process.env.REDIRECT_URI,
});

app.use(async (req, res, next) => {
  req.session = req.session || {};

  if (!req.session.accessToken || req.session.tokenExpirationTimestampMs <= Date.now()) {
    if (req.session.refreshToken) {
      spotifyApi.setRefreshToken(req.session.refreshToken);
      try {
        const data = await spotifyApi.refreshAccessToken();
        req.session.accessToken = data.body['access_token'];
        req.session.tokenExpirationTimestampMs = Date.now() + (data.body['expires_in'] * 1000);
        spotifyApi.setAccessToken(req.session.accessToken);
        next(); // Continue to the next middleware or route
      } catch (err) {
        console.error('Could not refresh access token', err);
        return res.status(401).json({ error: 'Unauthorized. Please log in again.' });
      }
    } else {
      return res.status(401).json({ error: 'Unauthorized. Please log in again.' });
    }
  } else {
    spotifyApi.setAccessToken(req.session.accessToken);
    next(); // Continue to the next middleware or route
  }
});

app.post('/login', async (req, res) => {
  const code = req.body.code;
  try {
    const data = await spotifyApi.authorizationCodeGrant(code);
    res.json({
      accessToken: data.body.access_token,
      refreshToken: data.body.refresh_token,
      expiresIn: data.body.expires_in,
    });
  } catch (err) {
    console.error('Spotify authorization error:', err);  // Log the detailed error
    res.status(401).json({ error: 'Unauthorized. Please log in again.' });
  }
});

app.post('/refresh', async (req, res) => {
  const refreshToken = req.body.refreshToken;

  try {
    spotifyApi.setRefreshToken(refreshToken);
    const data = await spotifyApi.refreshAccessToken();

    req.session.accessToken = data.body.access_token;
    req.session.tokenExpirationTimestampMs = Date.now() + (data.body['expires_in'] * 1000);

    res.json({
      accessToken: data.body.access_token,
      expiresIn: data.body.expires_in,
    });
  } catch (err) {
    console.error('Error in /refresh', err);
    res.status(400).json({ error: 'Failed to refresh token. Please log in again.' });
  }
});

app.get('/api/userSubscriptionLevel', async (req, res) => {
  try {
    const user = await spotifyApi.getMe();
    const product = user.body.product || 'free'; // Default to 'free' if no product found
    res.json({ product });
  } catch (error) {
    console.error('Error in /api/userSubscriptionLevel:', error);
    res.status(500).json({ error: 'Internal Server Error' });
  }
});

app.get('/api/getAccessToken', async (req, res) => {
  try {
    const accessToken = spotifyApi.getAccessToken();
    console.log('Access token:', accessToken);
    res.json({ accessToken });
  } catch (error) {
    console.error('Error in /api/getAccessToken', error);
    res.status(500).json({ error: 'Internal Server Error' });
  }
});


app.listen(port, () => {
  console.log(`Server listening on port ${port}`);
});

问题分析与解决方案

核心问题:全局认证中间件拦截登录/刷新请求

服务器中那个全局的app.use(async (req, res, next))中间件会拦截所有请求,包括/login和/refresh。但这两个路由是用于初始化认证的,此时用户session中还没有token信息,中间件会直接返回401,导致请求无法到达对应的处理逻辑,这就是useAuth无法连接服务器的根本原因。

修复步骤:

  1. 将全局认证中间件改为单独的中间件函数,只作用于需要认证的API路由:
// 替换原来的全局app.use认证中间件
const requireAuth = async (req, res, next) => {
  req.session = req.session || {};

  if (!req.session.accessToken || req.session.tokenExpirationTimestampMs <= Date.now()) {
    if (req.session.refreshToken) {
      spotifyApi.setRefreshToken(req.session.refreshToken);
      try {
        const data = await spotifyApi.refreshAccessToken();
        req.session.accessToken = data.body['access_token'];
        req.session.tokenExpirationTimestampMs = Date.now() + (data.body['expires_in'] * 1000);
        spotifyApi.setAccessToken(req.session.accessToken);
        next();
      } catch (err) {
        console.error('Could not refresh access token', err);
        return res.status(401).json({ error: 'Unauthorized. Please log in again.' });
      }
    } else {
      return res.status(401).json({ error: 'Unauthorized. Please log in again.' });
    }
  } else {
    spotifyApi.setAccessToken(req.session.accessToken);
    next();
  }
};

// 给需要认证的路由添加该中间件
app.get('/api/userSubscriptionLevel', requireAuth, async (req, res) => {
  // 原路由逻辑不变
});

app.get('/api/getAccessToken', requireAuth, async (req, res) => {
  // 原路由逻辑不变
});

其他优化调整

  1. Session Cookie适配开发环境:开发环境为HTTP协议,sameSite: 'None'必须配合secure: true才会被浏览器接受,因此修改session配置:
app.use(session({
  secret: process.env.SESSION_SECRET,
  resave: false,
  saveUninitialized: true,
  cookie: {
    secure: false,
    httpOnly: true,
    sameSite: 'Lax', // 改为Lax适配HTTP环境
  }
}));
  1. 登录时同步RefreshToken到Session:在/login路由中把refreshToken存入session,后续刷新操作无需客户端传递,更安全:
app.post('/login', async (req, res) => {
  const code = req.body.code;
  try {
    const data = await spotifyApi.authorizationCodeGrant(code);
    // 新增:将token存入session
    req.session.refreshToken = data.body.refresh_token;
    req.session.accessToken = data.body.access_token;
    req.session.tokenExpirationTimestampMs = Date.now() + (data.body.expires_in * 1000);
    
    res.json({
      accessToken: data.body.access_token,
      refreshToken: data.body.refresh_token,
      expiresIn: data.body.expires_in,
    });
  } catch (err) {
    console.error('Spotify authorization error:', err);
    res.status(401).json({ error: 'Unauthorized. Please log in again.' });
  }
});
  1. 客户端刷新请求简化:如果服务器已通过session管理refreshToken,客户端的refresh请求可以去掉body参数,依赖cookie传递session:
// 修改useAuth中的refresh请求
axios.post(`${API_URL}/refresh`, {}, { withCredentials: true })

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.19 06:05:55