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

Express配合Passport登录时req.session重置 回跳地址丢失问题

问题描述

实现站点登录拦截功能时,需要校验用户登录状态:未登录用户尝试访问发布新内容等权限受控页面时,会被重定向到登录页,此时将用户原本要访问的目标URL存入session,等用户登录成功后自动跳转回该地址。项目使用express-session做会话存储,passport-local实现本地账号认证。

当前异常现象:目标URL确实可以成功写入session,但用户提交登录POST请求完成认证时,req.session会被重置,之前存储的returnTo回跳地址被清空。测试其他POST接口确认仅登录POST请求会触发该问题,在登录接口内打印req.session可见会话状态异常,存在req.session为undefined、会话反复重置的现象。

已尝试的排查操作

  • 移除cookie自定义配置
  • 切换cookie secure属性为true/false
  • 修改session secret值
  • 切换resave配置为true/false
  • 切换saveUninitialized配置为true/false
  • 修改session名称

以上操作均无法解决问题,初步判断问题与Passport相关,但不清楚具体修复方式。


相关代码实现

登录状态校验、目标URL存储中间件

module.exports.isLoggedIn = (req, res, next) => {
    if (!req.isAuthenticated()) {
        req.session.returnTo = req.originalUrl
        console.log(req.session.returnTo)
        req.flash('error', 'You must be signed in first!');
        return res.redirect('/login');
    }
    next();
}

module.exports.isNotLoggedIn = (req, res, next) => {
    if (req.isAuthenticated()) {
        return res.redirect('/campgrounds')
    }
    next();
}

登录相关路由实现

const express = require('express');
const router = express.Router();
const passport = require('passport');
const catchAsync = require('../utils/catchAsync');
const User = require('../models/user');
const { isNotLoggedIn } = require('../middleware')

router.get('/register', isNotLoggedIn, (req, res) => {
    res.render('users/register');
});

router.post('/register', isNotLoggedIn, catchAsync(async (req, res, next) => {
    try {
        const { email, username, password } = req.body;
        const user = new User({ email, username });
        const registeredUser = await User.register(user, password);
        req.login(registeredUser, err => {
            if (err) return next(err);
            req.flash('success', 'Welcome to Yelp Camp!');
            res.redirect('/campgrounds');
        })
    } catch (e) {
        req.flash('error', e.message);
        res.redirect('register');
    }
}));

router.get('/login', isNotLoggedIn, (req, res) => {
    console.log(req.session)
    res.render('users/login');
})

router.post('/login', isNotLoggedIn, passport.authenticate('local', { failureFlash: true, failureRedirect: '/login' }), (req, res) => {
    req.flash('success', 'welcome back!');
    const returnTo = req.session.returnTo || '/campgrounds';
    console.log('this thsisdifsdk')
    console.log(req.session) // 此处req.session被重置,无法获取之前存储的回跳地址
    res.redirect(returnTo);
})

router.get('/logout', (req, res) => {
    req.logout((err) => {
        if (err) { return next(err); }
        req.flash('success', "Goodbye!");
        res.redirect('/campgrounds');
    });
})

module.exports = router;

app.js核心配置

const express = require('express')
const path = require('path')
const mongoose = require('mongoose')
const ejsMate = require('ejs-mate');
const session = require('express-session')
const flash = require('connect-flash')
const ExpressError = require('./utils/ExpressError')
const methodOverride = require('method-override')
const passport = require('passport');
const LocalStrategy = require('passport-local')
const User = require('./models/user')

const userRoutes = require('./routes/user')
const campgroundRoutes = require('./routes/campgrounds')
const reviewRoutes = require('./routes/reviews');

mongoose.connect('mongodb://localhost:27017/yelp-camp');

const db = mongoose.connection // 数据库连接快捷引用
db.on('error', console.error.bind(console, 'connection error:'));
db.once('open', () => {
    console.log('Database Connected');
})

const app = express();

app.engine('ejs', ejsMate)
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'))

app.use(express.urlencoded({ extended: true }));
app.use(methodOverride('_method'));
app.use(express.static(path.join(__dirname, 'public')))

const sessionConfig = {
    secret: 'meowmeowthecatsounduwu',
    resave: false,
    saveUninitialized: true,
    cookie: {
        httpOnly: true,
        expires: Date.now() + 1000 * 60 * 60 * 24 * 7,
        maxAge: 1000 * 60 * 60 * 24 * 7
    }
}

app.use(session(sessionConfig))
app.use(flash());

app.use(passport.initialize());
app.use(passport.session());
passport.use(new LocalStrategy(User.authenticate()));

passport.serializeUser(User.serializeUser());
passport.deserializeUser(User.deserializeUser());

app.use((req, res, next) => {
    console.log(req.session)
    res.locals.currentUser = req.user;
    res.locals.success = req.flash('success');
    res.locals.error = req.flash('error');
    next();
})

app.use('/', userRoutes);

app.listen(3000, () => {
    console.log('Serving on port 3000')
})

问题原因与修复方案

这个问题是Passport v0.6及以上版本的默认安全机制导致的:用户认证成功后Passport会自动重新生成新会话、废弃旧会话,用来防御会话固定攻击,旧会话上挂载的自定义字段不会自动迁移到新会话,所以之前存在session里的returnTo地址会被清空。之前调整session、cookie相关配置都不生效,就是因为问题根本不在express-session的配置上。

修复方式有两种,按需选择即可:

  1. 推荐方案:在Passport执行认证逻辑前提前暂存回跳地址
    在passport.authenticate中间件前加一层处理逻辑,提前把session里的returnTo值取出来暂存到res.locals上,等认证完成后直接从暂存位置取值即可,不需要等会话再生后再去旧session里找。
    登录POST路由修改示例:
    router.post('/login', isNotLoggedIn, (req, res, next) => {
        // 提前暂存回跳地址,避免passport再生会话时丢失
        res.locals.returnTo = req.session.returnTo;
        next();
    }, passport.authenticate('local', { failureFlash: true, failureRedirect: '/login' }), (req, res) => {
        req.flash('success', 'welcome back!');
        const returnTo = res.locals.returnTo || '/campgrounds';
        // 跳转后清除session里的冗余字段
        delete req.session.returnTo;
        res.redirect(returnTo);
    })
    
  2. 关闭Passport的会话再生配置(不推荐,会降低安全性)
    可以在配置passport.session时关闭默认的会话再生逻辑,认证成功后不会重置会话:
    app.use(passport.session({ regenerateSession: false }));
    
    这种方式虽然简单,但会失去会话固定攻击的防护能力,生产环境不建议使用。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.29 17:27:41