Files
company-ai-platform/app/modules/feishu/event_verification.py
JiuContinent d7db84571d ```
feat: 添加飞书用户模块和订阅功能支持

- 新增feishu_users模块用于处理飞书用户身份验证和权限管理
- 新增subscriptions模块用于处理订阅相关功能
- 新增personalization模块用于个性化服务
- 在alembic迁移配置中注册新的模型模块
- 在API路由器中添加feishu_users和subscriptions路由
- 实现事件调度服务的改进,包括错误处理和状态更新优化
- 添加飞书命令处理的权限检查机制
- 实现飞书应用票据事件处理
- 改进审计日志记录功能
```
2026-07-27 08:02:17 +08:00

132 lines
5.1 KiB
Python

import base64
import json
import time
from hashlib import sha256
from secrets import compare_digest
from typing import Any, Mapping
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
from cryptography.hazmat.primitives.padding import PKCS7
from fastapi import HTTPException, status
from app.core.config import get_settings
from app.modules.feishu.constants import FeishuPayloadKey
_SIGNATURE_MAX_AGE_SECONDS = 300
_SIGNATURE_HEADER = "x-lark-signature"
_TIMESTAMP_HEADER = "x-lark-request-timestamp"
_NONCE_HEADER = "x-lark-request-nonce"
class FeishuWebhookVerifier:
"""Verify, decrypt, and normalize an HTTP webhook before business handling."""
def verify(
self,
raw_body: bytes,
headers: Mapping[str, str],
) -> dict[str, Any]:
settings = get_settings()
if settings.feishu_encrypt_key:
self._verify_signature(raw_body, headers, settings.feishu_encrypt_key)
payload = self._load_json(raw_body)
if FeishuPayloadKey.ENCRYPT in payload:
if not settings.feishu_encrypt_key:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="FEISHU_ENCRYPT_KEY is required for encrypted webhooks",
)
payload = self._decrypt(
str(payload[FeishuPayloadKey.ENCRYPT]),
settings.feishu_encrypt_key,
)
self._verify_token(payload, settings.feishu_verification_token)
return payload
@staticmethod
def _load_json(raw_body: bytes) -> dict[str, Any]:
try:
payload = json.loads(raw_body)
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid Feishu webhook JSON",
) from exc
if not isinstance(payload, dict):
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Invalid Feishu webhook payload",
)
return payload
@staticmethod
def _verify_signature(
raw_body: bytes,
headers: Mapping[str, str],
encrypt_key: str,
) -> None:
normalized = {str(key).lower(): str(value) for key, value in headers.items()}
timestamp = normalized.get(_TIMESTAMP_HEADER)
nonce = normalized.get(_NONCE_HEADER)
signature = normalized.get(_SIGNATURE_HEADER)
if not timestamp or not nonce or not signature:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing Feishu webhook signature headers",
)
try:
request_time = int(timestamp)
except ValueError as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Feishu webhook timestamp",
) from exc
if abs(int(time.time()) - request_time) > _SIGNATURE_MAX_AGE_SECONDS:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Expired Feishu webhook signature",
)
signed = (
timestamp.encode("utf-8")
+ nonce.encode("utf-8")
+ encrypt_key.encode("utf-8")
+ raw_body
)
expected = sha256(signed).hexdigest()
if not compare_digest(signature, expected):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Feishu webhook signature",
)
@staticmethod
def _decrypt(encrypted: str, encrypt_key: str) -> dict[str, Any]:
try:
key = sha256(encrypt_key.encode("utf-8")).digest()
encrypted_bytes = base64.b64decode(encrypted, validate=True)
decryptor = Cipher(algorithms.AES(key), modes.CBC(key[:16])).decryptor()
padded = decryptor.update(encrypted_bytes) + decryptor.finalize()
unpadder = PKCS7(algorithms.AES.block_size).unpadder()
cleartext = unpadder.update(padded) + unpadder.finalize()
except (ValueError, TypeError) as exc:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid encrypted Feishu webhook",
) from exc
return FeishuWebhookVerifier._load_json(cleartext)
@staticmethod
def _verify_token(payload: dict[str, Any], expected: str | None) -> None:
if not expected:
raise HTTPException(
status_code=status.HTTP_503_SERVICE_UNAVAILABLE,
detail="FEISHU_VERIFICATION_TOKEN is required",
)
header = payload.get(FeishuPayloadKey.HEADER) or {}
token = payload.get(FeishuPayloadKey.TOKEN) or header.get(FeishuPayloadKey.TOKEN)
if not token or not compare_digest(str(token), expected):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Invalid Feishu token",
)