passwords.py 1.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354
  1. """Argon2id 密码散列、校验与复杂度策略。"""
  2. from __future__ import annotations
  3. from argon2 import PasswordHasher, Type
  4. from argon2.exceptions import Argon2Error
  5. _PASSWORD_HASHER = PasswordHasher(type=Type.ID)
  6. _DUMMY_PASSWORD_HASH = _PASSWORD_HASHER.hash(
  7. "fixed-invalid-credential-placeholder"
  8. )
  9. PASSWORD_MIN_LENGTH = 8
  10. PASSWORD_MAX_LENGTH = 64
  11. PASSWORD_MIN_CHAR_CLASSES = 3
  12. def validate_password_policy(value: str) -> None:
  13. """密码复杂度策略:长度8~64,且包含大写/小写/数字/特殊字符中至少三类。
  14. 适用于建户初始密码、管理员重置密码、门户首次设密;
  15. 抛出 ValueError(消息可直接面向用户),调用方负责转译为接口错误。
  16. """
  17. if len(value) < PASSWORD_MIN_LENGTH or len(value) > PASSWORD_MAX_LENGTH:
  18. raise ValueError("密码长度必须在8~64个字符之间")
  19. char_classes = sum(
  20. (
  21. any(c.islower() for c in value),
  22. any(c.isupper() for c in value),
  23. any(c.isdigit() for c in value),
  24. any(not c.isalnum() for c in value),
  25. )
  26. )
  27. if char_classes < PASSWORD_MIN_CHAR_CLASSES:
  28. raise ValueError(
  29. "密码必须包含大写字母、小写字母、数字、特殊字符中的至少三类"
  30. )
  31. def hash_password(password: str) -> str:
  32. if not password:
  33. raise ValueError("密码不能为空")
  34. return _PASSWORD_HASHER.hash(password)
  35. def verify_password(password_hash: str | None, password: str) -> bool:
  36. try:
  37. return _PASSWORD_HASHER.verify(
  38. password_hash or _DUMMY_PASSWORD_HASH,
  39. password,
  40. )
  41. except Argon2Error:
  42. return False