ForcePilot/backend/package/yuxi/channel/extensions/alexa/security.py
Kris 37493b6da6 feat(alexa): 新增Amazon Alexa渠道插件完整实现
新增了Alexa渠道的所有核心功能模块,包括配置管理、请求校验、会话管理、安全验证、去重、响应格式化、渐进式响应、主动推送、提醒功能、配对绑定以及Webhook端点,并完成了插件注册和配置项定义。
2026-05-21 10:38:31 +08:00

99 lines
3.5 KiB
Python

import logging
import time
import httpx
logger = logging.getLogger(__name__)
_DEFAULT_TOKEN_CACHE_TTL = 300
class AlexaSecurity:
VALID_POLICIES = {"open", "pairing", "allowlist", "disabled"}
def __init__(self, account):
self._account = account
self._policy = account.dm_policy if account.dm_policy in self.VALID_POLICIES else "open"
self._allowlist: set[str] = set(account.allow_from) if account.allow_from else set()
self._account_linking_enabled = getattr(account, "account_linking_enabled", False)
self._oauth_client_id = getattr(account, "oauth_client_id", "")
self._oauth_client_secret = getattr(account, "oauth_client_secret", "")
self._oauth_introspect_url = getattr(account, "oauth_introspect_url", "")
self._token_cache: dict[str, dict] = {}
self._http = httpx.Client(timeout=httpx.Timeout(10.0))
@property
def account_linking_enabled(self) -> bool:
return self._account_linking_enabled
def resolve_dm_policy(self) -> str:
return self._policy
def check_allowlist(self, user_id: str) -> bool:
if self._policy == "open":
return True
if self._policy == "disabled":
return False
if not self._allowlist:
return False
return user_id in self._allowlist
def add_to_allowlist(self, user_id: str):
self._allowlist.add(user_id)
def remove_from_allowlist(self, user_id: str):
self._allowlist.discard(user_id)
def verify_access_token(self, access_token: str) -> dict:
if not access_token:
return {"user_id": None, "verified": False}
cached = self._token_cache.get(access_token)
if cached and cached.get("expires_at", 0) > time.time():
return cached
result = self._do_verify(access_token)
result["expires_at"] = result.get("expires_at", time.time() + _DEFAULT_TOKEN_CACHE_TTL)
self._token_cache[access_token] = result
return result
def _do_verify(self, access_token: str) -> dict:
if self._oauth_introspect_url:
return self._verify_via_introspect(access_token)
return self._verify_via_jwt(access_token)
def _verify_via_introspect(self, access_token: str) -> dict:
try:
resp = self._http.post(
self._oauth_introspect_url,
json={"token": access_token},
headers={"Authorization": f"Bearer {self._oauth_client_secret}"},
)
if resp.status_code == 200:
data = resp.json()
return {
"user_id": data.get("sub") or data.get("user_id"),
"verified": data.get("active", False),
}
logger.warning("OAuth introspect returned %d", resp.status_code)
except Exception:
logger.exception("OAuth token introspection failed")
return {"user_id": None, "verified": False}
def _verify_via_jwt(self, access_token: str) -> dict:
try:
from server.utils.auth_utils import AuthUtils
payload = AuthUtils.verify_access_token(access_token)
user_id = payload.get("sub")
if user_id:
return {"user_id": user_id, "verified": True}
except Exception:
logger.exception("JWT token verification failed")
return {"user_id": None, "verified": False}
def close(self):
if self._http:
self._http.close()