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

部署环境下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;

排查与修复方案

  1. 调整Session中间件顺序,移除冗余依赖
    express-session无需依赖cookie-parser,当前cookie-parser在session之前可能导致cookie读取异常,直接移除app.use(cookieParser());。

  2. 配置Trust Proxy
    部署平台均为反向代理,需让Express信任代理,否则secure cookie会失效。在app.use(session(...))之前添加:

    app.set('trust proxy', 1);
    
  3. 修正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 // 按需启用
      }
      
  4. 前端请求携带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;
    };
    
  5. 移除不必要的延迟
    回调中的setTimeout可能导致Session保存与重定向不同步,直接重定向即可:

    // 替换原延迟代码
    console.log("saved")
    console.log(req.session)
    res.redirect(`${process.env.FRONT_URL}/auth/callback`);
    
  6. 检查环境变量一致性

    • 生产环境NODE_ENV必须设为production
    • FRONT_URL和SPOTIFY_REDIRECT_URI必须为HTTPS地址,避免混合内容拦截cookie

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.01 01:10:54