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

NestJS自定义Passport策略实现遇JSON循环结构转换错误求助

NestJS自定义Passport策略:循环结构转JSON错误排查

错误信息

[Nest] 25976  - 16/03/2023 23:11:17   ERROR [ExceptionsHandler] Converting circular structure to JSON
    --> starting at object with constructor 'Socket'
    |     property 'parser' -> object with constructor 'HTTPParser'
    --- property 'socket' closes the circle

TypeError: Converting circular structure to JSON
    --> starting at object with constructor 'Socket'
    |     property 'parser' -> object with constructor 'HTTPParser'
    --- property 'socket' closes the circle
    at JSON.stringify (<anonymous>)
    at SelectQueryBuilder.<anonymous> (src\query-builder\SelectQueryBuilder.ts:2121:57)
    at step (\node_modules\tslib\tslib.js:144:27)
    at Object.next (\node_modules\tslib\tslib.js:125:57)
    at \tslib\tslib.js:118:75
    at new Promise (<anonymous>)
    at __awaiter (\node_modules\tslib\tslib.js:114:16)
    at SelectQueryBuilder.loadRawResults (\node_modules\typeorm\query-builder\SelectQueryBuilder.js:1639:38)
    at SelectQueryBuilder.<anonymous> (\src\query-builder\SelectQueryBuilder.ts:2050:37)
    at step (\node_modules\tslib\tslib.js:144:27)

已实现的代码步骤

1. 安装依赖

npm install passport-custom

2. 创建自定义策略类

nest g cl modules/auth/strategies/custom.strategy --flat --no-spec

自定义策略类代码

import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { Strategy } from 'passport-custom';

import { AuthService } from '../services/auth.service';

@Injectable()
export class CustomStrategy extends PassportStrategy(Strategy, 'custom') {

    constructor(private authService: AuthService) {
        super();
    }
    
    async validate(tokenRegister: string) {
        const user = await this.authService.validateUserByToken(tokenRegister);
        if (!user) {
            throw new UnauthorizedException('No permitido');
        }

        return user;
    }
}

Auth控制器代码

@UseGuards(AuthGuard('custom'))
@Post('loginByToken')
loginByToken(@Req() req: Request) {
    const user = req.user as User;
    if (user.image)
        user.image = bufferToString(user.image);

    return this.authService.generateJWTByToken(user);
}

Auth模块代码

@Module({
  imports: [
    UsersModule,
    PassportModule,
    JwtModule.registerAsync({
      inject: [config.KEY],
      useFactory: (configService: ConfigType<typeof config>) => {
        return {
          secret: configService.jwtSecret,
          signOptions: {
            expiresIn: '365d', //12h
          },
        };
      },
    }),
  ],
  providers: [AuthService, LocalStrategy, JwtStrategy, CustomStrategy],
  controllers: [AuthController],
})
export class AuthModule {}

Auth服务代码

async validateUserByToken(tokenRegister: string) {
    const user = await this.userService.findOneByToken(tokenRegister);

    if (user) {
        if (!user.isAllowed)
            throw new ConflictException('Usuario restringido.');

        return user;
    }

    return null;
}

async generateJWTByToken(user: User) {
    const payload: PayloadToken = { role: user.role, sub: user.id };
    const userFind = await this.userService.findOneByToken(user.tokenRegister);

    return {
          access_token: this.jwtService.sign(payload),
          userFind,
    };
}

User服务代码

async findOneByToken(token: string) {
    const user = await this.userRepo.findOne({
        where: {
            tokenRegister: token
        }
    });

    if (!user)
        throw new NotFoundException(`Usuario no encontrado.`);
    
    if (!user.isAllowed)
        throw new NotFoundException(`Usuario #${user.id} no permitido.`);
        
    return this.userData(user);
}

问题原因及解决方案

问题根源

错误是因为直接返回了带有循环引用的TypeORM实体对象。TypeORM实体实例包含内部属性(如manager、connection、关联的Socket对象等),这些属性形成了循环引用,当Nest尝试将响应序列化为JSON时,JSON.stringify无法处理这种结构,从而抛出错误。

具体触发点:

  1. CustomStrategy的validate方法返回了完整的用户实体,Passport会将该对象挂载到req.user上,后续控制器/服务在返回时触发序列化。
  2. generateJWTByToken方法中返回的userFind也是完整的实体对象,同样存在循环引用。

解决步骤

  1. 返回纯数据对象,剥离TypeORM实体的内部属性
    修改userData方法,确保它返回一个不包含TypeORM内部属性的纯对象,比如手动提取业务需要的字段:

    userData(user: User) {
        return {
            id: user.id,
            username: user.username,
            email: user.email,
            role: user.role,
            isAllowed: user.isAllowed,
            image: user.image,
            tokenRegister: user.tokenRegister
        };
    }
    

    或者在查询时使用select指定需要的字段,避免加载不必要的属性:

    // UserService.findOneByToken修改查询语句
    const user = await this.userRepo.findOne({
        where: { tokenRegister: token },
        select: ['id', 'username', 'email', 'role', 'isAllowed', 'image', 'tokenRegister']
    });
    
  2. 修改CustomStrategy的validate方法
    确保返回的是纯数据对象而非实体实例,避免Passport挂载带循环引用的对象到req.user:

    async validate(tokenRegister: string) {
        const user = await this.authService.validateUserByToken(tokenRegister);
        if (!user) {
            throw new UnauthorizedException('No permitido');
        }
        // 只保留验证所需的核心字段
        return {
            id: user.id,
            role: user.role,
            tokenRegister: user.tokenRegister
        };
    }
    
  3. 优化generateJWTByToken方法
    不需要再次查询用户(req.user已包含用户信息),直接使用传入的用户数据构造响应,同时确保返回的用户数据是纯对象:

    async generateJWTByToken(user: User) {
        const payload: PayloadToken = { role: user.role, sub: user.id };
        const userResponse = {
            id: user.id,
            username: user.username,
            email: user.email,
            role: user.role,
            isAllowed: user.isAllowed,
            image: user.image
        };
        return {
            access_token: this.jwtService.sign(payload),
            user: userResponse
        };
    }
    
  4. 启用class-transformer自动序列化(可选)
    定义UserDTO并使用@Exclude()装饰器排除不需要的字段,配合ClassSerializerInterceptor自动处理序列化:

    // user.dto.ts
    import { Exclude } from 'class-transformer';
    
    export class UserDTO {
        id: number;
        username: string;
        email: string;
        role: string;
        isAllowed: boolean;
        image: string;
    
        @Exclude()
        tokenRegister: string;
        @Exclude()
        manager: any;
        @Exclude()
        connection: any;
    }
    

    在控制器中启用拦截器:

    @UseInterceptors(ClassSerializerInterceptor)
    @Controller('auth')
    export class AuthController {}
    

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.27 16:27:56