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

使用NestJS+Passport+TypeORM构建Discord机器人仪表盘API时的重复会话条目及会话支持缺失问题

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的会话存储),它会自动帮你处理会话的创建、更新、过期清理,从根源上避免重复插入的问题。

给你一套完整的配置示例:

  1. 在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 {}
  1. 在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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.16 07:08:05