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的配置上。
修复方式有两种,按需选择即可:
- 推荐方案:在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); }) - 关闭Passport的会话再生配置(不推荐,会降低安全性)
可以在配置passport.session时关闭默认的会话再生逻辑,认证成功后不会重置会话:
这种方式虽然简单,但会失去会话固定攻击的防护能力,生产环境不建议使用。app.use(passport.session({ regenerateSession: false }));
内容的提问来源于stack exchange,提问作者Mu Desu
相关产品推荐
相关产品推荐

