自定义Redis Session Store的set方法为何被触发两次?
登录会话创建时RedisStore的set方法被调用两次问题排查
问题场景
使用NestJS、Passport、express-session开发Web应用,基于Redis(ioredis)实现自定义会话管理时,发现登录过程中自定义RedisStore的set方法被触发两次,其他方法仅触发一次。已验证Passport的serializeUser、deserializeUser方法,以及LocalAuthGuard的canActivate方法均只执行一次。
控制台日志触发顺序:
- get
- destroy
- set
- [Function: serialized]
- set
LocalAuthGuard实现
import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class LocalAuthGuard extends AuthGuard('local') { async canActivate(context: ExecutionContext) { const result = (await super.canActivate(context)) as boolean; const request = context.switchToHttp().getRequest(); await super.logIn(request); return result; } }
express-session配置
app.use( session({ name: 'dekada_session', store: new RedisStore(redisService), secret: 'keyboard cat', resave: false, saveUninitialized: false, rolling: false, cookie: { maxAge: 3600000, secure: false, sameSite: 'lax', httpOnly: true, }, }), );
自定义RedisStore的set方法
set(sid: string, session: SessionData, callback?: (error?: any) => void): void { console.log('set'); const ttl = 3600; this.redisService.getClient().setex(`sess:${sid}`, ttl, JSON.stringify(session), (error) => { callback(error); }); }
根因分析
- Passport登录流程的冗余会话更新:
super.canActivate(context)执行Local策略验证后,Passport内部已完成会话初始化并调用logIn序列化用户;手动再次调用super.logIn(request)会触发第二次会话内容更新,导致set方法被调用两次。 - express-session的状态变更触发逻辑:
saveUninitialized: false配置下,会话从无到有、再到用户信息写入的两次状态变更,都会触发存储操作。
解决方案
方案1:移除冗余的logIn调用
AuthGuard('local')的默认实现已在验证通过后自动执行logIn流程,无需手动调用。修改后的LocalAuthGuard:
import { ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; @Injectable() export class LocalAuthGuard extends AuthGuard('local') { async canActivate(context: ExecutionContext) { return super.canActivate(context) as Promise<boolean>; } }
方案2:给RedisStore的set方法添加幂等校验
若需保留现有Guard逻辑,可在set方法中对比会话内容,仅当内容变化时执行Redis写入:
set(sid: string, session: SessionData, callback?: (error?: any) => void): void { const sessionStr = JSON.stringify(session); this.redisService.getClient().get(`sess:${sid}`, (err, existingSession) => { if (err) return callback(err); // 会话内容无变化时直接返回,避免重复写入 if (existingSession === sessionStr) return callback(); const ttl = 3600; this.redisService.getClient().setex(`sess:${sid}`, ttl, sessionStr, callback); }); }
内容的提问来源于stack exchange,提问作者Nazar Duma
相关产品推荐
相关产品推荐

