announcement.py 9.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. """
  2. 公告接口(客户端使用)- 加密版本
  3. """
  4. import hmac
  5. import hashlib
  6. import base64
  7. import json
  8. import secrets
  9. from typing import Optional, Dict, Any
  10. from fastapi import APIRouter, Depends, HTTPException
  11. from sqlalchemy.orm import Session
  12. from sqlalchemy import or_
  13. from pydantic import BaseModel
  14. from cryptography.hazmat.primitives import hashes
  15. from cryptography.hazmat.primitives.asymmetric import padding
  16. from app.database import get_db
  17. from app.models.announcement import Announcement
  18. from app.core.encrypt import encryptor
  19. from app.utils.timestamp_validator import TimestampValidator
  20. from app.config import settings
  21. from app.core.logger import get_client_logger
  22. logger = get_client_logger()
  23. router = APIRouter()
  24. class AnnouncementRequest(BaseModel):
  25. """公告获取请求"""
  26. encrypted_params: str # Base64编码的AES加密参数
  27. aes_key: str # Base64编码的AES密钥(RSA加密后)
  28. key_version: int # 公钥版本
  29. timestamp: int # 请求时间戳
  30. nonce: str # 随机字符串,防止重放攻击
  31. signature: str # HMAC签名
  32. def _generate_signature(data: str, secret_key: str) -> str:
  33. """生成HMAC签名"""
  34. signature = hmac.new(
  35. secret_key.encode(),
  36. data.encode(),
  37. hashlib.sha256
  38. ).digest()
  39. return base64.b64encode(signature).decode()
  40. def _verify_signature(data: str, signature: str, secret_key: str) -> bool:
  41. """验证HMAC签名"""
  42. try:
  43. expected_signature = _generate_signature(data, secret_key)
  44. return signature == expected_signature
  45. except Exception as e:
  46. logger.exception("签名验证异常")
  47. return False
  48. @router.post("")
  49. async def get_announcement(
  50. request: AnnouncementRequest,
  51. db: Session = Depends(get_db),
  52. ):
  53. """
  54. 获取公告列表(支持检索与分页)- 加密版本
  55. 安全特性:
  56. - AES加密传输请求参数
  57. - RSA加密AES密钥传输
  58. - 时间戳验证防止重放攻击
  59. - HMAC签名确保数据完整性
  60. """
  61. try:
  62. # 1. 验证时间戳
  63. is_valid, error_msg = TimestampValidator.validate_timestamp(request.timestamp, db)
  64. if not is_valid:
  65. logger.warning(f"公告获取请求时间戳验证失败: {error_msg}")
  66. return {
  67. "code": 400,
  68. "msg": f"时间戳验证失败: {error_msg}",
  69. "timestamp": TimestampValidator.generate_timestamp()
  70. }
  71. # 2. 验证签名
  72. signature_data = f"{request.timestamp}:{request.nonce}:{request.encrypted_params}:{request.aes_key}:{request.key_version}"
  73. if not _verify_signature(signature_data, request.signature, settings.SECRET_KEY):
  74. logger.warning("公告获取请求签名验证失败")
  75. return {
  76. "code": 400,
  77. "msg": "签名验证失败",
  78. "timestamp": TimestampValidator.generate_timestamp()
  79. }
  80. # 3. 使用RSA私钥解密AES密钥
  81. try:
  82. # 确保使用正确的密钥版本
  83. if not encryptor.load_key_by_version(db, request.key_version):
  84. logger.error(f"无法加载密钥版本 {request.key_version}")
  85. return {
  86. "code": 400,
  87. "msg": "无效的密钥版本",
  88. "timestamp": TimestampValidator.generate_timestamp()
  89. }
  90. # 解密AES密钥
  91. encrypted_aes_key_bytes = base64.b64decode(request.aes_key)
  92. aes_key_bytes = encryptor.private_key.decrypt(
  93. encrypted_aes_key_bytes,
  94. padding.OAEP(
  95. mgf=padding.MGF1(algorithm=hashes.SHA256()),
  96. algorithm=hashes.SHA256(),
  97. label=None
  98. )
  99. )
  100. except Exception as e:
  101. logger.exception("AES密钥解密失败")
  102. return {
  103. "code": 400,
  104. "msg": "密钥解密失败",
  105. "timestamp": TimestampValidator.generate_timestamp()
  106. }
  107. # 4. 使用AES解密请求参数 (CTR模式,与前端保持一致)
  108. try:
  109. encrypted_params_bytes = base64.b64decode(request.encrypted_params)
  110. # 解析AES加密的数据:IV(16) + 加密数据
  111. if len(encrypted_params_bytes) < 16:
  112. raise ValueError("加密数据长度无效")
  113. iv = encrypted_params_bytes[:16]
  114. encrypted_payload = encrypted_params_bytes[16:]
  115. # AES CTR模式解密
  116. from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
  117. from cryptography.hazmat.backends import default_backend
  118. cipher = Cipher(algorithms.AES(aes_key_bytes), modes.CTR(iv), backend=default_backend())
  119. decryptor = cipher.decryptor()
  120. decrypted_bytes = decryptor.update(encrypted_payload) + decryptor.finalize()
  121. # JSON解析
  122. params: Dict[str, Any] = json.loads(decrypted_bytes.decode('utf-8'))
  123. # 记录解密后的参数(用于调试)
  124. logger.debug(f"[公告接口解密] 解密后的请求参数: {json.dumps(params, ensure_ascii=False, indent=2)}")
  125. except Exception as e:
  126. logger.exception("请求参数解密失败")
  127. return {
  128. "code": 400,
  129. "msg": "参数解密失败",
  130. "timestamp": TimestampValidator.generate_timestamp()
  131. }
  132. # 5. 提取业务参数
  133. page = params.get('page', 1)
  134. page_size = params.get('page_size', 10)
  135. keyword = params.get('keyword')
  136. # 参数验证
  137. if not isinstance(page, int) or page < 1:
  138. page = 1
  139. if not isinstance(page_size, int) or page_size < 1 or page_size > 100:
  140. page_size = 10
  141. if keyword and not isinstance(keyword, str):
  142. keyword = None
  143. # 6. 执行业务逻辑
  144. offset = (page - 1) * page_size
  145. query = db.query(Announcement).filter(Announcement.is_active == 1, Announcement.is_deleted == 0)
  146. if keyword:
  147. like_pattern = f"%{keyword}%"
  148. query = query.filter(
  149. or_(
  150. Announcement.title.like(like_pattern),
  151. Announcement.content.like(like_pattern),
  152. )
  153. )
  154. # 按优先级降序(紧急->重要->普通),然后按创建时间降序
  155. query = query.order_by(Announcement.priority.desc(), Announcement.created_at.desc(), Announcement.id.desc())
  156. total = query.count()
  157. announcements = query.offset(offset).limit(page_size).all()
  158. # 7. 准备响应数据
  159. response_data = {
  160. "data": [
  161. {
  162. "id": a.id,
  163. "title": a.title,
  164. "content": a.content,
  165. "publish_time": a.publish_time.isoformat() if a.publish_time else None,
  166. "created_at": a.created_at.isoformat() if a.created_at else None,
  167. }
  168. for a in announcements
  169. ],
  170. "total": total,
  171. "page": page,
  172. "page_size": page_size,
  173. }
  174. # 8. 记录明文响应数据日志
  175. logger.debug(f"[公告接口响应] 明文响应数据: {json.dumps(response_data, ensure_ascii=False, indent=2)}")
  176. # 9. 使用AES加密响应数据 (CTR模式,与前端保持一致)
  177. # 🔐 安全设计:使用请求中解密出的同一个AES密钥加密响应
  178. # 这样前端可以用本地保存的AES密钥直接解密,无需额外的RSA密钥交换
  179. response_json = json.dumps(response_data, ensure_ascii=False, separators=(',', ':'))
  180. # 使用解密出的AES密钥加密响应
  181. response_iv = secrets.token_bytes(16)
  182. from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
  183. from cryptography.hazmat.backends import default_backend
  184. cipher = Cipher(algorithms.AES(aes_key_bytes), modes.CTR(response_iv), backend=default_backend())
  185. encryptor_obj = cipher.encryptor()
  186. data_bytes = response_json.encode('utf-8')
  187. # CTR模式不需要填充
  188. encrypted_response = encryptor_obj.update(data_bytes) + encryptor_obj.finalize()
  189. # 组合:IV(16) + 加密数据
  190. encrypted_response_with_iv = response_iv + encrypted_response
  191. # 9. 生成响应签名
  192. timestamp = TimestampValidator.generate_timestamp()
  193. response_signature_data = f"{timestamp}:{request.nonce}:{base64.b64encode(encrypted_response_with_iv).decode()}"
  194. response_signature = _generate_signature(response_signature_data, settings.SECRET_KEY)
  195. # 10. 返回加密响应
  196. # 注意:这里不返回encrypted_key,因为前端已经知道AES密钥
  197. return {
  198. "code": 200,
  199. "msg": "成功",
  200. "encrypted_data": base64.b64encode(encrypted_response_with_iv).decode(),
  201. "signature": response_signature,
  202. "timestamp": timestamp,
  203. "nonce": request.nonce
  204. }
  205. except Exception as e:
  206. logger.exception("公告获取异常")
  207. return {
  208. "code": 500,
  209. "msg": "系统异常,请稍后重试;若持续发生请联系客服",
  210. "timestamp": TimestampValidator.generate_timestamp()
  211. }