本次提交对钉钉插件进行了多维度改进: 1. 代码整洁优化:移除多余空行、调整导入顺序 2. 签名逻辑修正:修复hmac签名参数顺序,对齐官方文档 3. 消息撤回增强:使用枚举替代硬编码字符串,添加缓存未命中日志 4. 配置与能力扩展:新增互动卡片模板ID、Bot命令配置项,重构机器人编码获取逻辑 5. 流式会话优化:移除硬编码默认卡片ID,动态读取账户配置 6. 健康检查增强:新增流式端点连通性校验,替代原有简化校验逻辑 7. 附件下载升级:适配钉钉图片下载API,新增图片附件下载能力 8. 封装改进:将私有配置读取方法封装为公开接口,避免破坏封装性
114 lines
3.6 KiB
Python
114 lines
3.6 KiB
Python
"""钉钉 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",
|
||
]
|