#密码哈希与安全实践:使用 Passlib 实现安全的密码存储
📂 所属阶段:第四阶段 — 安全与认证(安全篇)
🔗 相关章节:OAuth2 与 JWT 鉴权 · 依赖注入系统
#1. 密码存储的原则
#1.1 绝对禁止的做法
# ❌ 绝对禁止!明文存储密码
def bad_register(email, password):
db.execute(f"INSERT INTO users VALUES ('{email}', '{password}')")
# ❌ 禁止!简单哈希(MD5/SHA1 可被彩虹表破解)
hashed = hashlib.md5(password.encode()).hexdigest()
# ❌ 禁止!固定盐值哈希(所有用户同一个盐)
salt = "myapp_salt"
hashed = hashlib.sha256((password + salt).encode()).hexdigest()#1.2 正确的做法:慢哈希 + 随机盐
- 慢哈希:故意设计得慢(如 bcrypt 耗时 ~300ms),让攻击者无法高速批量破解
- 随机盐:每个用户有独特的盐,相同密码的哈希值也不同,防止彩虹表攻击
#2. Passlib 详解
#2.1 安装
pip install passlib[bcrypt] bcrypt#2.2 基础用法
from passlib.context import CryptContext
# 配置:使用 bcrypt 算法,版本自动升级
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
# 哈希密码
def hash_password(password: str) -> str:
return pwd_context.hash(password)
# 验证密码
def verify_password(plain_password: str, hashed_password: str) -> bool:
return pwd_context.verify(plain_password, hashed_password)#2.3 bcrypt 原理
# bcrypt 会自动生成 22 字符的随机盐
# 存储格式:$2b$12$xxxxxx...($算法$代价因子$盐+哈希)
# 12 = 2^12 = 4096 次迭代,故意慢
# 示例
hashed = pwd_context.hash("MySecurePass123!")
# 每次结果都不同:
# $2b$12$LQv3c1yqBWVHxkd0LHAkCOYz6TtxMQJqhN8/X4.Qjj8PsGqxjPyWa
# $2b$12$J8yK8K1KxX.YyZzZzZzZzZAAAAAAAAAAAAAAAAAAAAAAAAAAA
# (随机盐不同,密文也不同,但都能验证通过)#3. 密码强度验证
#3.1 自定义 Pydantic 验证器
from pydantic import field_validator, model_validator
import re
class RegisterSchema(BaseModel):
email: EmailStr
password: str
password_confirm: str
@field_validator("password")
@classmethod
def validate_password_strength(cls, v: str) -> str:
if len(v) < 8:
raise ValueError("密码至少 8 个字符")
if not re.search(r"[A-Z]", v):
raise ValueError("密码必须包含至少一个大写字母")
if not re.search(r"[a-z]", v):
raise ValueError("密码必须包含至少一个小写字母")
if not re.search(r"\d", v):
raise ValueError("密码必须包含至少一个数字")
if not re.search(r"[!@#$%^&*(),.?\":{}|<>]", v):
raise ValueError("密码必须包含至少一个特殊字符")
return v
@model_validator
def passwords_match(self) -> "RegisterSchema":
if self.password != self.password_confirm:
raise ValueError("两次密码不一致")
return self#4. 完整的用户注册流程
# services/auth_service.py
from passlib.context import CryptContext
from models.user import User
from sqlalchemy.ext.asyncio import AsyncSession
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
class AuthService:
def __init__(self, db: AsyncSession):
self.db = db
async def register(self, email: str, password: str, name: str) -> User:
# 检查邮箱是否已存在
existing = await self.get_by_email(email)
if existing:
raise ValueError("该邮箱已被注册")
# 创建用户(密码已哈希)
user = User(
email=email,
hashed_password=pwd_context.hash(password),
name=name,
)
self.db.add(user)
await self.db.commit()
await self.db.refresh(user)
return user
async def login(self, email: str, password: str) -> User | None:
user = await self.get_by_email(email)
if not user:
return None
# 密码验证
if not pwd_context.verify(password, user.hashed_password):
return None
return user#5. 安全最佳实践清单
✅ 安全清单
1. 使用 bcrypt 或 argon2(慢哈希)
2. 每个用户使用随机盐(passlib 自动处理)
3. 设置合理的密码复杂度要求
4. 密码验证失败不要告诉用户是"密码错"还是"用户不存在"(防止用户枚举)
5. 账户锁定策略(连续 5 次失败后锁定 30 分钟)
6. Token 过期时间设置合理(access_token 15-30 分钟)
7. 使用 HTTPS 传输所有认证数据
8. 密钥存环境变量,不写代码里
❌ 常见错误
1. 明文或简单哈希存储密码
2. 固定盐值
3. 密码错误提示太详细("密码错误"而非"邮箱或密码错误")
4. Token 不过期
5. 在 URL 中传递 token(会被日志记录)#6. 高级:Argon2 替代 bcrypt
# Argon2id(2015 年密码哈希竞赛冠军,比 bcrypt 更安全)
pip install argon2-cffi
pwd_context = CryptContext(
schemes=["argon2"],
deprecated="auto",
argon2__memory_cost=65536, # 64MB 内存
argon2__time_cost=3, # 迭代次数
argon2__parallelism=4, # 并行度
)
# 比 bcrypt 更抗 GPU 攻击#7. 小结
| 操作 | 代码 |
|---|---|
| 哈希 | pwd_context.hash(password) |
| 验证 | pwd_context.verify(plain, hashed) |
| 自动升级 | deprecated="auto"(旧算法自动升级) |
💡 记住:永远不要自己实现哈希算法,使用经过安全审计的库(Passlib + bcrypt/argon2)。密码安全是 Web 应用的底线。
🔗 扩展阅读

