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

如何解决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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.26 03:14:58