"""安全组件:bcrypt 密码哈希 + JWT 双 Token(对齐《02-技术文档》4.2)""" import uuid from datetime import datetime, timedelta, timezone import bcrypt import jwt from app.config import settings from app.core import errors from app.core.response import BizException def hash_password(plain: str) -> str: return bcrypt.hashpw(plain.encode(), bcrypt.gensalt()).decode() def verify_password(plain: str, hashed: str) -> bool: try: return bcrypt.checkpw(plain.encode(), hashed.encode()) except ValueError: return False def _create_token(subject: int, token_type: str, ttl: int, epoch: int = 0) -> tuple[str, str, int]: """生成 JWT,返回 (token, jti, 过期时间戳)""" now = datetime.now(timezone.utc) jti = uuid.uuid4().hex exp = now + timedelta(seconds=ttl) payload = { "sub": str(subject), "jti": jti, "type": token_type, # access / refresh / admin:{role} "ep": epoch, # 令牌纪元:改密/注销后递增使旧 Token 全端失效 "iat": int(now.timestamp()), "exp": exp, } token = jwt.encode(payload, settings.jwt_secret, algorithm=settings.jwt_algorithm) return token, jti, int(exp.timestamp()) def create_access_token(user_id: int, epoch: int = 0) -> tuple[str, str, int]: return _create_token(user_id, "access", settings.access_token_ttl, epoch) def create_refresh_token(user_id: int, epoch: int = 0) -> tuple[str, str, int]: return _create_token(user_id, "refresh", settings.refresh_token_ttl, epoch) def create_admin_token(admin_id: int, role: str) -> tuple[str, str, int]: """管理员 Token,payload 带角色,有效期 12h""" token, jti, exp = _create_token(admin_id, f"admin:{role}", settings.admin_token_ttl) return token, jti, exp def decode_token(token: str) -> dict: """解码并校验 Token,失败抛 1002""" try: payload = jwt.decode(token, settings.jwt_secret, algorithms=[settings.jwt_algorithm]) except jwt.PyJWTError: raise BizException(errors.UNAUTHORIZED, http_status=401) return payload