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

使用bcrypt时旧refreshToken匹配新哈希的异常问题排查

bcrypt对比旧refreshToken与新哈希值意外返回true的问题

问题描述

我在使用bcrypt对比明文refreshToken和数据库中存储的哈希值时遇到异常:调用refreshToken()方法时,明明数据库里存的是最新的refreshToken哈希值,传入的参数是旧的refreshToken明文,但bcrypt.compare()却返回true。

通过console.log可以确认:

  • user.authInfo.refreshToken是数据库中最新更新的哈希值
  • refreshToken参数是已被替换的旧token明文

但用完全无关的字符串(比如tototititata)对比新哈希时,bcrypt.compare()能正常返回false,只有旧token对比新哈希时出问题。

相关代码

refreshToken方法

async refreshToken(userId: number, refreshToken: string): Promise<any> {
    const user: User = await this.userService.findByIdWithAuthInfo(userId);
    
    if (!user?.authInfo?.refreshToken) {
      throw new ImATeapotException('You cannot do that.');
    }

    console.log(refreshToken);
    console.log(user.authInfo.refreshToken);
    const isMatch: boolean = await bcrypt.compare(refreshToken, user.authInfo.refreshToken);

    if (!isMatch) {
      throw new ImATeapotException('You cannot do that.');
    }

    return this.generateTokenPairAndUpdateAuthInfo(user);
}

哈希与token生成相关方法

private async hashData(data: string): Promise<string> {
    return await bcrypt.hash(
      data,
      this.config.get<number>("security.saltRounds")
    );
}

private async generateToken(id: number, username: string, refresh: boolean): Promise<string> {
    return this.jwtService.signAsync(
      {
        sub: id,
        username: username,
      },
      {
        secret: this.config.get<string>(
          refresh ? "security.jwtRefreshSecret" : "security.jwtSecret"
        ),
        expiresIn: this.config.get<number>(
          refresh ? "security.refreshTokenDuration" : "security.accessTokenDuration"
        ),
      }
    );
}

private async generateTokenPairAndUpdateAuthInfo(user: User): Promise<any> {
    const accessToken = await this.generateToken(user.id, user.username, false);
    const refreshToken = await this.generateToken(user.id, user.username, true);

    await this.authInfoService.updateTokens(
      user.authInfoId,
      await this.hashData(accessToken),
      await this.hashData(refreshToken),
    );

    return { accessToken, refreshToken };
}

authInfoService.updateTokens()仅负责调用仓库方法更新数据库,且数据库中的值已确认是正确更新的。

核心疑问

为什么已被替换的旧refreshToken明文,和最新的refreshToken哈希值对比时,bcrypt.compare()会返回true?这会不会是缓存或者异步逻辑导致的问题?


排查方向与可能原因

1. 检查JWT refreshToken的生成逻辑

你的generateToken方法生成refreshToken时,仅用sub(用户ID)和username作为payload。如果旧refreshToken还没过期,且新生成的refreshToken和旧token的payload、签名密钥、过期时间完全一致,JWT库会生成完全相同的token字符串——此时新旧token明文一样,哈希值自然相同,bcrypt.compare必然返回true。

建议在generateTokenPairAndUpdateAuthInfo里打印新生成的refreshToken,和传入的旧token直接对比,看是否完全一致。

2. 验证数据库读取的时效性

虽然你确认数据库值已更新,但要排查ORM的查询缓存问题:

  • 比如使用TypeORM等框架时,默认可能开启了实体缓存,导致findByIdWithAuthInfo读取的是旧数据
  • 可以在查询前强制清除缓存,或者临时关闭缓存测试,确认读取到的是最新哈希值

3. bcrypt的验证逻辑特性

bcrypt的哈希结果包含salt,只要明文相同,不管哈希生成的时间或salt是否不同,compare都会返回true(验证时会提取哈希中的salt重新计算对比)。所以本质问题还是新旧refreshToken的明文是同一个字符串。

4. 异步逻辑的时序检查

确认generateTokenPairAndUpdateAuthInfo中的异步调用是否正确:

await this.authInfoService.updateTokens(
  user.authInfoId,
  await this.hashData(accessToken),
  await this.hashData(refreshToken),
);

这里的await hashData是串行执行,能确保传入的是哈希后的字符串。如果不小心漏加await,会把Promise对象传入,导致数据库存储异常,但你已确认数据库值正确,这个可能性较低。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.16 23:04:50