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无法处理这种结构,从而抛出错误。
具体触发点:
CustomStrategy的validate方法返回了完整的用户实体,Passport会将该对象挂载到req.user上,后续控制器/服务在返回时触发序列化。generateJWTByToken方法中返回的userFind也是完整的实体对象,同样存在循环引用。
解决步骤
返回纯数据对象,剥离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'] });修改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 }; }优化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 }; }启用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
相关产品推荐
相关产品推荐

