使用NestJS+Passport+TypeORM构建Discord机器人仪表盘API时的重复会话条目及会话支持缺失问题
兄弟,我一看你这个错误栈就知道是会话存储和实体设计踩了典型的坑——我之前做Discord OAuth2集成+NestJS会话管理的时候也栽过类似的跟头,来一步步给你捋清楚解决:
首先拆解核心错误:Duplicate entry '[access_token]' for key 'sessions.PRIMARY'
这个问题的根因非常明确:你大概率把Discord返回的access_token设成了sessions表的主键,但这完全违背了会话管理的逻辑——Discord的access_token是给你调用Discord API用的,不是服务端会话的唯一标识;就算Discord的token是唯一的,Passport的会话逻辑也会在用户重复登录时尝试插入新会话,直接导致主键冲突。
第一步:紧急修复Session实体的主键设计
立刻把会话表的主键改成服务端自己生成的唯一标识(UUID或者自增ID都可以),绝对不要依赖第三方平台的token。给你一个标准的Session实体模板:
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm'; @Entity('sessions') export class Session { // 用UUID生成服务端专属的会话主键,全局唯一 @PrimaryGeneratedColumn('uuid') id: string; // 存储Passport自动生成的会话ID(对应req.sessionID),必须唯一 @Column({ unique: true, length: 255 }) sessionId: string; // 存储会话数据(比如用户ID、Discord的token等,要JSON序列化后存) @Column({ type: 'text' }) sessionData: string; // 会话过期时间,用来自动清理无效会话 @Column({ type: 'datetime' }) expiresAt: Date; @CreateDateColumn() createdAt: Date; @UpdateDateColumn() updatedAt: Date; }
第二步:用官方适配的会话存储替代手动逻辑
你肯定是在手动处理会话的插入操作,这很容易出问题。直接用connect-typeorm这个工具(NestJS生态里官方适配TypeORM的会话存储),它会自动帮你处理会话的创建、更新、过期清理,从根源上避免重复插入的问题。
给你一套完整的配置示例:
- 在
AppModule里注册会话存储:
import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { Connection } from 'typeorm'; import { TypeORMStore } from 'connect-typeorm'; import * as session from 'express-session'; import { PassportModule } from '@nestjs/passport'; import { Session } from './entities/session.entity'; @Module({ imports: [ TypeOrmModule.forFeature([Session]), PassportModule.register({ session: true }), // 开启Passport的会话支持 ], providers: [ { provide: 'SESSION_STORE', useFactory: (connection: Connection) => { const store = new TypeORMStore({ cleanupLimit: 2, // 每次请求清理2条过期会话 ttl: 86400 * 7, // 会话有效期7天(单位:秒) }); store.setRepository(connection.getRepository(Session)); return store; }, inject: [Connection], }, ], exports: ['SESSION_STORE'], }) export class AppModule {}
- 在
main.ts里挂载会话和Passport中间件:
import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module'; import * as session from 'express-session'; import * as passport from 'passport'; async function bootstrap() { const app = await NestFactory.create(AppModule); const sessionStore = app.get('SESSION_STORE'); // 挂载会话中间件,关键配置要注意 app.use( session({ secret: process.env.SESSION_SECRET, // 一定要用环境变量存这个密钥 resave: false, // 不要重复保存未修改的会话 saveUninitialized: false, // 不给未登录的请求创建空会话,避免无效数据 store: sessionStore, cookie: { secure: process.env.NODE_ENV === 'production', // 生产环境开启HTTPS-only httpOnly: true, // 禁止前端JS访问cookie,防XSS maxAge: 86400 * 7 * 1000, // 和ttl对应,单位:毫秒 }, }), ); // 挂载Passport的会话中间件 app.use(passport.initialize()); app.use(passport.session()); await app.listen(3000); } bootstrap();
第三步:修复Passport的序列化/反序列化逻辑
很多人在这里偷懒,直接把整个用户对象或者Discord的token序列化到会话里,不仅会导致会话数据过大,还可能隐含安全问题。正确的做法是只序列化用户ID,反序列化时再通过ID查询用户:
import { Injectable } from '@nestjs/common'; import { PassportSerializer } from '@nestjs/passport'; import { UserService } from './user.service'; @Injectable() export class SessionSerializer extends PassportSerializer { constructor(private readonly userService: UserService) { super(); } // 序列化:只把用户ID存到会话里 serializeUser(user: any, done: (err: Error | null, userId: number) => void): void { done(null, user.id); } // 反序列化:从会话里的ID查询完整用户对象 async deserializeUser(userId: number, done: (err: Error | null, user: any) => void): Promise<void> { try { const user = await this.userService.findById(userId); done(null, user); } catch (err) { done(err, null); } } }
额外优化:限制单用户仅一个活跃会话(可选)
如果你希望同一个用户只能有一个活跃会话(比如防止多端登录),可以在登录成功后,先删除该用户的所有旧会话,再创建新的:
// 在你的AuthService的登录回调方法里 async afterLogin(req: Request) { const user = req.user; const sessionRepo = this.connection.getRepository(Session); // 模糊匹配会话数据里包含当前用户ID的旧会话 const oldSessions = await sessionRepo .createQueryBuilder('session') .where('session.sessionData LIKE :userId', { userId: `%"userId":${user.id}%` }) .getMany(); if (oldSessions.length > 0) { await sessionRepo.remove(oldSessions); } return { message: '登录成功', user }; }
按照上面的步骤改完,你的重复会话主键错误应该就完全解决了,而且会话管理的逻辑也会更规范安全。
备注:内容来源于stack exchange,提问作者Meenic

