使用bcrypt时旧refreshToken匹配新哈希的异常问题排查
问题描述
我在使用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

