This commit is contained in:
zwt13703 2026-04-27 17:25:11 +08:00
parent 052c72eb5a
commit 43561548fd
2 changed files with 43 additions and 20 deletions

View File

@ -29,14 +29,20 @@ async def verify_callback(
@router.post("/webhook") @router.post("/webhook")
async def receive_callback(request: Request, db: AsyncSession = Depends(get_db)): async def receive_callback(
"""接收消息回调:解密 XML → 拉取消息 → 入库""" request: Request,
msg_signature: str = Query("", alias="msg_signature"),
timestamp: str = Query(""),
nonce: str = Query(""),
db: AsyncSession = Depends(get_db),
):
"""接收消息回调:验证签名 → 解密 XML → 拉取消息 → 入库"""
try: try:
xml_body = await request.body() xml_body = await request.body()
xml_str = xml_body.decode("utf-8") 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: if not token or not open_kfid:
logger.warning("解密后 Token 或 OpenKfId 为空") logger.warning("解密后 Token 或 OpenKfId 为空")
return PlainTextResponse("fail") return PlainTextResponse("fail")

View File

@ -66,22 +66,33 @@ def verify_url(msg_signature: str, timestamp: str, nonce: str, echostr: str) ->
return decrypt(echostr) return decrypt(echostr)
def decrypt_message(xml_body: str) -> tuple[str, str]: def decrypt_message(xml_body: str, msg_signature: str = "",
"""POST 请求:解密 XML 消息,返回 (token, open_kfid) timestamp: str = "", nonce: str = "") -> tuple[str, str]:
"""POST 请求:验证签名 + 解密 XML 消息,返回 (token, open_kfid)
注意解密后的 XML 包含 <Token> <OpenKfId> 等字段 解密后的 XML 包含 <Token> <OpenKfId> 等字段
""" """
root = ET.fromstring(xml_body) root = ET.fromstring(xml_body)
encrypt_elem = root.find("Encrypt") encrypt_elem = root.find("Encrypt")
if encrypt_elem is None or encrypt_elem.text is None: if encrypt_elem is None or encrypt_elem.text is None:
raise ValueError("XML 中缺少 Encrypt 字段") 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) plain_text = decrypt(encrypt_str)
# 解析解密后的 XML
plain_root = ET.fromstring(plain_text)
# 解密后的内容可能是 XML 或 JSON取决于微信客服的配置
# 先尝试 XML 解析
try:
plain_root = ET.fromstring(plain_text)
token = "" token = ""
open_kfid = "" open_kfid = ""
token_elem = plain_root.find("Token") token_elem = plain_root.find("Token")
@ -90,5 +101,11 @@ def decrypt_message(xml_body: str) -> tuple[str, str]:
token = token_elem.text or "" token = token_elem.text or ""
if kfid_elem is not None: if kfid_elem is not None:
open_kfid = kfid_elem.text or "" 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 return token, open_kfid