"""认证服务:登录业务 + JWT 签发/校验(HS256)+ 密码哈希/校验。 密码用 stdlib PBKDF2(免 bcrypt 依赖),存储格式自描述: pbkdf2_sha256$600000$$ """ import hashlib import hmac import secrets import time import jwt from sqlalchemy.ext.asyncio import AsyncSession from config.settings import settings from model.sys_user import SysUser from repositories.sys_user import SysUserRepo from utils.exceptions import ForbiddenError, ParamError _ITERATIONS = 600_000 # ---- JWT ---- def create_token(user_id: int, *, extra: dict | None = None) -> str: """签发 token:仅存 user_id + iat/exp,用户信息校验时按 id 回查 sys_user。""" now = int(time.time()) payload = {"user_id": user_id, "iat": now, "exp": now + settings.jwt.expire_seconds} if extra: payload.update(extra) return jwt.encode(payload, settings.jwt.secret, algorithm=settings.jwt.algorithm) def decode_token(token: str) -> dict: """校验签名 + 过期,返回 claims;任何失败抛异常(调用方转 401)。""" return jwt.decode(token, settings.jwt.secret, algorithms=[settings.jwt.algorithm]) # ---- 密码 ---- def hash_password(password: str) -> str: salt = secrets.token_hex(16) digest = hashlib.pbkdf2_hmac("sha256", password.encode(), bytes.fromhex(salt), _ITERATIONS) return f"pbkdf2_sha256${_ITERATIONS}${salt}${digest.hex()}" def verify_password(password: str, stored: str) -> bool: try: _, iters, salt, digest = stored.split("$") except ValueError: return False calc = hashlib.pbkdf2_hmac("sha256", password.encode(), bytes.fromhex(salt), int(iters)) return hmac.compare_digest(calc.hex(), digest) # ---- 登录业务(路由层只编排,不碰数据/逻辑) ---- def user_payload(u: SysUser) -> dict: """对外暴露的用户信息(不含密码等敏感字段)。""" return { "id": u.id, "username": u.username, "user_type": u.user_type, "employee_role": u.employee_role, "customer_level": u.customer_level, "status": u.status, } async def login(db: AsyncSession, username: str, password: str) -> dict: """校验账号密码与状态,成功签发 token 并返回用户信息;失败抛业务异常。""" user = await SysUserRepo(db).get_by_username(username) if user is None or not verify_password(password, user.password_hash): raise ParamError("用户名或密码错误") if user.status != "正常": raise ForbiddenError("账号状态异常,无法登录") return {"token": create_token(user.id), "user": user_payload(user)} async def register( db: AsyncSession, username: str, password: str, phone: str, real_name: str | None = None, ) -> dict: """客户注册:唯一性校验 → 密码加密存储 → 返回用户信息(不自动签发 token)。""" repo = SysUserRepo(db) if await repo.get_by_username(username) is not None: raise ParamError("用户名已存在") if await repo.get_by_phone(phone) is not None: raise ParamError("手机号已注册") user = SysUser( username=username, password_hash=hash_password(password), phone=phone, real_name=real_name, user_type="CUSTOMER", status="正常", ) user = await repo.add(user) return user_payload(user)