密码哈希与安全实践:使用 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 应用的底线。


🔗 扩展阅读