passlib 默认不自动识别哈希格式,必须显式指定算法或启用 auto-detect;需通过 cryptcontext 配置多算法并依赖标准前缀实现自动路由,配合 needs_update() 实现渐进式密码升级。

Passlib 默认不自动识别哈希格式,必须显式指定算法或启用 auto-detect
Passlib 不会像某些框架那样“猜”你传进来的哈希属于哪种算法。如果你直接用 crypt_context.verify() 验证一个混合了 bcrypt、sha256_crypt 和 pbkdf2_sha256 的哈希池,它默认只认当前配置的默认算法,其余全报 InvalidHashError 或 UnknownHashError。
正确做法是初始化时启用多算法支持:
from passlib.context import CryptContext <p>crypt_ctx = CryptContext( schemes=["bcrypt", "sha256_crypt", "pbkdf2_sha256"], default="bcrypt", deprecated=["sha256_crypt"], # 可选:标记旧算法为 deprecated )</p>
这样 crypt_ctx.verify() 才能根据哈希前缀(如 $2b$、$5$、$pbkdf2-sha256$)自动路由到对应解析器。
- 哈希字符串必须带标准前缀,否则无法 auto-detect;手动生成的裸 hex 值(如
abc123...)不支持 -
deprecated列表里的算法仍可验证,但调用needs_update()会返回True,方便你渐进升级 - 不要在运行时动态增删
schemes,CryptContext实例应全局复用,否则开销大且线程不安全
验证时别跳过 needs_update() 检查,否则老算法永远没机会淘汰
多算法共存不是终点,而是过渡手段。如果用户密码还是用 sha256_crypt 哈希的,你得在登录成功后主动重哈希并存新值——否则系统永远卡在旧算法上。
典型流程:
def check_password(user_input: str, stored_hash: str) -> bool:
if not crypt_ctx.verify(user_input, stored_hash):
return False
if crypt_ctx.needs_update(stored_hash):
new_hash = crypt_ctx.hash(user_input) # 自动用 default 算法(如 bcrypt)
save_new_hash_to_db(new_hash)
return True
-
crypt_ctx.needs_update()返回True当且仅当哈希属于deprecated列表,或强度低于当前默认算法推荐参数(如 bcrypt rounds - 不要用
crypt_ctx.hash()重新哈希明文再比对——这绕过了 auto-detect,且浪费 CPU - 数据库字段必须足够长(建议 ≥ 255 字符),因为不同算法输出长度差异大(
bcrypt约 60 字符,pbkdf2_sha256可达 100+)
迁移存量密码时,identify() 比正则更可靠
批量升级老数据时,别靠 stored_hash.startswith("$5$") 这类硬编码判断算法类型。Passlib 提供 crypt_ctx.identify(),它复用内部解析逻辑,兼容各种变体和边界情况。
例如:
# 从数据库读出一批旧哈希 old_hashes = fetch_all_password_hashes() <p>for h in old_hashes: algo = crypt_ctx.identify(h) # 返回 "sha256_crypt" / "des_crypt" / None if algo and algo in crypt_ctx.deprecated: plain = prompt_for_plaintext_or_use_backup() # 实际中需交互或走密钥恢复流程 new_hash = crypt_ctx.hash(plain) update_db(h, new_hash)</p>
-
identify()对无效哈希返回None,不会抛异常,适合批量处理 - 它比手动解析前缀更健壮——比如能识别
md5_crypt的多种历史变体($1$、$apr1$) - 注意:
identify()不校验哈希本身是否合法,只做格式识别;验证仍要用verify()
Web 应用里别把 CryptContext 实例塞进 request scope
有人图省事,在 FastAPI 或 Flask 的每个请求里新建 CryptContext,这是错的。它内部缓存了算法实现和参数校验逻辑,重复初始化既慢又浪费内存。
- 必须定义为模块级全局变量(如
auth.py里一个crypt_ctx),所有地方 import 复用 - 并发安全:
CryptContext是线程安全的,但它的.hash()方法底层调用 C 扩展(如 bcrypt),实际是 GIL 限制下的串行,高并发时注意瓶颈 - 若用异步框架(如 Starlette),
.hash()是阻塞操作,务必用loop.run_in_executor包裹,否则拖垮整个 event loop
多算法兼容的关键不在“支持多少种”,而在“如何让旧数据安静退役”。auto-detect、needs_update()、identify() 这三个能力要连起来用,缺一不可。单独配一堆 schemes 只是制造假安全感。
Python免费学习笔记(深入):立即使用
在学习笔记中,你将探索 Python 的核心概念和高级技巧!











