如何解决NestJS+Prisma实现注销/刷新令牌的PrismaClientValidationError?
问题:NestJS+Prisma+JWT实现注销/刷新令牌时触发PrismaClientValidationError
问题现象
- 调用
this.prisma.user.updateMany()时,id参数被传入包含sub、email等字段的对象,触发"未知参数sub"的错误 - 调用
this.prisma.user.findUnique()时,id参数传入对象而非预期的整数类型,触发"参数id无效,期望Int,实际传入Object"的错误
注销函数错误日志
Invalid `this.prisma.user.updateMany()` invocation in ...auth.service.ts:45:32 42 } 43 44 async logout(userId: number) { → 45 await this.prisma.user.updateMany({ where: { id: { sub: 5, ~~~ email: "dummymail", iat: 1713191159, exp: 1713192059, ? equals?: Int | IntFieldRefInput, ? in?: Int[], ? notIn?: Int[], ? lt?: Int | IntFieldRefInput, ? lte?: Int | IntFieldRefInput, ? gt?: Int | IntFieldRefInput, ? gte?: Int | IntFieldRefInput, ? not?: Int | NestedIntFilter }, hashedRt: { not: null } }, data: { hashedRt: null } }) Unknown argument `sub`. Did you mean `in`? Available options are marked with ?. PrismaClientValidationError:
刷新令牌函数错误日志
Invalid `this.prisma.user.findUnique()` invocation in auth.service.ts:60:45 57 } 58 59 async refreshTokens(userId: number, rt: string) { → 60 const user = await this.prisma.user.findUnique({ where: { id: { sub: 5, email: "admin@gmail.com", iat: 1713191159, exp: 1713795959, refreshToken: "token is here" } ~~~~~~~~~~~~~~~~~~~~~~~ } }) Argument `id`: Invalid value provided. Expected Int, provided Object. PrismaClientValidationError:
auth.service.ts源码
import { ForbiddenException, Injectable } from '@nestjs/common'; import { PrismaService } from 'src/prisma/prisma.service'; import { AuthDto } from './dto/auth.dto'; import * as bcrypt from 'bcrypt'; import { Tokens } from './types'; import { JwtService } from '@nestjs/jwt'; @Injectable() export class AuthService { constructor(private prisma: PrismaService, private jwtService: JwtService){} async signupLocal(dto: AuthDto): Promise<Tokens> { const hash = await this.hashData(dto.password); const newUser = await this.prisma.user.create({ data:{ email: dto.email, hash, }, }); const tokens = await this.getTokens(newUser.id, newUser.email); await this.updateRtHash(newUser.id, tokens.refresh_token); return tokens; } async signinLocal(dto:AuthDto): Promise<Tokens> { const user = await this.prisma.user.findUnique({ where:{ email: dto.email, }, }); if (!user) throw new ForbiddenException("Oruulkue"); const passwordMatches = await bcrypt.compare(dto.password, user.hash); if (!passwordMatches) throw new ForbiddenException("Oruulkue"); const tokens = await this.getTokens(user.id, user.email); await this.updateRtHash(user.id, tokens.refresh_token); return tokens; } async logout(userId: number) { await this.prisma.user.updateMany({ where:{ id:userId, hashedRt: { not: null, } }, data: { hashedRt: null, } }); return true; } async refreshTokens(userId: number, rt: string) { const user = await this.prisma.user.findUnique({// where: { id: userId, }, }); if(!user || !user.hashedRt) throw new ForbiddenException("Oruulkue") const rtMatches = await bcrypt.compare(rt, user.hashedRt) if(!rtMatches) throw new ForbiddenException("Oruulkue") const tokens = await this.getTokens(user.id, user.email); await this.updateRtHash(user.id, tokens.refresh_token); return tokens; } async updateRtHash(userId: number, rt: string){ const hash = await this.hashData(rt) await this.prisma.user.update({ where: { id: userId, }, data: { hashedRt: hash, }, }); } hashData(data: string){ return bcrypt.hash(data, 10); } async getTokens(userId: number, email: string){ const [at, rt] = await Promise.all([ this.jwtService.signAsync( { sub: userId, email, }, { secret: 'at-secret', expiresIn: 60*15, }, ), this.jwtService.signAsync( { sub: userId, email, }, { secret: 'rt-secret', expiresIn: 60*60*24*7, }, ), ]); return{ access_token: at, refresh_token: rt, }; } }
问题原因
你的AuthService中logout和refreshTokens方法定义的userId参数类型是number,但实际调用时传入的是JWT解析后的完整payload对象(包含sub、email、iat等字段)。这说明控制器层在调用这两个方法时,错误地把整个JWT payload当成了userId参数,而没有提取payload中的sub字段(即实际的用户ID)。
解决方案
1. 修正控制器层的参数传递
假设你使用了JWT守卫,在控制器中需要从请求对象的user属性(JWT解析后的payload)里提取sub字段,转成数字后再传入服务层方法:
// 示例AuthController代码 import { Controller, Post, UseGuards, Request, Headers } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { AuthService } from './auth.service'; @Controller('auth') export class AuthController { constructor(private readonly authService: AuthService) {} // 注销接口 @UseGuards(AuthGuard('jwt')) @Post('logout') async logout(@Request() req) { // 提取payload中的sub,转成整数类型 const userId = parseInt(req.user.sub, 10); return this.authService.logout(userId); } // 刷新令牌接口 @UseGuards(AuthGuard('jwt-refresh')) @Post('refresh') async refreshTokens(@Request() req, @Headers('authorization') authHeader: string) { const userId = parseInt(req.user.sub, 10); // 从Authorization头中提取刷新令牌(格式:Bearer <token>) const refreshToken = authHeader?.split(' ')[1]; if (!refreshToken) throw new ForbiddenException('缺少刷新令牌'); return this.authService.refreshTokens(userId, refreshToken); } }
2. 增加类型安全(可选)
为JWT payload创建类型定义,避免后续参数混淆:
// src/auth/types/jwt-payload.ts export interface JwtPayload { sub: string; email: string; iat?: number; exp?: number; }
然后在控制器中使用该类型:
import { JwtPayload } from './types/jwt-payload'; // ... async logout(@Request() req: { user: JwtPayload }) { const userId = parseInt(req.user.sub, 10); return this.authService.logout(userId); }
3. 服务层参数校验(可选)
在AuthService的方法中添加参数类型校验,提前拦截错误:
async logout(userId: number) { if (typeof userId !== 'number' || isNaN(userId)) { throw new ForbiddenException('无效的用户ID'); } // 原逻辑代码... } async refreshTokens(userId: number, rt: string) { if (typeof userId !== 'number' || isNaN(userId)) { throw new ForbiddenException('无效的用户ID'); } if (!rt || typeof rt !== 'string') { throw new ForbiddenException('无效的刷新令牌'); } // 原逻辑代码... }
内容的提问来源于stack exchange,提问作者Khuslensaikhan Battsetseg
相关产品推荐
相关产品推荐

