部署环境下Express Session不持久化问题求助
问题:部署后重定向SessionID不一致,会话无法持久化
本地运行正常,但部署后重定向回应用的SessionID与登录成功时不一致,导致前后端无法通信。登录流程中跳转到Spotify登录正常,但重定向后会话未持久化,页面持续加载;后端日志显示登录成功、会话已保存,但重定向后的SessionID不同。
已尝试的方案:自定义域名、切换Render/Vercel部署平台、Redis存储会话、使用cookie-parser、调整静态文件、默认内存Session、配置trust proxy,均未解决问题。
后端index.js代码
const express = require('express'); const SpotifyWebApi = require('spotify-web-api-node'); require('dotenv').config(); const cors = require('cors'); const app = express(); const path = require('path'); const session = require('express-session'); const cookieParser = require('cookie-parser'); // app.use(session({ // secret: process.env.SESSION_SECRET, // resave: false, // saveUninitialized: true, // cookie: { // secure: process.env.NODE_ENV === "production", // sameSite: process.env.NODE_ENV === "production" ? 'None' : 'Lax' // //domain: '.onrender.com' // } // })); app.use(cookieParser()); app.use(cors({ origin: process.env.FRONT_URL, // adjust if your frontend port is different credentials: true })); //Redis setup const { createClient } = require('redis'); const redisClient = createClient({ url: 'rediss://red-cmcp61f109ks73921rng:WbyyCCp36c1oIHIO5irK8nwAN0nlyU5P@ohio-redis.render.com:6379', legacyMode: true// Your Redis URL }); redisClient.connect().catch(console.error); const RedisStore = require('connect-redis')(session); // Session setup with Redis app.use(session({ store: new RedisStore({ client: redisClient }), secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, // You can set this to false to comply with laws that require permission before setting a cookie name: 'HaromonyCookie', cookie: { httpOnly: true, sameSite: process.env.NODE_ENV === "production" ? 'None' : 'Lax', secure: process.env.NODE_ENV === "production", domain: process.env.DOMAIN // Should be true if using HTTPS } // Add other configurations as needed })); redisClient.on('connect', () => console.log('Redis client connected')); redisClient.on('error', (err) => console.log('Redis Client Error', err)); // Serve static files from the React app app.use(express.static(path.join(__dirname, '../../client/build'))); const spotifyApi = new SpotifyWebApi({ clientId: process.env.SPOTIFY_CLIENT_ID, clientSecret: process.env.SPOTIFY_CLIENT_SECRET, redirectUri: process.env.SPOTIFY_REDIRECT_URI }); // have to rewrite this to because I am not storing token in spotifyApi object app.post('/refresh', (req, res) => { const refreshToken = req.session.user.refreshToken; spotifyApi.setRefreshToken(refreshToken); spotifyApi.refreshAccessToken().then(data => { res.json({ accessToken: data.body['access_token'], expiresIn: data.body['expires_in'] }); }).catch(error => { console.error('Error refreshing access token:', error); res.status(500).json({ error: 'Error refreshing access token' }); }); } ); app.get('/api/token', (req, res) => { console.log("token endpoint hit") console.log('Session ID:', req.sessionID); console.log('Cookies sent:', req.cookies); if (req.session.user && req.session.user.accessToken) { console.log("sending token") res.json({ accessToken: req.session.user.accessToken }); } else { res.status(401).json({ error: 'Unauthorized: No active session' }); } }); // Redirect users to this endpoint for Spotify login, then redirect them to the /callback endpoint app.get('/login', (req, res) => { console.log("STEP 1") console.log("--------------------") console.log("login enpoint hit") console.log("--------------------") const scopes = ['user-read-private', 'user-read-email', 'playlist-read-private', 'playlist-modify-private', 'playlist-modify-public']; res.redirect(spotifyApi.createAuthorizeURL(scopes)); }); app.post('/logout', (req, res) => { req.session.destroy((err) => { if (err) { res.status(500).send('Could not log out, please try again'); } else { res.clearCookie('connect.sid', { path: '/' }); res.status(200).send('Logged out'); } }); }); // Spotify will redirect users to this endpoint after login app.get('/callback', (req, res) => { console.log("STEP 2") console.log("--------------------") console.log("callback endpoint hit") console.log("--------------------") res.set('Cache-Control', 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0'); res.set('Pragma', 'no-cache'); // HTTP 1.0. res.set('Expires', '0'); const error = req.query.error; const code = req.query.code; if (error) { console.error('Callback Error:', error); res.send(`Callback Error: ${error}`); return; } spotifyApi.authorizationCodeGrant(code).then(data => { const accessToken = data.body['access_token']; const refreshToken = data.body['refresh_token']; const expiresIn = data.body['expires_in']; const expiryTime = new Date().getTime() + expiresIn * 1000; console.log('Session ID:', req.sessionID); console.log('Cookies received:', req.cookies); req.session.user = { accessToken: accessToken, refreshToken: refreshToken, expiresIn: expiresIn, expiryTime: expiryTime }; req.session.save(err => { if (err) { console.error('Error saving session:', err); res.send(`Error saving session: ${err}`); } else { console.log("saved") console.log(req.session) // In your Express route after successful authentication setTimeout(() => { console.log('Redirecting to front URL'); res.redirect(`${process.env.FRONT_URL}/auth/callback`); }, 1000); } }); }).catch(error => { console.error('Error getting Tokens:', error); res.send(`Error getting Tokens: ${error}`); }); }); app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../client/build', 'index.html')); }); const port = process.env.PORT || 3001; app.listen(port, () => { console.log(`Listening at http://localhost:${port}`); });
前端AuthCallback组件代码
import React, { useEffect, useContext } from 'react'; import { AuthContext } from '../contexts/authContext'; const AuthCallback = () => { const { fetchTokens } = useContext(AuthContext); useEffect(() => { const fetchAndSetTokens = async () => { console.log("AuthCallback reached and fetching tokens...") const tokens = await fetchTokens(); if (tokens) { localStorage.setItem('accessToken', tokens); localStorage.setItem('isLoggedIn', true); // Redirect to home or other page after successful login window.location.href = `${process.env.REACT_APP_FRONT}/home` || "localhost:3000/home"; } }; fetchAndSetTokens(); }, [fetchTokens]); return <div>Loading...</div>; // Or some loading indicator }; export default AuthCallback;
排查与修复方案
调整Session中间件顺序,移除冗余依赖
express-session无需依赖cookie-parser,当前cookie-parser在session之前可能导致cookie读取异常,直接移除app.use(cookieParser());。配置Trust Proxy
部署平台均为反向代理,需让Express信任代理,否则securecookie会失效。在app.use(session(...))之前添加:app.set('trust proxy', 1);修正Cookie配置
- 移除或修正
domain配置:若使用平台默认域名,无需手动设置domain;若用自定义域名,设置为.yourdomain.com(带前缀点适配子域名)。 - 添加
path: '/'确保所有路径都能携带cookie:cookie: { httpOnly: true, sameSite: process.env.NODE_ENV === "production" ? 'None' : 'Lax', secure: process.env.NODE_ENV === "production", path: '/', // domain: process.env.DOMAIN // 按需启用 }
- 移除或修正
前端请求携带Credentials
确保fetchTokens函数中设置credentials: 'include',否则浏览器不会传递Session cookie:// 示例fetchTokens实现 const fetchTokens = async () => { const res = await fetch(`${process.env.REACT_APP_BACKEND_URL}/api/token`, { credentials: 'include' }); if (res.ok) { const data = await res.json(); return data.accessToken; } return null; };移除不必要的延迟
回调中的setTimeout可能导致Session保存与重定向不同步,直接重定向即可:// 替换原延迟代码 console.log("saved") console.log(req.session) res.redirect(`${process.env.FRONT_URL}/auth/callback`);检查环境变量一致性
- 生产环境
NODE_ENV必须设为production FRONT_URL和SPOTIFY_REDIRECT_URI必须为HTTPS地址,避免混合内容拦截cookie
- 生产环境
内容的提问来源于stack exchange,提问作者SHIN003
相关产品推荐
相关产品推荐

