9c6c477f55
CI/CD Pipeline / Validate Code Quality And Tests (pull_request) Failing after 2m22s
CI/CD Pipeline / Unit Tests (pull_request) Failing after 2m24s
CI/CD Pipeline / Integration Tests (pull_request) Failing after 37s
CI/CD Pipeline / Frontend Lint (pull_request) Successful in 4m3s
CI/CD Pipeline / Build & Push Staging (Watchtower auto-deploy) (pull_request) Has been skipped
CI/CD Pipeline / Build Production Runtime Images (pull_request) Has been skipped
CI/CD Pipeline / Staging API Integration Tests (pull_request) Has been skipped
CI/CD Pipeline / Deploy Production (pull_request) Has been skipped
CI/CD Pipeline / Staging E2E Tests (pull_request) Has been skipped
CI/CD Pipeline / Production Browser E2E (pull_request) Has been skipped
P0 关键修复: - P0-1: 注册接口添加 RateLimitMiddleware 限流保护 - P0-3: /metrics 端点添加 JWT 认证(移除匿名访问) - P0-4: 修复 Celery 任务名冲突(generation_task vs generate_video) - P1-5: JWT logout token 黑名单机制 P1 修复: - P1-1: forgot_password 硬编码 localhost → 使用 settings.APP_BASE_URL - P1-2: generation.py 直接创建 DB 连接 → 使用依赖注入 - P1-6: Image.open() 未关闭 → 统一使用 with 语句 - P1-7: 订阅续费事务修复 P2 代码质量: - P2-1: 修复 EditingMode 枚举重复定义 → 统一引用 shared 包 - P2-2: 修复 SMTP_FRON_NAME → SMTP_FROM_NAME 拼写 - P2-3: UserModel subscription_quota 类型统一为 float - P2-4: .env.production DATABASE_MAX_OVERFLOW 30 → 10 - 清理 15 处 except:pass(保留 2 处有注释说明的) - 禁用 SVG 上传(XSS 风险) - 删除 decode_token_unsafe() 不安全函数 - 简化 /ready 端点 - 删除 8 处死代码、10 个空文件/模块 - 合并 3 对 100% 重复函数 - 对齐 6 个废弃环境变量 v2 修复(代码审查后): - 修复密码重置路由路径: /password/forgot → /forgot-password, /password/reset → /reset-password(与前端 API 对齐) - 合并 _check_project_access: asset_libraries.py 和 edit_plans.py 中的重复函数统一到 _helpers.py(含空字符串守卫 + 中文错误信息) - 顺手修复: HTTPException 统一从 fastapi 导入(替换 starlette 导入) - OSS_ENDPOINT 拼写修复拆分为单独 PR,本 PR 不包含
170 lines
4.7 KiB
Python
170 lines
4.7 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:
|
||
|
||
# 提取当前的 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,
|
||
)
|