如何在未知用户ID时验证bcrypt哈希后的API令牌有效性?
未知用户ID时验证哈希存储的API令牌的解决方案
你当前的实现面临的核心问题是验证时无法快速定位到对应用户的哈希记录,遍历全库的性能问题确实不可忽视。以下是两种高效的解决方案,无需用户额外传递ID,也避免全库遍历:
方案1:令牌中嵌入加密的用户ID片段
生成令牌时,将用户ID用对称加密(如AES)处理后,与随机生成的机密部分拼接成完整令牌。验证时先解密令牌中的用户ID片段,再精准查询该用户的哈希列表进行验证。
代码示例
const crypto = require('crypto'); const bcrypt = require('bcrypt'); // AES加密配置(密钥需存环境变量,不要硬编码) const ENCRYPTION_KEY = process.env.AES_ENCRYPTION_KEY; const IV_LENGTH = 16; // AES固定IV长度 // AES加密函数 function encrypt(text) { const iv = crypto.randomBytes(IV_LENGTH); const cipher = crypto.createCipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); let encrypted = cipher.update(text); encrypted = Buffer.concat([encrypted, cipher.final()]); return `${iv.toString('hex')}:${encrypted.toString('hex')}`; } // AES解密函数 function decrypt(text) { const [ivHex, encryptedHex] = text.split(':'); if (!ivHex || !encryptedHex) throw new Error('Invalid encrypted format'); const iv = Buffer.from(ivHex, 'hex'); const encryptedText = Buffer.from(encryptedHex, 'hex'); const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv); let decrypted = decipher.update(encryptedText); decrypted = Buffer.concat([decrypted, decipher.final()]); return decrypted.toString(); } async function createToken(userId: number) { // 生成随机机密部分 const randomSecret = crypto.randomBytes(32).toString('hex'); // 加密用户ID并拼接成完整令牌 const encryptedUserId = encrypt(userId.toString()); const token = `${encryptedUserId}.${randomSecret}`; // 哈希令牌并存储 const salt = await bcrypt.genSalt(15); const hash = await bcrypt.hash(token, salt); await db.store({ userId, hash }); } async function verifyToken(token: string) { try { const [encryptedUserIdPart, randomSecretPart] = token.split('.'); if (!encryptedUserIdPart || !randomSecretPart) return false; // 解密得到用户ID const userIdStr = decrypt(encryptedUserIdPart); const userId = parseInt(userIdStr); if (isNaN(userId)) return false; // 查询该用户的所有哈希记录并验证 const hashes = await db.get(userId); for (const hash of hashes) { if (await bcrypt.compare(token, hash)) { return true; } } return false; } catch (err) { // 解密失败(令牌篡改/格式错误)直接返回无效 return false; } }
优缺点
- 优点:无需修改数据库结构,仅需调整令牌生成逻辑;用户ID被加密,不会泄露敏感信息。
- 缺点:加密密钥需严格保密,一旦泄露可能导致用户ID被破解;令牌长度会略有增加。
方案2:为令牌分配唯一非敏感标识符(Token ID)
生成令牌时,同时生成一个公开的、非敏感的唯一标识符(如UUID),将其与随机机密部分拼接成令牌。数据库存储时关联userId、tokenId和hash,并为tokenId建立索引。验证时通过令牌拆分出tokenId,直接查询对应记录进行哈希验证。
代码示例
const { v4: uuidv4 } = require('uuid'); const crypto = require('crypto'); const bcrypt = require('bcrypt'); async function createToken(userId: number) { // 生成唯一Token ID const tokenId = uuidv4(); // 生成随机机密部分 const randomSecret = crypto.randomBytes(32).toString('hex'); const token = `${tokenId}.${randomSecret}`; // 哈希令牌并存储(需确保tokenId字段有索引) const salt = await bcrypt.genSalt(15); const hash = await bcrypt.hash(token, salt); await db.store({ userId, tokenId, hash }); } async function verifyToken(token: string) { const [tokenId, randomSecretPart] = token.split('.'); if (!tokenId || !randomSecretPart) return false; // 通过Token ID直接查询对应记录 const record = await db.getByTokenId(tokenId); if (!record) return false; // 验证哈希匹配 return await bcrypt.compare(token, record.hash); }
优缺点
- 优点:查询性能最优(O(1)),支持单独吊销某条令牌(直接删除对应tokenId的记录);逻辑更简洁。
- 缺点:需要修改数据库表结构,添加
tokenId字段并建立索引;令牌长度会增加(UUID的长度)。
关键注意事项
- 无论哪种方案,都不要将用户ID明文放在令牌中,避免泄露用户敏感信息。
- 加密密钥、数据库连接信息等敏感配置必须存储在环境变量中,禁止硬编码。
- 建议为用户的令牌记录添加过期时间,定期清理无效令牌,减少查询时的遍历数量。
内容的提问来源于stack exchange,提问作者gorootde
相关产品推荐
相关产品推荐

