```
feat: 添加飞书用户模块和订阅功能支持 - 新增feishu_users模块用于处理飞书用户身份验证和权限管理 - 新增subscriptions模块用于处理订阅相关功能 - 新增personalization模块用于个性化服务 - 在alembic迁移配置中注册新的模型模块 - 在API路由器中添加feishu_users和subscriptions路由 - 实现事件调度服务的改进,包括错误处理和状态更新优化 - 添加飞书命令处理的权限检查机制 - 实现飞书应用票据事件处理 - 改进审计日志记录功能 ```
This commit is contained in:
131
app/modules/feishu/event_verification.py
Normal file
131
app/modules/feishu/event_verification.py
Normal file
@@ -0,0 +1,131 @@
|
||||
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",
|
||||
)
|
||||
Reference in New Issue
Block a user