Files
xiaoxia 20909e2058
CI/CD Pipeline / Frontend Unit Tests (push) Successful in 44s
CI/CD Pipeline / Check if frontend-only change (push) Has been skipped
CI/CD Pipeline / Validate - Code Quality (push) Successful in 4m59s
CI/CD Pipeline / Validate - Type Check (mypy) (push) Successful in 1m4s
CI/CD Pipeline / Validate - Migration (alembic) (push) Successful in 48s
CI/CD Pipeline / Frontend Lint (push) Successful in 28s
CI/CD Pipeline / PR Build API Image (push) Has been skipped
CI/CD Pipeline / PR Build Web Image (push) Has been skipped
CI/CD Pipeline / PR Build Worker Image (push) Has been skipped
CI/CD Pipeline / Build Staging API Image (push) Successful in 14m43s
CI/CD Pipeline / Build Staging Web Image (push) Successful in 1m33s
CI/CD Pipeline / Build Staging Worker Image (push) Successful in 4m48s
CI/CD Pipeline / Build Production API Image (push) Has been skipped
CI/CD Pipeline / Build Production Web Image (push) Has been skipped
CI/CD Pipeline / Build Production Worker Image (push) Has been skipped
CI/CD Pipeline / Unit Tests (push) Successful in 4m46s
CI/CD Pipeline / Deploy Production (push) Has been skipped
CI/CD Pipeline / Integration Tests (push) Successful in 2m18s
CI/CD Pipeline / Deploy Staging (Watchtower auto-deploy) (push) Successful in 1m22s
CI/CD Pipeline / Production Browser E2E (push) Has been skipped
CI/CD Pipeline / Staging E2E Tests (push) Failing after 18s
CI/CD Pipeline / ACR Image Cleanup (push) Successful in 32s
CI/CD Pipeline / Staging API Integration Tests (push) Successful in 3m9s
fix: 修复ai_tasks.py中3个ruff F401未使用import (#792)
2026-07-24 00:20:28 +08:00

172 lines
4.8 KiB
Python
Executable File
Raw Permalink 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
from packages.domain.auth.password_hasher import PasswordHasherPort, PasswordValidatorPort
class PasswordHasher(PasswordHasherPort):
"""密码哈希服务"""
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:
# 提取当前的 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(PasswordValidatorPort):
"""密码强度验证器"""
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,
)