ForcePilot/backend/package/yuxi/channels/plugins/dingtalk/signature.py
Kris ce0a7e7232 refactor(dingtalk): 完成钉钉适配器全量优化与功能完善
本次提交对钉钉插件进行了多维度改进:
1.  代码整洁优化:移除多余空行、调整导入顺序
2.  签名逻辑修正:修复hmac签名参数顺序,对齐官方文档
3.  消息撤回增强:使用枚举替代硬编码字符串,添加缓存未命中日志
4.  配置与能力扩展:新增互动卡片模板ID、Bot命令配置项,重构机器人编码获取逻辑
5.  流式会话优化:移除硬编码默认卡片ID,动态读取账户配置
6.  健康检查增强:新增流式端点连通性校验,替代原有简化校验逻辑
7.  附件下载升级:适配钉钉图片下载API,新增图片附件下载能力
8.  封装改进:将私有配置读取方法封装为公开接口,避免破坏封装性
2026-07-09 04:18:06 +08:00

114 lines
3.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

"""钉钉 HTTP 模式签名校验。
实现钉钉 HTTP 模式 Webhook 回调的签名验签(设计方案 §3.3)。钉钉 HTTP
模式签名规则:
1. 拼接 ``timestamp + "\\n" + sign_secret``
2. 计算 HMAC-SHA256 拼接字符串Base64 编码
3. 与回调中的 ``sign`` 字段比对(恒定时间比较防时序攻击)
4. ``timestamp`` 与当前时间差超过 60 分钟拒绝(防重放)
Stream 模式由 SDK 内置 TLS 加密,无需签名校验(``verifySignature`` 直接
返回 ``true``)。
依赖方向:仅标准库 + ``yuxi.channels.contract.errors.*``,不污染框架层。
"""
from __future__ import annotations
import base64
import hashlib
import hmac
import time
from typing import Any
from yuxi.channels.contract.errors import ValidationError
from ._constants import HTTP_SIGN_TIMESTAMP_TOLERANCE_SEC
def compute_signature(timestamp: int, sign_secret: str) -> str:
"""计算钉钉 HTTP 模式签名。
钉钉官方签名规则(对齐开放平台文档):
- ``string_to_sign = f"{timestamp}\\n{sign_secret}"``
- ``hmac.new(key=sign_secret, msg=string_to_sign, digestmod=sha256)``
Args:
timestamp: 回调中的 ``timestamp`` 字段(毫秒级时间戳)。
sign_secret: HTTP 模式签名密钥(``sign_secret`` 配置项)。
Returns:
Base64 编码的 HMAC-SHA256 签名字符串。
"""
string_to_sign = f"{timestamp}\n{sign_secret}"
hmac_code = hmac.new(
key=sign_secret.encode("utf-8"),
msg=string_to_sign.encode("utf-8"),
digestmod=hashlib.sha256,
).digest()
return base64.b64encode(hmac_code).decode("utf-8")
def verify_signature(
timestamp: int,
sign: str,
sign_secret: str,
*,
now_ts: int | None = None,
) -> bool:
"""校验钉钉 HTTP 模式签名。
Args:
timestamp: 回调中的 ``timestamp`` 字段(毫秒级时间戳)。
sign: 回调中的 ``sign`` 字段(待校验签名)。
sign_secret: HTTP 模式签名密钥。
now_ts: 当前时间戳(毫秒),用于测试注入;为 ``None`` 时取
``int(time.time() * 1000)``。
Returns:
``True`` 表示签名通过;``False`` 表示签名失败或时间戳过期。
本函数 **不抛异常**,调用方据返回值决定是否终止入站管道。
"""
if not sign or not sign_secret or not timestamp:
return False
now = now_ts if now_ts is not None else int(time.time() * 1000)
if abs(now - timestamp) > HTTP_SIGN_TIMESTAMP_TOLERANCE_SEC * 1000:
return False
expected = compute_signature(timestamp, sign_secret)
return hmac.compare_digest(expected, sign)
def extract_timestamp_sign(payload: dict[str, Any]) -> tuple[int, str]:
"""从钉钉 HTTP 模式回调 payload 提取 timestamp 与 sign。
Args:
payload: 钉钉 HTTP 模式回调的完整 payload。
Returns:
``(timestamp, sign)`` 元组。字段缺失时返回 ``(0, "")``。
Raises:
ValidationError: timestamp 字段存在但非整数。
"""
timestamp = payload.get("timestamp", 0)
if timestamp and not isinstance(timestamp, int):
try:
timestamp = int(timestamp)
except (TypeError, ValueError) as exc:
raise ValidationError(
field="timestamp",
message=f"timestamp must be integer: {timestamp}",
) from exc
sign = payload.get("sign", "") or ""
return int(timestamp), sign
__all__ = [
"compute_signature",
"verify_signature",
"extract_timestamp_sign",
]