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无法连接服务器的根本原因。
修复步骤:
- 将全局认证中间件改为单独的中间件函数,只作用于需要认证的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) => { // 原路由逻辑不变 });
其他优化调整
- 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环境 } }));
- 登录时同步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.' }); } });
- 客户端刷新请求简化:如果服务器已通过session管理refreshToken,客户端的refresh请求可以去掉body参数,依赖cookie传递session:
// 修改useAuth中的refresh请求 axios.post(`${API_URL}/refresh`, {}, { withCredentials: true })
内容的提问来源于stack exchange,提问作者macbas
相关产品推荐
相关产品推荐

