| from datetime import datetime, timedelta |
| from typing import Any, Optional |
| from jose import jwt, JWTError |
| from passlib.context import CryptContext |
| import secrets |
| import string |
|
|
| from app.core.config import settings |
| from loguru import logger |
|
|
| |
| |
|
|
| |
| |
| |
| |
|
|
|
|
| def create_access_token( |
| data: dict, expires_delta: Optional[timedelta] = None |
| ) -> str: |
| """ |
| 创建访问令牌 |
| |
| Args: |
| data: 要编码的数据 |
| expires_delta: 过期时间增量 |
| |
| Returns: |
| JWT令牌字符串 |
| """ |
| to_encode = data.copy() |
| if expires_delta: |
| expire = datetime.utcnow() + expires_delta |
| else: |
| expire = datetime.utcnow() + timedelta( |
| minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES |
| ) |
|
|
| to_encode.update({"exp": expire}) |
| encoded_jwt = jwt.encode( |
| to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM |
| ) |
| return encoded_jwt |
|
|
|
|
| def verify_token(token: str) -> Optional[dict]: |
| """ |
| 验证JWT令牌 |
| |
| Args: |
| token: JWT令牌 |
| |
| Returns: |
| 解码后的数据或None |
| """ |
| try: |
| payload = jwt.decode( |
| token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM] |
| ) |
| return payload |
| except JWTError as e: |
| logger.warning(f"JWT验证失败: {e}") |
| return None |
|
|
|
|
| def get_password_hash(password: str) -> str: |
| """ |
| 生成密码哈希 (统一在 UserService 中管理) |
| """ |
| pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") |
| return pwd_context.hash(password) |
|
|
|
|
| def verify_password(plain_password: str, hashed_password: str) -> bool: |
| """ |
| 验证密码 (统一在 UserService 中管理) |
| """ |
| pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") |
| return pwd_context.verify(plain_password, hashed_password) |
|
|
|
|
| def generate_random_string(length: int = 32) -> str: |
| """ |
| 生成随机字符串 |
| """ |
| alphabet = string.ascii_letters + string.digits |
| return ''.join(secrets.choice(alphabet) for _ in range(length)) |
|
|
|
|
| def generate_reset_token() -> str: |
| """ |
| 生成密码重置令牌 |
| """ |
| return secrets.token_urlsafe(32) |
|
|
|
|
| |
| |
|
|
| class SecurityUtils: |
| """ |
| 安全工具类 |
| """ |
|
|
| @staticmethod |
| def mask_email(email: str) -> str: |
| """ |
| 邮箱脱敏 |
| """ |
| if "@" not in email: |
| return email |
|
|
| local, domain = email.split("@", 1) |
| if len(local) <= 2: |
| masked_local = "*" * len(local) |
| else: |
| masked_local = local[0] + "*" * (len(local) - 2) + local[-1] |
|
|
| return f"{masked_local}@{domain}" |
|
|
| @staticmethod |
| def mask_phone(phone: str) -> str: |
| """ |
| 手机号脱敏 |
| """ |
| if len(phone) < 7: |
| return phone |
|
|
| return phone[:3] + "****" + phone[-4:] |
|
|
| @staticmethod |
| def validate_password_strength(password: str) -> dict: |
| """ |
| 验证密码强度 |
| """ |
| result = { |
| "valid": True, |
| "score": 0, |
| "issues": [] |
| } |
|
|
| |
| if len(password) < 8: |
| result["valid"] = False |
| result["issues"].append("密码长度至少8位") |
| else: |
| result["score"] += 1 |
|
|
| |
| if any(c.isdigit() for c in password): |
| result["score"] += 1 |
| else: |
| result["issues"].append("密码应包含数字") |
|
|
| |
| if any(c.islower() for c in password): |
| result["score"] += 1 |
| else: |
| result["issues"].append("密码应包含小写字母") |
|
|
| |
| if any(c.isupper() for c in password): |
| result["score"] += 1 |
| else: |
| result["issues"].append("密码应包含大写字母") |
|
|
| |
| special_chars = "!@#$%^&*()_+-=[]{}|;:,.<>?" |
| if any(c in special_chars for c in password): |
| result["score"] += 1 |
| else: |
| result["issues"].append("密码应包含特殊字符") |
|
|
| |
| if result["score"] < 3: |
| result["valid"] = False |
|
|
| return result |
|
|
|
|
| |
| security_utils = SecurityUtils() |
|
|