| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027 |
- """
- 客户端认证接口 - 统一API格式
- """
- from fastapi import APIRouter, Depends, HTTPException, Request
- from fastapi.responses import JSONResponse
- from sqlalchemy.orm import Session
- from app.database import get_db
- from app.api.deps import get_current_user_session, get_current_user_session_for_heartbeat
- from app.models.user import User
- from app.models.card import Card
- from app.models.recharge_record import RechargeRecord
- from app.models.system_config import SystemConfig
- from app.core.security import verify_password, get_password_hash
- from app.core.encrypt import encryptor
- from app.core.logger import logger
- from app.config import settings
- from app.schemas.client_api import *
- from app.services.key_rotation_service import KeyRotationService
- from app.services.session_service import SessionService
- from app.services.heartbeat_monitor import HeartbeatMonitor
- from app.services.anti_repeat_service import anti_repeat_service
- from app.utils.timestamp_validator import TimestampValidator
- from app.utils.timezone import beijing_now, datetime_to_beijing
- from datetime import datetime, timedelta
- import json
- import base64
- import hmac
- import hashlib
- from cryptography.hazmat.primitives import hashes
- from cryptography.hazmat.primitives.asymmetric import padding
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
- from cryptography.hazmat.backends import default_backend
- router = APIRouter()
- def _verify_hmac_signature(data: str, signature_b64: str, secret_key: str) -> bool:
- try:
- expected = hmac.new(secret_key.encode(), data.encode(), hashlib.sha256).digest()
- return base64.b64encode(expected).decode() == signature_b64
- except Exception:
- logger.exception("签名验证异常")
- return False
- def _process_mode_b_request(data: dict, db: Session) -> tuple[dict, bytes]:
- """
- 处理 Mode B 请求格式(encrypted_params + aes_key + key_version + timestamp + nonce + signature)
- 返回 (解密后的 params dict, AES密钥) 或抛出 HTTPException
- """
- try:
- required_keys = ("encrypted_params", "aes_key", "key_version", "timestamp", "nonce", "signature")
- if not all(k in data for k in required_keys):
- raise ValueError("缺少加密请求必需字段")
- encrypted_params_b64 = data.get("encrypted_params")
- encrypted_aes_key_b64 = data.get("aes_key")
- key_version = int(data.get("key_version"))
- timestamp = int(data.get("timestamp"))
- nonce = data.get("nonce")
- signature = data.get("signature")
- # 1. 验证时间戳
- is_valid, err_msg = TimestampValidator.validate_timestamp(timestamp, db)
- if not is_valid:
- raise ValueError(f"时间戳验证失败: {err_msg}")
- # 2. 验证签名(字段顺序需与 Rust 端一致)
- signature_data = f"{timestamp}:{nonce}:{encrypted_params_b64}:{encrypted_aes_key_b64}:{key_version}"
- if not _verify_hmac_signature(signature_data, signature, settings.SECRET_KEY):
- raise ValueError("签名验证失败")
- # 3. 加载私钥版本并用私钥解密 AES 密钥
- if not encryptor.load_key_by_version(db, key_version):
- raise ValueError(f"无效的密钥版本 {key_version}")
- encrypted_aes_key_bytes = base64.b64decode(encrypted_aes_key_b64)
- aes_key_bytes = encryptor.private_key.decrypt(
- encrypted_aes_key_bytes,
- padding.OAEP(
- mgf=padding.MGF1(algorithm=hashes.SHA256()),
- algorithm=hashes.SHA256(),
- label=None
- )
- )
- # 4. 解密 encrypted_params(IV(16) + payload),与 Rust 使用 CTR 模式保持一致
- encrypted_params_bytes = base64.b64decode(encrypted_params_b64)
- if len(encrypted_params_bytes) < 16:
- raise ValueError("加密参数长度无效")
- iv = encrypted_params_bytes[:16]
- encrypted_payload = encrypted_params_bytes[16:]
- cipher = Cipher(algorithms.AES(aes_key_bytes), modes.CTR(iv), backend=default_backend())
- decryptor = cipher.decryptor()
- decrypted_bytes = decryptor.update(encrypted_payload) + decryptor.finalize()
- params = json.loads(decrypted_bytes.decode("utf-8"))
- # 记录解密后的参数(用于调试)
- logger.debug(f"[Mode B解密] 解密后的请求参数: {json.dumps(params, ensure_ascii=False, indent=2)}")
- return params, aes_key_bytes
- except HTTPException:
- raise
- except Exception as e:
- logger.exception("Mode B 请求处理失败")
- raise HTTPException(status_code=400, detail=str(e))
- def process_client_request(request: Request, db: Session) -> tuple[ClientApiRequest, dict]:
- """
- 处理客户端请求:解密、验证时间戳
- Returns:
- tuple: (解析后的请求对象, 响应头)
- """
- try:
- # 获取请求数据
- data = request.json() if hasattr(request, 'json') else {}
- # 调试日志:打印原始请求数据
- logger.debug(f"[API请求] 原始请求数据: {json.dumps(data, ensure_ascii=False, indent=2)}")
- # 解密请求数据
- if settings.ENCRYPT_ENABLED:
- encrypted_req = EncryptedRequest(**data)
- decrypted_data = encryptor.decrypt_from_client(
- encrypted_req.encrypted,
- encrypted_req.key_version,
- db
- )
- logger.debug(f"[API请求] 解密后数据: {json.dumps(decrypted_data, ensure_ascii=False, indent=2)}")
- else:
- decrypted_data = data
- logger.debug(f"[API请求] 未加密数据: {json.dumps(decrypted_data, ensure_ascii=False, indent=2)}")
- # 解析为统一格式
- api_request = ClientApiRequest(**decrypted_data)
- logger.info(f"[API请求] 解析后的请求 - 操作: {api_request.action}, 时间戳: {api_request.timestamp}, 版本: {api_request.version}")
- # 验证时间戳
- is_valid, error_msg = TimestampValidator.validate_timestamp(api_request.timestamp, db)
- if not is_valid:
- logger.warning(f"[API请求] 时间戳验证失败: {error_msg}")
- raise HTTPException(status_code=400, detail=error_msg)
- # 准备响应头
- response_headers = {}
- if settings.ENCRYPT_ENABLED:
- active_version = KeyRotationService.get_active_key_version(db)
- response_headers["X-Key-Version"] = str(active_version)
- logger.debug(f"[API请求] 响应密钥版本: {active_version}")
- return api_request, response_headers
- except Exception as e:
- logger.error(f"[API请求] 处理客户端请求失败: {e}")
- raise HTTPException(status_code=400, detail=f"请求格式错误: {str(e)}")
- def create_client_response(data: dict = None, code: int = 200, msg: str = "成功", db: Session = None) -> dict:
- """
- 创建客户端响应:统一格式化并加密
- Returns:
- dict: 响应数据(已加密)
- """
- try:
- # 创建响应对象
- response_data = ClientApiResponse(
- code=code,
- msg=msg,
- data=data,
- timestamp=TimestampValidator.generate_timestamp(),
- version="1.0"
- )
- # 转换为字典
- response_dict = response_data.dict()
- # 调试日志:打印响应数据
- logger.info(f"[API响应] 响应数据 - 状态码: {code}, 消息: {msg}, 数据大小: {len(str(data)) if data else 0} 字符")
- logger.debug(f"[API响应] 完整响应数据: {json.dumps(response_dict, ensure_ascii=False, indent=2)}")
- # 加密响应
- if settings.ENCRYPT_ENABLED and db:
- encrypted_data, key_version = encryptor.encrypt_for_client(response_dict, db)
- encrypted_response = {
- "encrypted": encrypted_data,
- "key_version": key_version
- }
- logger.debug(f"[API响应] 加密响应数据: {json.dumps(encrypted_response, ensure_ascii=False, indent=2)}")
- return encrypted_response
- return response_dict
- except Exception as e:
- logger.error(f"[API响应] 创建客户端响应失败: {e}")
- # 返回未加密的错误响应
- error_response = {
- "code": 500,
- "msg": f"响应处理失败: {str(e)}",
- "timestamp": TimestampValidator.generate_timestamp(),
- "version": "1.0"
- }
- logger.debug(f"[API响应] 错误响应数据: {json.dumps(error_response, ensure_ascii=False, indent=2)}")
- return error_response
- @router.post("/register")
- async def register(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """用户注册(仅支持加密调用)"""
- try:
- # 检查是否为 Mode B 加密格式(encrypted_params + aes_key)
- data = await request.json()
- is_mode_b = bool(data.get("encrypted_params") and data.get("aes_key"))
- if is_mode_b:
- # Mode B 加密请求(来自 Rust secure_*)
- params, aes_key_bytes = _process_mode_b_request(data, db)
- # 验证操作类型如果有(可选)
- # 解析注册参数
- register_params = RegisterRequest(**{
- "username": params.get("username"),
- "password": params.get("password"),
- "email": params.get("email"),
- "card_code": params.get("card_code") or params.get("cardCode"),
- "machine_code": params.get("machine_code") or params.get("machineCode")
- })
- else:
- # 加密请求:使用原有逻辑(模式 A)
- api_request, response_headers = process_client_request(request, db)
- # 验证操作类型
- if api_request.action != ApiActions.REGISTER:
- return create_client_response(code=400, msg="操作类型不匹配", db=db)
- # 解析注册参数
- register_params = RegisterRequest(**api_request.params)
- # 检查用户名是否存在
- logger.info(f"[用户注册] 检查用户名是否存在: {register_params.username}")
- existing_user = db.query(User).filter(User.username == register_params.username).first()
- if existing_user:
- logger.warning(f"[用户注册] 用户名已存在: {register_params.username}")
- return create_secure_response(code=400, msg="用户名已存在", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- logger.info(f"[用户注册] 用户名可用: {register_params.username}")
- # 验证卡密
- logger.info(f"[用户注册] 检查卡密: {register_params.card_code}")
- card = db.query(Card).filter(
- Card.card_code == register_params.card_code,
- Card.status == 0
- ).first()
- if not card:
- logger.warning(f"[用户注册] 卡密无效或已被使用: {register_params.card_code}")
- return create_secure_response(code=400, msg="卡密无效或已被使用", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- logger.info(f"[用户注册] 卡密有效: {register_params.card_code}")
- # 计算到期时间
- expire_time = beijing_now() + timedelta(days=card.days)
- # 创建用户
- user = User(
- username=register_params.username,
- password=get_password_hash(register_params.password),
- email=register_params.email,
- machine_code=register_params.machine_code, # 可能为None,第一次登录时绑定
- card_id=card.id,
- expire_time=expire_time,
- status=1,
- created_at=beijing_now(),
- updated_at=beijing_now()
- )
- db.add(user)
- db.flush() # 刷新以获取user.id
-
- # 更新卡密状态
- card.user_id = user.id
- card.activated_at = beijing_now()
- card.expire_time = expire_time
- card.status = 1
- card.updated_at = beijing_now()
-
- # 创建充值记录
- recharge_record = RechargeRecord(
- user_id=user.id,
- card_id=card.id,
- software_code=card.software_code,
- days=card.days,
- expire_time_before=None, # 新用户,之前没有到期时间
- expire_time_after=expire_time,
- created_at=beijing_now()
- )
- db.add(recharge_record)
- db.flush() # 刷新以获取recharge_record.id
- # 创建历史记录
- from app.utils.history import create_user_history, create_card_history, create_recharge_record_history
- create_user_history(db, user, "INSERT")
- create_card_history(db, card, "UPDATE")
- create_recharge_record_history(db, recharge_record, "INSERT")
- # 一次性提交所有更改
- db.commit()
- logger.info(f"[用户注册] 数据库提交成功,用户ID: {user.id}")
- # 准备响应数据
- response_data = RegisterResponseData(user_id=user.id)
- logger.info(f"用户注册成功: {register_params.username}")
- return create_secure_response(data=response_data.dict(), aes_key=aes_key_bytes, key_version=data.get("key_version"))
- except Exception as e:
- db.rollback()
- logger.error(f"用户注册失败: {e}")
- return create_secure_response(code=500, msg="注册失败", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- @router.post("/login")
- async def login(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """用户登录(仅支持加密调用)"""
- try:
- logger.info("[用户登录] 开始处理登录请求")
- # 检查是否为 Mode B 加密格式
- data = await request.json()
- is_mode_b = bool(data.get("encrypted_params") and data.get("aes_key"))
- if is_mode_b:
- # Mode B 加密请求(来自 Rust secure_*)
- params, aes_key_bytes = _process_mode_b_request(data, db)
- login_params = LoginRequest(**{
- "username": params.get("username"),
- "password": params.get("password"),
- "machine_code": params.get("machine_code") or params.get("machineCode")
- })
- else:
- # 不支持的请求格式
- logger.warning("[用户登录] 不支持的请求格式:仅支持Mode B加密调用")
- return {"code": 400, "msg": "不支持的请求格式:仅支持加密调用"}
- logger.info(f"[用户登录] 用户名: {login_params.username}, 机器码: {login_params.machine_code}")
- # 获取系统配置(使用 KeyRotationService 提供的配置字典)
- # 注意:KeyRotationService 暴露的是 get_config_dict,而非 get_config
- config = KeyRotationService.get_config_dict()
- # 查找用户
- user = db.query(User).filter(User.username == login_params.username).first()
- if not user:
- logger.warning(f"[用户登录] 用户不存在: {login_params.username}")
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 验证密码
- if not verify_password(login_params.password, user.password):
- logger.warning(f"[用户登录] 密码验证失败: {login_params.username}")
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 验证/绑定机器码
- if user.machine_code:
- if user.machine_code != login_params.machine_code:
- logger.warning(f"[用户登录] 机器码不匹配 - 用户: {user.machine_code}, 请求: {login_params.machine_code}")
- if is_plaintext:
- return {"code": 403, "msg": "机器码不匹配,请先解绑"}
- else:
- return create_client_response(code=403, msg="机器码不匹配,请先解绑", db=db)
- else:
- # 用户已解绑或尚未绑定机器码,第一次登录时绑定当前机器码
- user.machine_code = login_params.machine_code
- logger.info(f"[用户登录] 绑定新机器码: {login_params.machine_code}")
- # 检查是否过期
- current_time = beijing_now().replace(tzinfo=None) # 转换为naive datetime进行比较
- if user.expire_time and user.expire_time < current_time:
- logger.warning(f"[用户登录] 用户卡密已过期: {user.expire_time}")
- return create_secure_response(code=403, msg="卡密已过期", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 获取客户端IP和User-Agent
- client_ip = request.client.host if request.client else None
- user_agent = request.headers.get("User-Agent")
- # 创建用户会话
- logger.debug(f"[用户登录] 创建会话 - 用户ID: {user.id}, IP: {client_ip}")
- session_success, session_msg, session_id = SessionService.create_session(
- db=db,
- user_id=user.id,
- machine_code=login_params.machine_code,
- ip_address=client_ip,
- user_agent=user_agent
- )
- if not session_success:
- logger.error(f"[用户登录] 创建会话失败: {session_msg}")
- return create_secure_response(code=500, msg=f"创建会话失败: {session_msg}", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 更新登录信息
- user.last_login_time = beijing_now()
- user.last_login_ip = client_ip
- user.updated_at = beijing_now()
- db.commit()
- # 准备响应数据
- response_data = LoginResponseData(
- user_id=user.id,
- username=user.username,
- session_id=session_id,
- expire_time=user.expire_time.isoformat() if user.expire_time else None,
- machine_code=user.machine_code
- )
- logger.info(f"[用户登录] 登录成功 - 用户: {login_params.username}, SessionID: {session_id[:8]}...")
- return create_secure_response(data=response_data.dict(), aes_key=aes_key_bytes, key_version=data.get("key_version"))
- except Exception as e:
- # 回滚数据库事务以避免半提交状态
- try:
- db.rollback()
- except Exception:
- # 回滚失败不影响主流程,继续记录异常
- logger.warning("回滚数据库事务时发生异常")
- # 记录完整堆栈以便排查
- logger.exception("用户登录失败")
- # 返回给客户端一个友好的错误提示(不暴露内部细节)
- return create_secure_response(code=500, msg="系统异常,登录失败,请稍后重试;若持续发生请联系客服", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- @router.post("/unbind")
- async def unbind(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """机器解绑(仅支持加密调用)"""
- try:
- # 获取请求数据
- data = await request.json()
- # 仅支持 Mode B 加密格式
- if data.get("encrypted_params") and data.get("aes_key"):
- params, aes_key_bytes = _process_mode_b_request(data, db)
- req = UnbindRequest(**{
- "username": params.get("username"),
- "password": params.get("password")
- })
- else:
- # 不支持的请求格式
- logger.warning("[机器解绑] 不支持的请求格式:仅支持Mode B加密调用")
- return {"code": 400, "msg": "不支持的请求格式:仅支持加密调用"}
- # 检查重复调用
- allow_call, error_msg = await anti_repeat_service.check_and_set_repeat_lock(
- "unbind", req.username, lock_duration=5
- )
- if not allow_call:
- logger.warning(f"[机器解绑] 重复调用检测: {req.username} - {error_msg}")
- return create_secure_response(code=429, msg=error_msg, aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 查找用户
- user = db.query(User).filter(User.username == req.username).first()
- if not user:
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 验证密码
- if not verify_password(req.password, user.password):
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 解绑机器码
- user.machine_code = None
- user.updated_at = beijing_now()
- db.commit()
- logger.info(f"机器解绑成功: {req.username}")
- return create_secure_response(code=200, msg="解绑成功", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- except Exception as e:
- try:
- db.rollback()
- except Exception:
- logger.warning("回滚数据库事务时发生异常")
- logger.exception("机器解绑失败")
- return create_secure_response(code=500, msg="系统异常,操作未完成,请稍后重试;若持续发生请联系客服", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- @router.post("/recharge")
- async def recharge(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """卡密充值(仅支持加密调用)"""
- try:
- # 获取请求数据(仅支持 Mode B 加密格式)
- data = await request.json()
- if data.get("encrypted_params") and data.get("aes_key"):
- params, aes_key_bytes = _process_mode_b_request(data, db)
- req = {
- "username": params.get("username"),
- "password": params.get("password"),
- "card_code": params.get("card_code") or params.get("cardCode")
- }
- else:
- # 不支持的请求格式
- logger.warning("[卡密充值] 不支持的请求格式:仅支持Mode B加密调用")
- return {"code": 400, "msg": "不支持的请求格式:仅支持加密调用"}
- # 验证必填字段
- if not req["username"] or not req["password"] or not req["card_code"]:
- return {"code": 400, "msg": "请填写完整的充值信息"}
- # 检查重复调用
- allow_call, error_msg = await anti_repeat_service.check_and_set_repeat_lock(
- "recharge", req["username"], lock_duration=5
- )
- if not allow_call:
- logger.warning(f"[卡密充值] 重复调用检测: {req['username']} - {error_msg}")
- return create_secure_response(code=429, msg=error_msg, aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 查找用户
- user = db.query(User).filter(User.username == req["username"]).first()
- if not user:
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 验证密码
- if not verify_password(req["password"], user.password):
- return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 验证卡密
- card = db.query(Card).filter(
- Card.card_code == req["card_code"],
- Card.status == 0
- ).first()
- if not card:
- return create_secure_response(code=400, msg="卡密无效或已被使用", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- # 计算新的到期时间
- expire_time_before = user.expire_time
- if expire_time_before and datetime_to_beijing(expire_time_before) > beijing_now():
- # 如果还有有效期,则累加
- expire_time_after = expire_time_before + timedelta(days=card.days)
- else:
- # 如果已过期或没有有效期,则从当前时间开始计算
- expire_time_after = beijing_now() + timedelta(days=card.days)
- # 更新用户到期时间
- user.expire_time = expire_time_after
- user.updated_at = beijing_now()
- # 更新卡密状态
- card.user_id = user.id
- card.activated_at = beijing_now()
- card.expire_time = expire_time_after
- card.status = 1
- card.updated_at = beijing_now()
- # 创建充值记录
- recharge_record = RechargeRecord(
- user_id=user.id,
- card_id=card.id,
- software_code=card.software_code,
- days=card.days,
- expire_time_before=expire_time_before,
- expire_time_after=expire_time_after,
- created_at=beijing_now()
- )
- db.add(recharge_record)
- # 先提交主记录以获取ID
- db.commit()
- # 刷新对象以获取生成的ID
- db.refresh(recharge_record)
- # 创建历史记录
- from app.utils.history import create_user_history, create_card_history, create_recharge_record_history
- create_user_history(db, user, "UPDATE")
- create_card_history(db, card, "UPDATE")
- create_recharge_record_history(db, recharge_record, "INSERT")
- # 提交历史记录
- db.commit()
- logger.info(f"用户 {req['username']} 卡密充值成功: {req['card_code']}")
- return create_secure_response(
- code=200,
- msg="充值成功",
- data={
- "expire_time": expire_time_after.isoformat(),
- "days": card.days
- },
- aes_key=aes_key_bytes,
- key_version=data.get("key_version")
- )
- except Exception as e:
- try:
- db.rollback()
- except Exception:
- logger.warning("回滚数据库事务时发生异常")
- logger.exception("卡密充值失败")
- return create_secure_response(code=500, msg="系统异常,充值失败,请稍后重试;若持续发生请联系客服", aes_key=aes_key_bytes, key_version=data.get("key_version"))
- @router.post("/heartbeat")
- async def heartbeat(
- user_session: tuple[User, Session] = Depends(get_current_user_session_for_heartbeat),
- db: Session = Depends(get_db)
- ):
- """心跳检测"""
- try:
- user, session = user_session
- # 记录心跳日志
- logger.info(f"[心跳检测] 用户: {user.username}(ID:{user.id}), Session: {session.session_id[:8]}...")
- # 记录心跳成功
- HeartbeatMonitor.record_heartbeat_success(db, user.id, session.session_id)
- # 检查用户是否过期
- if user.expire_time and datetime_to_beijing(user.expire_time) < beijing_now():
- logger.warning(f"用户 {user.username} 卡密已过期")
- return create_client_response(code=403, msg="卡密已过期", db=db)
- # 检查用户状态
- if user.status != 1:
- logger.warning(f"用户 {user.username} 状态异常: {user.status}")
- return create_client_response(code=403, msg="账号状态异常", db=db)
- # 更新会话活动时间(已在依赖注入中自动更新)
- # SessionService.validate_session 中已经调用了 update_activity
- # 获取系统配置
- from app.services.session_service import SessionService
- config = SessionService.get_session_config(db)
- # 返回心跳响应,包含更多状态信息
- response_data = HeartbeatResponseData(
- expire_time=user.expire_time.isoformat() if user.expire_time else None
- )
- # 获取session延长配置
- session_config = SessionService.get_session_config(db)
- # 获取客户端配置(通过心跳实时更新)
- from app.models.system_config import SystemConfig
- client_configs = {
- "api_timeout_seconds": SystemConfig.get_config_value('api_timeout_seconds', 30),
- "client_heartbeat_interval_seconds": SystemConfig.get_config_value('client_heartbeat_interval_seconds', 30),
- "max_heartbeat_failures": SystemConfig.get_config_value('max_heartbeat_failures', 10),
- "max_network_failures": SystemConfig.get_config_value('max_network_failures', 15),
- "sweep_batch_size": SystemConfig.get_config_value('sweep_batch_size', 10),
- "sweep_purchase_interval": SystemConfig.get_config_value('sweep_purchase_interval', 30),
- "sweep_max_purchases": SystemConfig.get_config_value('sweep_max_purchases', 100),
- "page_size": SystemConfig.get_config_value('page_size', 10),
- "steam_auth_countdown_seconds": SystemConfig.get_config_value('steam_auth_countdown_seconds', 30),
- }
- # 获取当前激活的密钥信息
- from app.models.api_key import APIKey
- current_key = db.query(APIKey).filter(APIKey.is_active == 1).first()
- key_info = None
- if current_key:
- key_info = {
- "key_version": current_key.key_version,
- "key_updated_at": current_key.updated_at.isoformat() if current_key.updated_at else None,
- }
- # 添加扩展响应数据
- extended_data = response_data.dict()
- extended_data.update({
- "server_time": beijing_now().isoformat(),
- "session_status": "active",
- "next_heartbeat": client_configs.get("client_heartbeat_interval_seconds", 30),
- "session_expires_at": session.expires_at.isoformat(),
- "session_extend_enabled": session_config.get("session_extend_enabled", True),
- "session_extend_threshold_hours": session_config.get("session_extend_threshold_hours", 4),
- # 实时配置更新
- "client_configs": client_configs,
- # 密钥版本信息(用于客户端密钥同步)
- "key_info": key_info
- })
- logger.debug(f"用户 {user.username} 心跳检测成功")
- return create_client_response(data=extended_data, db=db)
- except Exception as e:
- logger.error(f"心跳检测失败: {e}")
- return create_client_response(code=500, msg="心跳检测失败", db=db)
- def create_secure_response(data: dict = None, code: int = 200, msg: str = "成功", aes_key: bytes = None, key_version: int = None) -> dict:
- """
- 创建安全通信响应:使用前端提供的AES密钥加密
- Args:
- data: 响应数据
- code: 响应码
- msg: 响应消息
- aes_key: 前端提供的AES密钥(32字节)
- key_version: 密钥版本
- Returns:
- dict: 加密后的响应
- """
- try:
- # 创建响应对象
- response_data = ClientApiResponse(
- code=code,
- msg=msg,
- data=data,
- timestamp=TimestampValidator.generate_timestamp(),
- version="1.0"
- )
- # 转换为字典
- response_dict = response_data.dict()
- logger.debug(f"[安全响应] 原始响应数据: {json.dumps(response_dict, ensure_ascii=False, indent=2)}")
- # 使用前端提供的AES密钥和CTR模式加密(与公告API完全一致)
- from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
- from cryptography.hazmat.backends import default_backend
- # 生成随机IV(与公告API完全一致)
- import secrets
- response_iv = secrets.token_bytes(16)
- logger.debug(f"[安全响应] AES密钥长度: {len(aes_key)}, AES密钥前8字节: {aes_key[:8].hex()}, 随机IV: {response_iv.hex()}")
- cipher = Cipher(algorithms.AES(aes_key), modes.CTR(response_iv), backend=default_backend())
- encryptor_obj = cipher.encryptor()
- data_bytes = json.dumps(response_dict, ensure_ascii=False).encode('utf-8')
- logger.debug(f"[安全响应] JSON数据长度: {len(data_bytes)}, 内容: {data_bytes[:200]}...")
- # CTR模式不需要填充
- encrypted_response = encryptor_obj.update(data_bytes) + encryptor_obj.finalize()
- logger.debug(f"[安全响应] 加密payload长度: {len(encrypted_response)}")
- # 组合:IV(16) + 加密数据(与公告API完全一致)
- encrypted_response_with_iv = response_iv + encrypted_response
- logger.debug(f"[安全响应] 组合结果长度: {len(encrypted_response_with_iv)}")
- # Base64编码
- encrypted_data = base64.b64encode(encrypted_response_with_iv).decode('utf-8')
- logger.debug(f"[安全响应] Base64结果长度: {len(encrypted_data)}")
- logger.debug(f"[安全响应] 加密响应数据: {json.dumps({'encrypted': encrypted_data, 'key_version': key_version}, ensure_ascii=False, indent=2)}")
- return {
- "encrypted": encrypted_data,
- "key_version": key_version
- }
- except Exception as e:
- logger.error(f"[安全响应] 创建安全响应失败: {e}")
- # 返回未加密的错误响应
- error_response = {
- "code": 500,
- "msg": f"响应处理失败: {str(e)}",
- "timestamp": TimestampValidator.generate_timestamp(),
- "version": "1.0"
- }
- return error_response
- @router.post("/logout")
- async def logout(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """用户登出"""
- try:
- logger.info("[用户登出] 开始处理登出请求")
- # 处理客户端请求
- api_request, response_headers = process_client_request(request, db)
- # 验证操作类型
- if api_request.action != ApiActions.LOGOUT:
- logger.warning(f"[用户登出] 操作类型不匹配: {api_request.action}")
- return create_client_response(code=400, msg="操作类型不匹配", db=db)
- # 解析登出参数
- logout_params = LogoutRequest(**api_request.params)
- logger.info(f"[用户登出] 用户ID: {logout_params.user_id}, Session: {logout_params.session_id[:8]}...")
- # 验证会话(确保是当前用户的有效会话)
- is_valid, session, error_msg = SessionService.validate_session(db, logout_params.session_id)
- if not is_valid:
- logger.warning(f"[用户登出] 会话验证失败: {error_msg}")
- # 即使会话验证失败,也允许登出(可能是会话已过期但用户主动登出)
- logger.info("[用户登出] 会话验证失败但仍允许登出")
- # 停用会话
- if session:
- SessionService.deactivate_session(db, logout_params.session_id)
- logger.info(f"[用户登出] 用户 {logout_params.user_id} 的会话已停用")
- # 记录心跳监控:清除用户的缓存状态
- HeartbeatMonitor.reset_user_heartbeat(logout_params.user_id, logout_params.session_id)
- # 记录登出日志(可以后续在API日志中体现)
- logger.info(f"[用户登出] 用户 {logout_params.user_id} 成功登出")
- return create_client_response(msg="登出成功", db=db)
- except Exception as e:
- logger.error(f"[用户登出] 登出失败: {e}")
- return create_client_response(code=500, msg="登出失败", db=db)
- @router.post("/validate-session")
- async def validate_session(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """验证用户会话(用于自动登录验证)"""
- try:
- logger.info("[会话验证] 开始验证用户会话")
- # 处理客户端请求
- api_request, response_headers = process_client_request(request, db)
- # 验证操作类型
- if api_request.action != ApiActions.VALIDATE_SESSION:
- logger.warning(f"[会话验证] 操作类型不匹配: {api_request.action}")
- return create_client_response(code=400, msg="操作类型不匹配", db=db)
- # 解析验证参数
- validate_params = ValidateSessionRequest(**api_request.params)
- logger.info(f"[会话验证] 验证会话ID: {validate_params.session_id[:8]}...")
- # 验证会话
- is_valid, session, error_msg = SessionService.validate_session(db, validate_params.session_id)
- if not is_valid:
- logger.warning(f"[会话验证] 会话验证失败: {error_msg}")
- return create_client_response(code=401, msg=f"会话验证失败: {error_msg}", db=db)
- # 获取用户信息
- user = db.query(User).filter(User.id == session.user_id).first()
- if not user:
- logger.warning("[会话验证] 用户不存在")
- return create_client_response(code=401, msg="用户不存在", db=db)
- # 检查用户是否过期
- if user.expire_time and datetime_to_beijing(user.expire_time) < beijing_now():
- logger.warning(f"[会话验证] 用户 {user.username} 卡密已过期")
- return create_client_response(code=403, msg="卡密已过期", db=db)
- # 检查用户状态
- if user.status != 1:
- logger.warning(f"[会话验证] 用户 {user.username} 状态异常: {user.status}")
- return create_client_response(code=403, msg="账号状态异常", db=db)
- # 更新会话活动时间
- SessionService.update_session_activity(db, session.session_id)
- # 准备响应数据
- response_data = ValidateSessionResponseData(
- user_id=user.id,
- username=user.username,
- expire_time=user.expire_time.isoformat() if user.expire_time else None,
- is_active=user.status == 1,
- machine_code=user.machine_code or ""
- )
- logger.info(f"[会话验证] 用户 {user.username} 会话验证成功")
- return create_client_response(data=response_data.dict(), db=db)
- except Exception as e:
- logger.error(f"[会话验证] 验证失败: {e}")
- return create_client_response(code=500, msg="会话验证失败", db=db)
- @router.post("/activate-card")
- async def activate_card(
- user: User = Depends(get_current_user_session),
- request: Request = None,
- db: Session = Depends(get_db)
- ):
- """激活卡密(充值)"""
- try:
- logger.info(f"[卡密激活] 开始处理激活请求 - 用户: {user.username}(ID:{user.id})")
- # 处理客户端请求
- api_request, response_headers = process_client_request(request, db)
- # 验证操作类型
- if api_request.action != ApiActions.ACTIVATE_CARD:
- logger.warning(f"[卡密激活] 操作类型不匹配: {api_request.action}")
- return create_client_response(code=400, msg="操作类型不匹配", db=db)
- # 解析激活参数
- activate_params = ActivateCardRequest(**api_request.params)
- logger.info(f"[卡密激活] 激活参数 - 用户ID: {activate_params.user_id}, 卡密: {activate_params.card_code[:8]}...")
- # 验证用户ID是否匹配当前登录用户
- if activate_params.user_id != user.id:
- return create_client_response(code=403, msg="无权操作其他用户", db=db)
-
- # 验证卡密
- card = db.query(Card).filter(
- Card.card_code == activate_params.card_code,
- Card.status == 0
- ).first()
- if not card:
- return create_client_response(code=400, msg="卡密无效或已被使用", db=db)
- # 检查软件特征码是否匹配
- software_code = api_request.params.get("software_code", "")
- if card.software_code != software_code:
- return create_client_response(code=400, msg="卡密与当前软件不匹配", db=db)
-
- # 计算新的到期时间
- expire_time_before = user.expire_time
- if expire_time_before and datetime_to_beijing(expire_time_before) > beijing_now():
- # 如果还有有效期,则累加
- expire_time_after = expire_time_before + timedelta(days=card.days)
- else:
- # 如果已过期或没有有效期,则从当前时间开始计算
- expire_time_after = beijing_now() + timedelta(days=card.days)
- # 更新用户到期时间
- user.expire_time = expire_time_after
- user.updated_at = beijing_now()
-
- # 更新卡密状态
- card.user_id = user.id
- card.activated_at = beijing_now()
- card.expire_time = expire_time_after
- card.status = 1
- card.updated_at = beijing_now()
-
- # 创建充值记录
- recharge_record = RechargeRecord(
- user_id=user.id,
- card_id=card.id,
- software_code=card.software_code,
- days=card.days,
- expire_time_before=expire_time_before,
- expire_time_after=expire_time_after,
- created_at=beijing_now()
- )
- db.add(recharge_record)
- # 创建历史记录
- from app.utils.history import create_user_history, create_card_history, create_recharge_record_history
- create_user_history(db, user, "UPDATE")
- create_card_history(db, card, "UPDATE")
- create_recharge_record_history(db, recharge_record, "INSERT")
- # 一次性提交所有更改
- db.commit()
- # 准备响应数据
- response_data = ActivateCardResponseData(
- expire_time=expire_time_after.isoformat(),
- days=card.days
- )
- logger.info(f"用户 {user.username} 激活卡密成功: {activate_params.card_code}")
- return create_client_response(data=response_data.dict(), db=db)
- except Exception as e:
- db.rollback()
- logger.error(f"激活卡密失败: {e}")
- return create_client_response(code=500, msg="激活失败", db=db)
- @router.get("/recharge-records")
- async def get_recharge_records(
- request: Request,
- db: Session = Depends(get_db)
- ):
- """获取充值记录"""
- try:
- # 获取请求参数
- params = dict(request.query_params)
-
- # 解密请求数据(如果有)
- user_id = None
- if settings.ENCRYPT_ENABLED:
- # GET请求可能通过query参数传递加密数据
- encrypted_data = params.get("encrypted", "")
- if encrypted_data:
- decrypted = encryptor.decrypt(encrypted_data)
- user_id = decrypted.get("user_id")
- else:
- user_id = params.get("user_id")
- if user_id:
- user_id = int(user_id)
-
- if not user_id:
- return {"code": 400, "msg": "缺少用户ID"}
-
- # 查询充值记录
- records = db.query(RechargeRecord).filter(
- RechargeRecord.user_id == user_id,
- RechargeRecord.is_deleted == 0
- ).order_by(RechargeRecord.created_at.desc()).all()
-
- # 格式化返回数据
- records_data = []
- for record in records:
- # 获取卡密代码
- card_code = ""
- if record.card_id:
- card = db.query(Card).filter(Card.id == record.card_id).first()
- if card:
- card_code = card.card_code
-
- records_data.append({
- "id": record.id,
- "card_code": card_code,
- "days": record.days,
- "expire_time_before": record.expire_time_before.isoformat() if record.expire_time_before else None,
- "expire_time_after": record.expire_time_after.isoformat() if record.expire_time_after else None,
- "created_at": record.created_at.isoformat() if record.created_at else None
- })
-
- return {
- "code": 200,
- "msg": "成功",
- "data": records_data
- }
-
- except Exception as e:
- logger.exception("获取充值记录失败")
- return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}
|