172 lines
4.8 KiB
Python
172 lines
4.8 KiB
Python
"""
|
||
密码哈希工具类
|
||
使用 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,
|
||
)
|