diff --git a/app/routes/webhook.py b/app/routes/webhook.py index e9f87f0..de47f7d 100644 --- a/app/routes/webhook.py +++ b/app/routes/webhook.py @@ -29,14 +29,20 @@ async def verify_callback( @router.post("/webhook") -async def receive_callback(request: Request, db: AsyncSession = Depends(get_db)): - """接收消息回调:解密 XML → 拉取消息 → 入库""" +async def receive_callback( + request: Request, + msg_signature: str = Query("", alias="msg_signature"), + timestamp: str = Query(""), + nonce: str = Query(""), + db: AsyncSession = Depends(get_db), +): + """接收消息回调:验证签名 → 解密 XML → 拉取消息 → 入库""" try: xml_body = await request.body() xml_str = xml_body.decode("utf-8") - logger.info(f"收到回调: {xml_str[:200]}") + logger.info(f"收到回调, encrypt 前100字符: {xml_str[:100]}") - token, open_kfid = decrypt_message(xml_str) + token, open_kfid = decrypt_message(xml_str, msg_signature, timestamp, nonce) if not token or not open_kfid: logger.warning("解密后 Token 或 OpenKfId 为空") return PlainTextResponse("fail") diff --git a/app/services/crypto.py b/app/services/crypto.py index ba30c2c..ef4acc1 100644 --- a/app/services/crypto.py +++ b/app/services/crypto.py @@ -66,29 +66,46 @@ def verify_url(msg_signature: str, timestamp: str, nonce: str, echostr: str) -> return decrypt(echostr) -def decrypt_message(xml_body: str) -> tuple[str, str]: - """POST 请求:解密 XML 消息,返回 (token, open_kfid) +def decrypt_message(xml_body: str, msg_signature: str = "", + timestamp: str = "", nonce: str = "") -> tuple[str, str]: + """POST 请求:验证签名 + 解密 XML 消息,返回 (token, open_kfid) - 注意:解密后的 XML 包含 等字段 + 解密后的 XML 包含 等字段 """ root = ET.fromstring(xml_body) encrypt_elem = root.find("Encrypt") if encrypt_elem is None or encrypt_elem.text is None: raise ValueError("XML 中缺少 Encrypt 字段") - encrypt_str = encrypt_elem.text + + # 去除可能的空白/换行,确保 Base64 解码正确 + encrypt_str = encrypt_elem.text.strip() + + # 验证签名(如果提供了签名参数) + if msg_signature and timestamp and nonce: + if not verify_signature(settings.callback_token, timestamp, nonce, + encrypt_str, msg_signature): + raise ValueError("消息签名验证失败") # 解密 plain_text = decrypt(encrypt_str) - # 解析解密后的 XML - plain_root = ET.fromstring(plain_text) - token = "" - open_kfid = "" - token_elem = plain_root.find("Token") - kfid_elem = plain_root.find("OpenKfId") - if token_elem is not None: - token = token_elem.text or "" - if kfid_elem is not None: - open_kfid = kfid_elem.text or "" - - return token, open_kfid + # 解密后的内容可能是 XML 或 JSON(取决于微信客服的配置) + # 先尝试 XML 解析 + try: + plain_root = ET.fromstring(plain_text) + token = "" + open_kfid = "" + token_elem = plain_root.find("Token") + kfid_elem = plain_root.find("OpenKfId") + if token_elem is not None: + token = token_elem.text or "" + if kfid_elem is not None: + open_kfid = kfid_elem.text or "" + return token, open_kfid + except ET.ParseError: + # 可能是 JSON 格式 + import json + data = json.loads(plain_text) + token = data.get("Token", "") + open_kfid = data.get("OpenKfId", "") + return token, open_kfid