如何在不存储全部密码参数的前提下验证用户密码?
密码哈希参数的存储方案:合并字符串或替代方案
当然可以将这些密码哈希相关的参数(salt、哈希值、scrypt的n/r/p等)合并为单个字符串存储,这也是行业内的常规做法,同时还有更省心的现成方案可选,下面具体说明:
方案一:手动合并为单个字符串
你需要把验证密码时必须的所有参数都编码为字符串,用一个不会冲突的分隔符拼接起来,存储到数据库的单个字段中。验证时再拆分解析这些参数,重新计算哈希并比对。
具体实现(基于你提供的代码)
import hashlib import hmac # 保留你原有的hashpw和checkpw函数 def hashpw( password: str, encoding: str, salt: bytes, n: int, r: int, p: int, maxmem: int = 32 * 1024 * 1024, # 32 MiB dklen: int = 64, ) -> bytes: if len(password) > 1024: raise ValueError( "Password cannot be longer than 1024 characters", ) encoded = password.encode(encoding) if len(encoded) > 1024: raise ValueError( "Password cannot be longer than 1024 bytes when encoded", ) return hashlib.scrypt(encoded, salt=salt, n=n, r=r, p=p, maxmem=maxmem, dklen=dklen) def checkpw( password: str, encoding: str, salt: bytes, hashed_password: bytes, n: int, r: int, p: int, maxmem: int = 32 * 1024 * 1024, dklen: int = 64, ) -> bool: candidate = hashpw(password, encoding, salt, n, r, p, maxmem, dklen) return hmac.compare_digest(candidate, hashed_password) # 新增生成可存储字符串的函数 def generate_storable_hash( password: str, encoding: str = "utf-8", n: int = 16384, r: int = 8, p: int = 1, maxmem: int = 32 * 1024 * 1024, dklen: int = 64 ) -> str: # 生成每个用户唯一的随机salt(必须,不能共用) salt = hashlib.random_bytes(16) hashed = hashpw(password, encoding, salt, n, r, p, maxmem, dklen) # 用$作为分隔符,拼接所有必要参数(hex编码二进制数据避免乱码) parts = [ "scrypt", # 标识使用的哈希算法,方便后续扩展 encoding, str(n), str(r), str(p), str(dklen), salt.hex(), hashed.hex() ] return "$".join(parts) # 新增解析存储字符串的函数 def parse_storable_hash(stored_str: str) -> tuple: parts = stored_str.split("$") if parts[0] != "scrypt": raise ValueError("不支持的哈希算法") encoding = parts[1] n = int(parts[2]) r = int(parts[3]) p = int(parts[4]) dklen = int(parts[5]) salt = bytes.fromhex(parts[6]) hashed_password = bytes.fromhex(parts[7]) return encoding, n, r, p, dklen, salt, hashed_password # 新增从存储字符串验证密码的函数 def checkpw_from_stored( password: str, stored_str: str, maxmem: int = 32 * 1024 * 1024 ) -> bool: encoding, n, r, p, dklen, salt, hashed = parse_storable_hash(stored_str) return checkpw(password, encoding, salt, hashed, n, r, p, maxmem, dklen)
关键注意事项
- 分隔符选择:要选不会出现在编码后内容里的字符,比如用hex编码二进制数据时,字符范围是0-9a-f,用
$或|这类分隔符不会冲突。 - salt必须唯一:每个用户的salt都要随机生成,不能共用,否则会大幅降低哈希的安全性。
- 参数完整性:必须包含验证时需要的所有参数(n/r/p/dklen/encoding等),少一个都无法正确验证密码。
方案二:使用成熟的密码哈希库(更推荐)
手动实现容易遗漏细节(比如salt生成的随机性、参数的兼容性处理),更推荐用现成的库,比如passlib,它已经封装了哈希参数的存储、salt生成、算法兼容等所有细节,代码更简洁,安全性更有保障。
示例代码
from passlib.context import CryptContext # 初始化密码上下文,指定使用scrypt算法 pwd_context = CryptContext(schemes=["scrypt"], deprecated="auto") # 生成可直接存储的哈希字符串 def generate_hash(password: str) -> str: return pwd_context.hash(password) # 验证密码与存储的哈希字符串 def verify_password(password: str, stored_hash: str) -> bool: return pwd_context.verify(password, stored_hash)
使用时只需要调用generate_hash得到字符串存入数据库,验证时调用verify_password传入密码和存储的字符串即可,完全不用自己处理salt、参数拼接等问题。
内容的提问来源于stack exchange,提问作者Lorang
相关产品推荐
相关产品推荐

