Files
xiaoxia-saas/packages/domain/auth/password_hasher.py
T
2026-06-21 06:52:19 +08:00

172 lines
4.8 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
密码哈希工具类
使用 bcrypt 安全存储密码
"""
from typing import Optional
import bcrypt
class PasswordHasher:
"""密码哈希服务"""
def __init__(self, rounds: int = 12):
"""
初始化密码哈希器
Args:
rounds: bcrypt cost factor(默认 12,推荐范围 10-14
值越大越安全,但计算时间越长
"""
if rounds < 4 or rounds > 31:
raise ValueError("rounds must be between 4 and 31")
self.rounds = rounds
def hash_password(self, password: str) -> str:
"""
哈希密码
Args:
password: 明文密码
Returns:
bcrypt 哈希字符串(包含 salt
Raises:
ValueError: 密码为空
"""
if not password:
raise ValueError("Password cannot be empty")
# bcrypt 需要 bytes
password_bytes = password.encode("utf-8")
# 生成 salt 并哈希
salt = bcrypt.gensalt(rounds=self.rounds)
hashed = bcrypt.hashpw(password_bytes, salt)
# 返回字符串(数据库存储)
return hashed.decode("utf-8")
def verify_password(self, password: str, hashed_password: str) -> bool:
"""
验证密码
Args:
password: 明文密码
hashed_password: 存储的哈希密码
Returns:
True 如果密码正确,否则 False
"""
if not password or not hashed_password:
return False
try:
password_bytes = password.encode("utf-8")
hashed_bytes = hashed_password.encode("utf-8")
return bcrypt.checkpw(password_bytes, hashed_bytes)
except Exception:
# 哈希格式错误或其他异常,返回 False
return False
def needs_rehash(self, hashed_password: str) -> bool:
"""
检查哈希是否需要重新计算
(当 cost factor 改变时需要重新哈希)
Args:
hashed_password: 存储的哈希密码
Returns:
True 如果需要重新哈希
"""
try:
hashed_bytes = hashed_password.encode("utf-8")
current_rounds = bcrypt.getsalt(hashed_bytes)
# 提取当前的 cost factor
# bcrypt hash 格式: $2b$rounds$salt+hash
parts = hashed_password.split("$")
if len(parts) >= 3:
stored_rounds = int(parts[2])
return stored_rounds != self.rounds
return False
except Exception:
return False
class PasswordValidator:
"""密码强度验证器"""
def __init__(
self,
min_length: int = 8,
require_uppercase: bool = True,
require_lowercase: bool = True,
require_digit: bool = True,
require_special: bool = False,
):
"""
初始化密码验证器
Args:
min_length: 最小长度
require_uppercase: 是否要求大写字母
require_lowercase: 是否要求小写字母
require_digit: 是否要求数字
require_special: 是否要求特殊字符
"""
self.min_length = min_length
self.require_uppercase = require_uppercase
self.require_lowercase = require_lowercase
self.require_digit = require_digit
self.require_special = require_special
def validate(self, password: str) -> tuple[bool, Optional[str]]:
"""
验证密码强度
Args:
password: 明文密码
Returns:
(是否有效, 错误信息)
"""
if not password:
return False, "Password cannot be empty"
if len(password) < self.min_length:
return False, f"Password must be at least {self.min_length} characters"
if self.require_uppercase and not any(c.isupper() for c in password):
return False, "Password must contain at least one uppercase letter"
if self.require_lowercase and not any(c.islower() for c in password):
return False, "Password must contain at least one lowercase letter"
if self.require_digit and not any(c.isdigit() for c in password):
return False, "Password must contain at least one digit"
if self.require_special:
special_chars = "!@#$%^&*()_+-=[]{}|;:,.<>?~"
if not any(c in special_chars for c in password):
return False, "Password must contain at least one special character"
return True, None
# 全局实例
password_hasher = PasswordHasher(rounds=12)
password_validator = PasswordValidator(
min_length=8,
require_uppercase=True,
require_lowercase=True,
require_digit=True,
require_special=False,
)