auth.py 43 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028
  1. """
  2. 客户端认证接口 - 统一API格式
  3. """
  4. from fastapi import APIRouter, Depends, HTTPException, Request
  5. from fastapi.responses import JSONResponse
  6. from sqlalchemy.orm import Session
  7. from app.database import get_db
  8. from app.api.deps import get_current_user_session, get_current_user_session_for_heartbeat
  9. from app.models.user import User
  10. from app.models.card import Card
  11. from app.models.recharge_record import RechargeRecord
  12. from app.models.system_config import SystemConfig
  13. from app.core.security import verify_password, get_password_hash
  14. from app.core.encrypt import encryptor
  15. from app.core.logger import get_client_logger
  16. logger = get_client_logger()
  17. from app.config import settings
  18. from app.schemas.client_api import *
  19. from app.services.key_rotation_service import KeyRotationService
  20. from app.services.session_service import SessionService
  21. from app.services.heartbeat_monitor import HeartbeatMonitor
  22. from app.services.anti_repeat_service import anti_repeat_service
  23. from app.utils.timestamp_validator import TimestampValidator
  24. from app.utils.timezone import beijing_now, datetime_to_beijing
  25. from datetime import datetime, timedelta
  26. import json
  27. import base64
  28. import hmac
  29. import hashlib
  30. from cryptography.hazmat.primitives import hashes
  31. from cryptography.hazmat.primitives.asymmetric import padding
  32. from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
  33. from cryptography.hazmat.backends import default_backend
  34. router = APIRouter()
  35. def _verify_hmac_signature(data: str, signature_b64: str, secret_key: str) -> bool:
  36. try:
  37. expected = hmac.new(secret_key.encode(), data.encode(), hashlib.sha256).digest()
  38. return base64.b64encode(expected).decode() == signature_b64
  39. except Exception:
  40. logger.exception("签名验证异常")
  41. return False
  42. def _process_mode_b_request(data: dict, db: Session) -> tuple[dict, bytes]:
  43. """
  44. 处理 Mode B 请求格式(encrypted_params + aes_key + key_version + timestamp + nonce + signature)
  45. 返回 (解密后的 params dict, AES密钥) 或抛出 HTTPException
  46. """
  47. try:
  48. required_keys = ("encrypted_params", "aes_key", "key_version", "timestamp", "nonce", "signature")
  49. if not all(k in data for k in required_keys):
  50. raise ValueError("缺少加密请求必需字段")
  51. encrypted_params_b64 = data.get("encrypted_params")
  52. encrypted_aes_key_b64 = data.get("aes_key")
  53. key_version = int(data.get("key_version"))
  54. timestamp = int(data.get("timestamp"))
  55. nonce = data.get("nonce")
  56. signature = data.get("signature")
  57. # 1. 验证时间戳
  58. is_valid, err_msg = TimestampValidator.validate_timestamp(timestamp, db)
  59. if not is_valid:
  60. raise ValueError(f"时间戳验证失败: {err_msg}")
  61. # 2. 验证签名(字段顺序需与 Rust 端一致)
  62. signature_data = f"{timestamp}:{nonce}:{encrypted_params_b64}:{encrypted_aes_key_b64}:{key_version}"
  63. if not _verify_hmac_signature(signature_data, signature, settings.SECRET_KEY):
  64. raise ValueError("签名验证失败")
  65. # 3. 加载私钥版本并用私钥解密 AES 密钥
  66. if not encryptor.load_key_by_version(db, key_version):
  67. raise ValueError(f"无效的密钥版本 {key_version}")
  68. encrypted_aes_key_bytes = base64.b64decode(encrypted_aes_key_b64)
  69. aes_key_bytes = encryptor.private_key.decrypt(
  70. encrypted_aes_key_bytes,
  71. padding.OAEP(
  72. mgf=padding.MGF1(algorithm=hashes.SHA256()),
  73. algorithm=hashes.SHA256(),
  74. label=None
  75. )
  76. )
  77. # 4. 解密 encrypted_params(IV(16) + payload),与 Rust 使用 CTR 模式保持一致
  78. encrypted_params_bytes = base64.b64decode(encrypted_params_b64)
  79. if len(encrypted_params_bytes) < 16:
  80. raise ValueError("加密参数长度无效")
  81. iv = encrypted_params_bytes[:16]
  82. encrypted_payload = encrypted_params_bytes[16:]
  83. cipher = Cipher(algorithms.AES(aes_key_bytes), modes.CTR(iv), backend=default_backend())
  84. decryptor = cipher.decryptor()
  85. decrypted_bytes = decryptor.update(encrypted_payload) + decryptor.finalize()
  86. params = json.loads(decrypted_bytes.decode("utf-8"))
  87. # 记录解密后的参数(用于调试)
  88. logger.debug(f"[Mode B解密] 解密后的请求参数: {json.dumps(params, ensure_ascii=False, indent=2)}")
  89. return params, aes_key_bytes
  90. except HTTPException:
  91. raise
  92. except Exception as e:
  93. logger.exception("Mode B 请求处理失败")
  94. raise HTTPException(status_code=400, detail=str(e))
  95. def process_client_request(request: Request, db: Session) -> tuple[ClientApiRequest, dict]:
  96. """
  97. 处理客户端请求:解密、验证时间戳
  98. Returns:
  99. tuple: (解析后的请求对象, 响应头)
  100. """
  101. try:
  102. # 获取请求数据
  103. data = request.json() if hasattr(request, 'json') else {}
  104. # 调试日志:打印原始请求数据
  105. logger.debug(f"[API请求] 原始请求数据: {json.dumps(data, ensure_ascii=False, indent=2)}")
  106. # 解密请求数据
  107. if settings.ENCRYPT_ENABLED:
  108. encrypted_req = EncryptedRequest(**data)
  109. decrypted_data = encryptor.decrypt_from_client(
  110. encrypted_req.encrypted,
  111. encrypted_req.key_version,
  112. db
  113. )
  114. logger.debug(f"[API请求] 解密后数据: {json.dumps(decrypted_data, ensure_ascii=False, indent=2)}")
  115. else:
  116. decrypted_data = data
  117. logger.debug(f"[API请求] 未加密数据: {json.dumps(decrypted_data, ensure_ascii=False, indent=2)}")
  118. # 解析为统一格式
  119. api_request = ClientApiRequest(**decrypted_data)
  120. logger.info(f"[API请求] 解析后的请求 - 操作: {api_request.action}, 时间戳: {api_request.timestamp}, 版本: {api_request.version}")
  121. # 验证时间戳
  122. is_valid, error_msg = TimestampValidator.validate_timestamp(api_request.timestamp, db)
  123. if not is_valid:
  124. logger.warning(f"[API请求] 时间戳验证失败: {error_msg}")
  125. raise HTTPException(status_code=400, detail=error_msg)
  126. # 准备响应头
  127. response_headers = {}
  128. if settings.ENCRYPT_ENABLED:
  129. active_version = KeyRotationService.get_active_key_version(db)
  130. response_headers["X-Key-Version"] = str(active_version)
  131. logger.debug(f"[API请求] 响应密钥版本: {active_version}")
  132. return api_request, response_headers
  133. except Exception as e:
  134. logger.error(f"[API请求] 处理客户端请求失败: {e}")
  135. raise HTTPException(status_code=400, detail=f"请求格式错误: {str(e)}")
  136. def create_client_response(data: dict = None, code: int = 200, msg: str = "成功", db: Session = None) -> dict:
  137. """
  138. 创建客户端响应:统一格式化并加密
  139. Returns:
  140. dict: 响应数据(已加密)
  141. """
  142. try:
  143. # 创建响应对象
  144. response_data = ClientApiResponse(
  145. code=code,
  146. msg=msg,
  147. data=data,
  148. timestamp=TimestampValidator.generate_timestamp(),
  149. version="1.0"
  150. )
  151. # 转换为字典
  152. response_dict = response_data.dict()
  153. # 调试日志:打印响应数据
  154. logger.info(f"[API响应] 响应数据 - 状态码: {code}, 消息: {msg}, 数据大小: {len(str(data)) if data else 0} 字符")
  155. logger.debug(f"[API响应] 完整响应数据: {json.dumps(response_dict, ensure_ascii=False, indent=2)}")
  156. # 加密响应
  157. if settings.ENCRYPT_ENABLED and db:
  158. encrypted_data, key_version = encryptor.encrypt_for_client(response_dict, db)
  159. encrypted_response = {
  160. "encrypted": encrypted_data,
  161. "key_version": key_version
  162. }
  163. logger.debug(f"[API响应] 加密响应数据: {json.dumps(encrypted_response, ensure_ascii=False, indent=2)}")
  164. return encrypted_response
  165. return response_dict
  166. except Exception as e:
  167. logger.error(f"[API响应] 创建客户端响应失败: {e}")
  168. # 返回未加密的错误响应
  169. error_response = {
  170. "code": 500,
  171. "msg": f"响应处理失败: {str(e)}",
  172. "timestamp": TimestampValidator.generate_timestamp(),
  173. "version": "1.0"
  174. }
  175. logger.debug(f"[API响应] 错误响应数据: {json.dumps(error_response, ensure_ascii=False, indent=2)}")
  176. return error_response
  177. @router.post("/register")
  178. async def register(
  179. request: Request,
  180. db: Session = Depends(get_db)
  181. ):
  182. """用户注册(仅支持加密调用)"""
  183. try:
  184. # 检查是否为 Mode B 加密格式(encrypted_params + aes_key)
  185. data = await request.json()
  186. is_mode_b = bool(data.get("encrypted_params") and data.get("aes_key"))
  187. if is_mode_b:
  188. # Mode B 加密请求(来自 Rust secure_*)
  189. params, aes_key_bytes = _process_mode_b_request(data, db)
  190. # 验证操作类型如果有(可选)
  191. # 解析注册参数
  192. register_params = RegisterRequest(**{
  193. "username": params.get("username"),
  194. "password": params.get("password"),
  195. "email": params.get("email"),
  196. "card_code": params.get("card_code") or params.get("cardCode"),
  197. "machine_code": params.get("machine_code") or params.get("machineCode")
  198. })
  199. else:
  200. # 加密请求:使用原有逻辑(模式 A)
  201. api_request, response_headers = process_client_request(request, db)
  202. # 验证操作类型
  203. if api_request.action != ApiActions.REGISTER:
  204. return create_client_response(code=400, msg="操作类型不匹配", db=db)
  205. # 解析注册参数
  206. register_params = RegisterRequest(**api_request.params)
  207. # 检查用户名是否存在
  208. logger.info(f"[用户注册] 检查用户名是否存在: {register_params.username}")
  209. existing_user = db.query(User).filter(User.username == register_params.username).first()
  210. if existing_user:
  211. logger.warning(f"[用户注册] 用户名已存在: {register_params.username}")
  212. return create_secure_response(code=400, msg="用户名已存在", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  213. logger.info(f"[用户注册] 用户名可用: {register_params.username}")
  214. # 验证卡密
  215. logger.info(f"[用户注册] 检查卡密: {register_params.card_code}")
  216. card = db.query(Card).filter(
  217. Card.card_code == register_params.card_code,
  218. Card.status == 0
  219. ).first()
  220. if not card:
  221. logger.warning(f"[用户注册] 卡密无效或已被使用: {register_params.card_code}")
  222. return create_secure_response(code=400, msg="卡密无效或已被使用", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  223. logger.info(f"[用户注册] 卡密有效: {register_params.card_code}")
  224. # 计算到期时间
  225. expire_time = beijing_now() + timedelta(days=card.days)
  226. # 创建用户
  227. user = User(
  228. username=register_params.username,
  229. password=get_password_hash(register_params.password),
  230. email=register_params.email,
  231. machine_code=register_params.machine_code, # 可能为None,第一次登录时绑定
  232. card_id=card.id,
  233. expire_time=expire_time,
  234. status=1,
  235. created_at=beijing_now(),
  236. updated_at=beijing_now()
  237. )
  238. db.add(user)
  239. db.flush() # 刷新以获取user.id
  240. # 更新卡密状态
  241. card.user_id = user.id
  242. card.activated_at = beijing_now()
  243. card.expire_time = expire_time
  244. card.status = 1
  245. card.updated_at = beijing_now()
  246. # 创建充值记录
  247. recharge_record = RechargeRecord(
  248. user_id=user.id,
  249. card_id=card.id,
  250. software_code=card.software_code,
  251. days=card.days,
  252. expire_time_before=None, # 新用户,之前没有到期时间
  253. expire_time_after=expire_time,
  254. created_at=beijing_now()
  255. )
  256. db.add(recharge_record)
  257. db.flush() # 刷新以获取recharge_record.id
  258. # 创建历史记录
  259. from app.utils.history import create_user_history, create_card_history, create_recharge_record_history
  260. create_user_history(db, user, "INSERT")
  261. create_card_history(db, card, "UPDATE")
  262. create_recharge_record_history(db, recharge_record, "INSERT")
  263. # 一次性提交所有更改
  264. db.commit()
  265. logger.info(f"[用户注册] 数据库提交成功,用户ID: {user.id}")
  266. # 准备响应数据
  267. response_data = RegisterResponseData(user_id=user.id)
  268. logger.info(f"用户注册成功: {register_params.username}")
  269. return create_secure_response(data=response_data.dict(), aes_key=aes_key_bytes, key_version=data.get("key_version"))
  270. except Exception as e:
  271. db.rollback()
  272. logger.error(f"用户注册失败: {e}")
  273. return create_secure_response(code=500, msg="注册失败", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  274. @router.post("/login")
  275. async def login(
  276. request: Request,
  277. db: Session = Depends(get_db)
  278. ):
  279. """用户登录(仅支持加密调用)"""
  280. try:
  281. logger.info("[用户登录] 开始处理登录请求")
  282. # 检查是否为 Mode B 加密格式
  283. data = await request.json()
  284. is_mode_b = bool(data.get("encrypted_params") and data.get("aes_key"))
  285. if is_mode_b:
  286. # Mode B 加密请求(来自 Rust secure_*)
  287. params, aes_key_bytes = _process_mode_b_request(data, db)
  288. login_params = LoginRequest(**{
  289. "username": params.get("username"),
  290. "password": params.get("password"),
  291. "machine_code": params.get("machine_code") or params.get("machineCode")
  292. })
  293. else:
  294. # 不支持的请求格式
  295. logger.warning("[用户登录] 不支持的请求格式:仅支持Mode B加密调用")
  296. return {"code": 400, "msg": "不支持的请求格式:仅支持加密调用"}
  297. logger.info(f"[用户登录] 用户名: {login_params.username}, 机器码: {login_params.machine_code}")
  298. # 获取系统配置(使用 KeyRotationService 提供的配置字典)
  299. # 注意:KeyRotationService 暴露的是 get_config_dict,而非 get_config
  300. config = KeyRotationService.get_config_dict()
  301. # 查找用户
  302. user = db.query(User).filter(User.username == login_params.username).first()
  303. if not user:
  304. logger.warning(f"[用户登录] 用户不存在: {login_params.username}")
  305. return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  306. # 验证密码
  307. if not verify_password(login_params.password, user.password):
  308. logger.warning(f"[用户登录] 密码验证失败: {login_params.username}")
  309. return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  310. # 验证/绑定机器码
  311. if user.machine_code:
  312. if user.machine_code != login_params.machine_code:
  313. logger.warning(f"[用户登录] 机器码不匹配 - 用户: {user.machine_code}, 请求: {login_params.machine_code}")
  314. if is_plaintext:
  315. return {"code": 403, "msg": "机器码不匹配,请先解绑"}
  316. else:
  317. return create_client_response(code=403, msg="机器码不匹配,请先解绑", db=db)
  318. else:
  319. # 用户已解绑或尚未绑定机器码,第一次登录时绑定当前机器码
  320. user.machine_code = login_params.machine_code
  321. logger.info(f"[用户登录] 绑定新机器码: {login_params.machine_code}")
  322. # 检查是否过期
  323. current_time = beijing_now().replace(tzinfo=None) # 转换为naive datetime进行比较
  324. if user.expire_time and user.expire_time < current_time:
  325. logger.warning(f"[用户登录] 用户卡密已过期: {user.expire_time}")
  326. return create_secure_response(code=403, msg="卡密已过期", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  327. # 获取客户端IP和User-Agent
  328. client_ip = request.client.host if request.client else None
  329. user_agent = request.headers.get("User-Agent")
  330. # 创建用户会话
  331. logger.debug(f"[用户登录] 创建会话 - 用户ID: {user.id}, IP: {client_ip}")
  332. session_success, session_msg, session_id = SessionService.create_session(
  333. db=db,
  334. user_id=user.id,
  335. machine_code=login_params.machine_code,
  336. ip_address=client_ip,
  337. user_agent=user_agent
  338. )
  339. if not session_success:
  340. logger.error(f"[用户登录] 创建会话失败: {session_msg}")
  341. return create_secure_response(code=500, msg=f"创建会话失败: {session_msg}", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  342. # 更新登录信息
  343. user.last_login_time = beijing_now()
  344. user.last_login_ip = client_ip
  345. user.updated_at = beijing_now()
  346. db.commit()
  347. # 准备响应数据
  348. response_data = LoginResponseData(
  349. user_id=user.id,
  350. username=user.username,
  351. session_id=session_id,
  352. expire_time=user.expire_time.isoformat() if user.expire_time else None,
  353. machine_code=user.machine_code
  354. )
  355. logger.info(f"[用户登录] 登录成功 - 用户: {login_params.username}, SessionID: {session_id[:8]}...")
  356. return create_secure_response(data=response_data.dict(), aes_key=aes_key_bytes, key_version=data.get("key_version"))
  357. except Exception as e:
  358. # 回滚数据库事务以避免半提交状态
  359. try:
  360. db.rollback()
  361. except Exception:
  362. # 回滚失败不影响主流程,继续记录异常
  363. logger.warning("回滚数据库事务时发生异常")
  364. # 记录完整堆栈以便排查
  365. logger.exception("用户登录失败")
  366. # 返回给客户端一个友好的错误提示(不暴露内部细节)
  367. return create_secure_response(code=500, msg="系统异常,登录失败,请稍后重试;若持续发生请联系客服", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  368. @router.post("/unbind")
  369. async def unbind(
  370. request: Request,
  371. db: Session = Depends(get_db)
  372. ):
  373. """机器解绑(仅支持加密调用)"""
  374. try:
  375. # 获取请求数据
  376. data = await request.json()
  377. # 仅支持 Mode B 加密格式
  378. if data.get("encrypted_params") and data.get("aes_key"):
  379. params, aes_key_bytes = _process_mode_b_request(data, db)
  380. req = UnbindRequest(**{
  381. "username": params.get("username"),
  382. "password": params.get("password")
  383. })
  384. else:
  385. # 不支持的请求格式
  386. logger.warning("[机器解绑] 不支持的请求格式:仅支持Mode B加密调用")
  387. return {"code": 400, "msg": "不支持的请求格式:仅支持加密调用"}
  388. # 检查重复调用
  389. allow_call, error_msg = await anti_repeat_service.check_and_set_repeat_lock(
  390. "unbind", req.username, lock_duration=5
  391. )
  392. if not allow_call:
  393. logger.warning(f"[机器解绑] 重复调用检测: {req.username} - {error_msg}")
  394. return create_secure_response(code=429, msg=error_msg, aes_key=aes_key_bytes, key_version=data.get("key_version"))
  395. # 查找用户
  396. user = db.query(User).filter(User.username == req.username).first()
  397. if not user:
  398. return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  399. # 验证密码
  400. if not verify_password(req.password, user.password):
  401. return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  402. # 解绑机器码
  403. user.machine_code = None
  404. user.updated_at = beijing_now()
  405. db.commit()
  406. logger.info(f"机器解绑成功: {req.username}")
  407. return create_secure_response(code=200, msg="解绑成功", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  408. except Exception as e:
  409. try:
  410. db.rollback()
  411. except Exception:
  412. logger.warning("回滚数据库事务时发生异常")
  413. logger.exception("机器解绑失败")
  414. return create_secure_response(code=500, msg="系统异常,操作未完成,请稍后重试;若持续发生请联系客服", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  415. @router.post("/recharge")
  416. async def recharge(
  417. request: Request,
  418. db: Session = Depends(get_db)
  419. ):
  420. """卡密充值(仅支持加密调用)"""
  421. try:
  422. # 获取请求数据(仅支持 Mode B 加密格式)
  423. data = await request.json()
  424. if data.get("encrypted_params") and data.get("aes_key"):
  425. params, aes_key_bytes = _process_mode_b_request(data, db)
  426. req = {
  427. "username": params.get("username"),
  428. "password": params.get("password"),
  429. "card_code": params.get("card_code") or params.get("cardCode")
  430. }
  431. else:
  432. # 不支持的请求格式
  433. logger.warning("[卡密充值] 不支持的请求格式:仅支持Mode B加密调用")
  434. return {"code": 400, "msg": "不支持的请求格式:仅支持加密调用"}
  435. # 验证必填字段
  436. if not req["username"] or not req["password"] or not req["card_code"]:
  437. return {"code": 400, "msg": "请填写完整的充值信息"}
  438. # 检查重复调用
  439. allow_call, error_msg = await anti_repeat_service.check_and_set_repeat_lock(
  440. "recharge", req["username"], lock_duration=5
  441. )
  442. if not allow_call:
  443. logger.warning(f"[卡密充值] 重复调用检测: {req['username']} - {error_msg}")
  444. return create_secure_response(code=429, msg=error_msg, aes_key=aes_key_bytes, key_version=data.get("key_version"))
  445. # 查找用户
  446. user = db.query(User).filter(User.username == req["username"]).first()
  447. if not user:
  448. return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  449. # 验证密码
  450. if not verify_password(req["password"], user.password):
  451. return create_secure_response(code=401, msg="用户名或密码错误", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  452. # 验证卡密
  453. card = db.query(Card).filter(
  454. Card.card_code == req["card_code"],
  455. Card.status == 0
  456. ).first()
  457. if not card:
  458. return create_secure_response(code=400, msg="卡密无效或已被使用", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  459. # 计算新的到期时间
  460. expire_time_before = user.expire_time
  461. if expire_time_before and datetime_to_beijing(expire_time_before) > beijing_now():
  462. # 如果还有有效期,则累加
  463. expire_time_after = expire_time_before + timedelta(days=card.days)
  464. else:
  465. # 如果已过期或没有有效期,则从当前时间开始计算
  466. expire_time_after = beijing_now() + timedelta(days=card.days)
  467. # 更新用户到期时间
  468. user.expire_time = expire_time_after
  469. user.updated_at = beijing_now()
  470. # 更新卡密状态
  471. card.user_id = user.id
  472. card.activated_at = beijing_now()
  473. card.expire_time = expire_time_after
  474. card.status = 1
  475. card.updated_at = beijing_now()
  476. # 创建充值记录
  477. recharge_record = RechargeRecord(
  478. user_id=user.id,
  479. card_id=card.id,
  480. software_code=card.software_code,
  481. days=card.days,
  482. expire_time_before=expire_time_before,
  483. expire_time_after=expire_time_after,
  484. created_at=beijing_now()
  485. )
  486. db.add(recharge_record)
  487. # 先提交主记录以获取ID
  488. db.commit()
  489. # 刷新对象以获取生成的ID
  490. db.refresh(recharge_record)
  491. # 创建历史记录
  492. from app.utils.history import create_user_history, create_card_history, create_recharge_record_history
  493. create_user_history(db, user, "UPDATE")
  494. create_card_history(db, card, "UPDATE")
  495. create_recharge_record_history(db, recharge_record, "INSERT")
  496. # 提交历史记录
  497. db.commit()
  498. logger.info(f"用户 {req['username']} 卡密充值成功: {req['card_code']}")
  499. return create_secure_response(
  500. code=200,
  501. msg="充值成功",
  502. data={
  503. "expire_time": expire_time_after.isoformat(),
  504. "days": card.days
  505. },
  506. aes_key=aes_key_bytes,
  507. key_version=data.get("key_version")
  508. )
  509. except Exception as e:
  510. try:
  511. db.rollback()
  512. except Exception:
  513. logger.warning("回滚数据库事务时发生异常")
  514. logger.exception("卡密充值失败")
  515. return create_secure_response(code=500, msg="系统异常,充值失败,请稍后重试;若持续发生请联系客服", aes_key=aes_key_bytes, key_version=data.get("key_version"))
  516. @router.post("/heartbeat")
  517. async def heartbeat(
  518. user_session: tuple[User, Session] = Depends(get_current_user_session_for_heartbeat),
  519. db: Session = Depends(get_db)
  520. ):
  521. """心跳检测"""
  522. try:
  523. user, session = user_session
  524. # 记录心跳日志
  525. logger.info(f"[心跳检测] 用户: {user.username}(ID:{user.id}), Session: {session.session_id[:8]}...")
  526. # 记录心跳成功
  527. HeartbeatMonitor.record_heartbeat_success(db, user.id, session.session_id)
  528. # 检查用户是否过期
  529. if user.expire_time and datetime_to_beijing(user.expire_time) < beijing_now():
  530. logger.warning(f"用户 {user.username} 卡密已过期")
  531. return create_client_response(code=403, msg="卡密已过期", db=db)
  532. # 检查用户状态
  533. if user.status != 1:
  534. logger.warning(f"用户 {user.username} 状态异常: {user.status}")
  535. return create_client_response(code=403, msg="账号状态异常", db=db)
  536. # 更新会话活动时间(已在依赖注入中自动更新)
  537. # SessionService.validate_session 中已经调用了 update_activity
  538. # 获取系统配置
  539. from app.services.session_service import SessionService
  540. config = SessionService.get_session_config(db)
  541. # 返回心跳响应,包含更多状态信息
  542. response_data = HeartbeatResponseData(
  543. expire_time=user.expire_time.isoformat() if user.expire_time else None
  544. )
  545. # 获取session延长配置
  546. session_config = SessionService.get_session_config(db)
  547. # 获取客户端配置(通过心跳实时更新)
  548. from app.models.system_config import SystemConfig
  549. client_configs = {
  550. "api_timeout_seconds": SystemConfig.get_config_value('api_timeout_seconds', 30),
  551. "client_heartbeat_interval_seconds": SystemConfig.get_config_value('client_heartbeat_interval_seconds', 30),
  552. "max_heartbeat_failures": SystemConfig.get_config_value('max_heartbeat_failures', 10),
  553. "max_network_failures": SystemConfig.get_config_value('max_network_failures', 15),
  554. "sweep_batch_size": SystemConfig.get_config_value('sweep_batch_size', 10),
  555. "sweep_purchase_interval": SystemConfig.get_config_value('sweep_purchase_interval', 30),
  556. "sweep_max_purchases": SystemConfig.get_config_value('sweep_max_purchases', 100),
  557. "page_size": SystemConfig.get_config_value('page_size', 10),
  558. "steam_auth_countdown_seconds": SystemConfig.get_config_value('steam_auth_countdown_seconds', 30),
  559. }
  560. # 获取当前激活的密钥信息
  561. from app.models.api_key import APIKey
  562. current_key = db.query(APIKey).filter(APIKey.is_active == 1).first()
  563. key_info = None
  564. if current_key:
  565. key_info = {
  566. "key_version": current_key.key_version,
  567. "key_updated_at": current_key.updated_at.isoformat() if current_key.updated_at else None,
  568. }
  569. # 添加扩展响应数据
  570. extended_data = response_data.dict()
  571. extended_data.update({
  572. "server_time": beijing_now().isoformat(),
  573. "session_status": "active",
  574. "next_heartbeat": client_configs.get("client_heartbeat_interval_seconds", 30),
  575. "session_expires_at": session.expires_at.isoformat(),
  576. "session_extend_enabled": session_config.get("session_extend_enabled", True),
  577. "session_extend_threshold_hours": session_config.get("session_extend_threshold_hours", 4),
  578. # 实时配置更新
  579. "client_configs": client_configs,
  580. # 密钥版本信息(用于客户端密钥同步)
  581. "key_info": key_info
  582. })
  583. logger.debug(f"用户 {user.username} 心跳检测成功")
  584. return create_client_response(data=extended_data, db=db)
  585. except Exception as e:
  586. logger.error(f"心跳检测失败: {e}")
  587. return create_client_response(code=500, msg="心跳检测失败", db=db)
  588. def create_secure_response(data: dict = None, code: int = 200, msg: str = "成功", aes_key: bytes = None, key_version: int = None) -> dict:
  589. """
  590. 创建安全通信响应:使用前端提供的AES密钥加密
  591. Args:
  592. data: 响应数据
  593. code: 响应码
  594. msg: 响应消息
  595. aes_key: 前端提供的AES密钥(32字节)
  596. key_version: 密钥版本
  597. Returns:
  598. dict: 加密后的响应
  599. """
  600. try:
  601. # 创建响应对象
  602. response_data = ClientApiResponse(
  603. code=code,
  604. msg=msg,
  605. data=data,
  606. timestamp=TimestampValidator.generate_timestamp(),
  607. version="1.0"
  608. )
  609. # 转换为字典
  610. response_dict = response_data.dict()
  611. logger.debug(f"[安全响应] 原始响应数据: {json.dumps(response_dict, ensure_ascii=False, indent=2)}")
  612. # 使用前端提供的AES密钥和CTR模式加密(与公告API完全一致)
  613. from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
  614. from cryptography.hazmat.backends import default_backend
  615. # 生成随机IV(与公告API完全一致)
  616. import secrets
  617. response_iv = secrets.token_bytes(16)
  618. logger.debug(f"[安全响应] AES密钥长度: {len(aes_key)}, AES密钥前8字节: {aes_key[:8].hex()}, 随机IV: {response_iv.hex()}")
  619. cipher = Cipher(algorithms.AES(aes_key), modes.CTR(response_iv), backend=default_backend())
  620. encryptor_obj = cipher.encryptor()
  621. data_bytes = json.dumps(response_dict, ensure_ascii=False).encode('utf-8')
  622. logger.debug(f"[安全响应] JSON数据长度: {len(data_bytes)}, 内容: {data_bytes[:200]}...")
  623. # CTR模式不需要填充
  624. encrypted_response = encryptor_obj.update(data_bytes) + encryptor_obj.finalize()
  625. logger.debug(f"[安全响应] 加密payload长度: {len(encrypted_response)}")
  626. # 组合:IV(16) + 加密数据(与公告API完全一致)
  627. encrypted_response_with_iv = response_iv + encrypted_response
  628. logger.debug(f"[安全响应] 组合结果长度: {len(encrypted_response_with_iv)}")
  629. # Base64编码
  630. encrypted_data = base64.b64encode(encrypted_response_with_iv).decode('utf-8')
  631. logger.debug(f"[安全响应] Base64结果长度: {len(encrypted_data)}")
  632. logger.debug(f"[安全响应] 加密响应数据: {json.dumps({'encrypted': encrypted_data, 'key_version': key_version}, ensure_ascii=False, indent=2)}")
  633. return {
  634. "encrypted": encrypted_data,
  635. "key_version": key_version
  636. }
  637. except Exception as e:
  638. logger.error(f"[安全响应] 创建安全响应失败: {e}")
  639. # 返回未加密的错误响应
  640. error_response = {
  641. "code": 500,
  642. "msg": f"响应处理失败: {str(e)}",
  643. "timestamp": TimestampValidator.generate_timestamp(),
  644. "version": "1.0"
  645. }
  646. return error_response
  647. @router.post("/logout")
  648. async def logout(
  649. request: Request,
  650. db: Session = Depends(get_db)
  651. ):
  652. """用户登出"""
  653. try:
  654. logger.info("[用户登出] 开始处理登出请求")
  655. # 处理客户端请求
  656. api_request, response_headers = process_client_request(request, db)
  657. # 验证操作类型
  658. if api_request.action != ApiActions.LOGOUT:
  659. logger.warning(f"[用户登出] 操作类型不匹配: {api_request.action}")
  660. return create_client_response(code=400, msg="操作类型不匹配", db=db)
  661. # 解析登出参数
  662. logout_params = LogoutRequest(**api_request.params)
  663. logger.info(f"[用户登出] 用户ID: {logout_params.user_id}, Session: {logout_params.session_id[:8]}...")
  664. # 验证会话(确保是当前用户的有效会话)
  665. is_valid, session, error_msg = SessionService.validate_session(db, logout_params.session_id)
  666. if not is_valid:
  667. logger.warning(f"[用户登出] 会话验证失败: {error_msg}")
  668. # 即使会话验证失败,也允许登出(可能是会话已过期但用户主动登出)
  669. logger.info("[用户登出] 会话验证失败但仍允许登出")
  670. # 停用会话
  671. if session:
  672. SessionService.deactivate_session(db, logout_params.session_id)
  673. logger.info(f"[用户登出] 用户 {logout_params.user_id} 的会话已停用")
  674. # 记录心跳监控:清除用户的缓存状态
  675. HeartbeatMonitor.reset_user_heartbeat(logout_params.user_id, logout_params.session_id)
  676. # 记录登出日志(可以后续在API日志中体现)
  677. logger.info(f"[用户登出] 用户 {logout_params.user_id} 成功登出")
  678. return create_client_response(msg="登出成功", db=db)
  679. except Exception as e:
  680. logger.error(f"[用户登出] 登出失败: {e}")
  681. return create_client_response(code=500, msg="登出失败", db=db)
  682. @router.post("/validate-session")
  683. async def validate_session(
  684. request: Request,
  685. db: Session = Depends(get_db)
  686. ):
  687. """验证用户会话(用于自动登录验证)"""
  688. try:
  689. logger.info("[会话验证] 开始验证用户会话")
  690. # 处理客户端请求
  691. api_request, response_headers = process_client_request(request, db)
  692. # 验证操作类型
  693. if api_request.action != ApiActions.VALIDATE_SESSION:
  694. logger.warning(f"[会话验证] 操作类型不匹配: {api_request.action}")
  695. return create_client_response(code=400, msg="操作类型不匹配", db=db)
  696. # 解析验证参数
  697. validate_params = ValidateSessionRequest(**api_request.params)
  698. logger.info(f"[会话验证] 验证会话ID: {validate_params.session_id[:8]}...")
  699. # 验证会话
  700. is_valid, session, error_msg = SessionService.validate_session(db, validate_params.session_id)
  701. if not is_valid:
  702. logger.warning(f"[会话验证] 会话验证失败: {error_msg}")
  703. return create_client_response(code=401, msg=f"会话验证失败: {error_msg}", db=db)
  704. # 获取用户信息
  705. user = db.query(User).filter(User.id == session.user_id).first()
  706. if not user:
  707. logger.warning("[会话验证] 用户不存在")
  708. return create_client_response(code=401, msg="用户不存在", db=db)
  709. # 检查用户是否过期
  710. if user.expire_time and datetime_to_beijing(user.expire_time) < beijing_now():
  711. logger.warning(f"[会话验证] 用户 {user.username} 卡密已过期")
  712. return create_client_response(code=403, msg="卡密已过期", db=db)
  713. # 检查用户状态
  714. if user.status != 1:
  715. logger.warning(f"[会话验证] 用户 {user.username} 状态异常: {user.status}")
  716. return create_client_response(code=403, msg="账号状态异常", db=db)
  717. # 更新会话活动时间
  718. SessionService.update_session_activity(db, session.session_id)
  719. # 准备响应数据
  720. response_data = ValidateSessionResponseData(
  721. user_id=user.id,
  722. username=user.username,
  723. expire_time=user.expire_time.isoformat() if user.expire_time else None,
  724. is_active=user.status == 1,
  725. machine_code=user.machine_code or ""
  726. )
  727. logger.info(f"[会话验证] 用户 {user.username} 会话验证成功")
  728. return create_client_response(data=response_data.dict(), db=db)
  729. except Exception as e:
  730. logger.error(f"[会话验证] 验证失败: {e}")
  731. return create_client_response(code=500, msg="会话验证失败", db=db)
  732. @router.post("/activate-card")
  733. async def activate_card(
  734. user: User = Depends(get_current_user_session),
  735. request: Request = None,
  736. db: Session = Depends(get_db)
  737. ):
  738. """激活卡密(充值)"""
  739. try:
  740. logger.info(f"[卡密激活] 开始处理激活请求 - 用户: {user.username}(ID:{user.id})")
  741. # 处理客户端请求
  742. api_request, response_headers = process_client_request(request, db)
  743. # 验证操作类型
  744. if api_request.action != ApiActions.ACTIVATE_CARD:
  745. logger.warning(f"[卡密激活] 操作类型不匹配: {api_request.action}")
  746. return create_client_response(code=400, msg="操作类型不匹配", db=db)
  747. # 解析激活参数
  748. activate_params = ActivateCardRequest(**api_request.params)
  749. logger.info(f"[卡密激活] 激活参数 - 用户ID: {activate_params.user_id}, 卡密: {activate_params.card_code[:8]}...")
  750. # 验证用户ID是否匹配当前登录用户
  751. if activate_params.user_id != user.id:
  752. return create_client_response(code=403, msg="无权操作其他用户", db=db)
  753. # 验证卡密
  754. card = db.query(Card).filter(
  755. Card.card_code == activate_params.card_code,
  756. Card.status == 0
  757. ).first()
  758. if not card:
  759. return create_client_response(code=400, msg="卡密无效或已被使用", db=db)
  760. # 检查软件特征码是否匹配
  761. software_code = api_request.params.get("software_code", "")
  762. if card.software_code != software_code:
  763. return create_client_response(code=400, msg="卡密与当前软件不匹配", db=db)
  764. # 计算新的到期时间
  765. expire_time_before = user.expire_time
  766. if expire_time_before and datetime_to_beijing(expire_time_before) > beijing_now():
  767. # 如果还有有效期,则累加
  768. expire_time_after = expire_time_before + timedelta(days=card.days)
  769. else:
  770. # 如果已过期或没有有效期,则从当前时间开始计算
  771. expire_time_after = beijing_now() + timedelta(days=card.days)
  772. # 更新用户到期时间
  773. user.expire_time = expire_time_after
  774. user.updated_at = beijing_now()
  775. # 更新卡密状态
  776. card.user_id = user.id
  777. card.activated_at = beijing_now()
  778. card.expire_time = expire_time_after
  779. card.status = 1
  780. card.updated_at = beijing_now()
  781. # 创建充值记录
  782. recharge_record = RechargeRecord(
  783. user_id=user.id,
  784. card_id=card.id,
  785. software_code=card.software_code,
  786. days=card.days,
  787. expire_time_before=expire_time_before,
  788. expire_time_after=expire_time_after,
  789. created_at=beijing_now()
  790. )
  791. db.add(recharge_record)
  792. # 创建历史记录
  793. from app.utils.history import create_user_history, create_card_history, create_recharge_record_history
  794. create_user_history(db, user, "UPDATE")
  795. create_card_history(db, card, "UPDATE")
  796. create_recharge_record_history(db, recharge_record, "INSERT")
  797. # 一次性提交所有更改
  798. db.commit()
  799. # 准备响应数据
  800. response_data = ActivateCardResponseData(
  801. expire_time=expire_time_after.isoformat(),
  802. days=card.days
  803. )
  804. logger.info(f"用户 {user.username} 激活卡密成功: {activate_params.card_code}")
  805. return create_client_response(data=response_data.dict(), db=db)
  806. except Exception as e:
  807. db.rollback()
  808. logger.error(f"激活卡密失败: {e}")
  809. return create_client_response(code=500, msg="激活失败", db=db)
  810. @router.get("/recharge-records")
  811. async def get_recharge_records(
  812. request: Request,
  813. db: Session = Depends(get_db)
  814. ):
  815. """获取充值记录"""
  816. try:
  817. # 获取请求参数
  818. params = dict(request.query_params)
  819. # 解密请求数据(如果有)
  820. user_id = None
  821. if settings.ENCRYPT_ENABLED:
  822. # GET请求可能通过query参数传递加密数据
  823. encrypted_data = params.get("encrypted", "")
  824. if encrypted_data:
  825. decrypted = encryptor.decrypt(encrypted_data)
  826. user_id = decrypted.get("user_id")
  827. else:
  828. user_id = params.get("user_id")
  829. if user_id:
  830. user_id = int(user_id)
  831. if not user_id:
  832. return {"code": 400, "msg": "缺少用户ID"}
  833. # 查询充值记录
  834. records = db.query(RechargeRecord).filter(
  835. RechargeRecord.user_id == user_id,
  836. RechargeRecord.is_deleted == 0
  837. ).order_by(RechargeRecord.created_at.desc()).all()
  838. # 格式化返回数据
  839. records_data = []
  840. for record in records:
  841. # 获取卡密代码
  842. card_code = ""
  843. if record.card_id:
  844. card = db.query(Card).filter(Card.id == record.card_id).first()
  845. if card:
  846. card_code = card.card_code
  847. records_data.append({
  848. "id": record.id,
  849. "card_code": card_code,
  850. "days": record.days,
  851. "expire_time_before": record.expire_time_before.isoformat() if record.expire_time_before else None,
  852. "expire_time_after": record.expire_time_after.isoformat() if record.expire_time_after else None,
  853. "created_at": record.created_at.isoformat() if record.created_at else None
  854. })
  855. return {
  856. "code": 200,
  857. "msg": "成功",
  858. "data": records_data
  859. }
  860. except Exception as e:
  861. logger.exception("获取充值记录失败")
  862. return {"code": 500, "msg": "系统异常,操作未完成,请稍后重试;若持续发生请联系客服"}