diff --git a/app/application/configuration.py b/app/application/configuration.py index 41371afb2..59f62d669 100644 --- a/app/application/configuration.py +++ b/app/application/configuration.py @@ -72,6 +72,16 @@ class TransferRetryConfig: max_failed_retries: Any +@dataclass(frozen=True, slots=True) +class TokenRuntimeConfig: + """令牌编解码在一次宿主生命周期内使用的安全配置快照。""" + + secret_key: str + resource_secret_key: str + access_token_expire_minutes: int + resource_access_token_expire_seconds: int + + @dataclass(frozen=True, slots=True) class ApiRuntimeConfig: """单次 API 请求使用的宿主配置快照。""" @@ -318,6 +328,7 @@ class SystemConfigService: _configured_system_config: SystemConfigService | None = None _transfer_retry_config_provider: Callable[[], TransferRetryConfig] | None = None +_token_runtime_config_provider: Callable[[], TokenRuntimeConfig] | None = None _runtime_configuration: RuntimeConfiguration | None = None _runtime_settings_service: RuntimeSettingsService | None = None @@ -350,6 +361,21 @@ def get_transfer_retry_config() -> TransferRetryConfig: return _transfer_retry_config_provider() +def configure_token_runtime_config( + provider: Callable[[], TokenRuntimeConfig], +) -> None: + """由启动组合根登记令牌安全配置快照工厂。""" + global _token_runtime_config_provider + _token_runtime_config_provider = provider + + +def get_token_runtime_config() -> TokenRuntimeConfig: + """返回当前令牌编解码使用的不可变配置快照。""" + if _token_runtime_config_provider is None: + raise RuntimeError("令牌运行时配置尚未装配") + return _token_runtime_config_provider() + + def configure_runtime_configuration(configuration: RuntimeConfiguration) -> None: """由启动组合根登记各运行面使用的类型化配置快照工厂。""" global _runtime_configuration diff --git a/app/application/security/token.py b/app/application/security/token.py index 77f098180..ede3fdccd 100644 --- a/app/application/security/token.py +++ b/app/application/security/token.py @@ -4,6 +4,7 @@ import base64 import datetime import hashlib import hmac +import importlib import json import os import traceback @@ -16,7 +17,7 @@ from Crypto.Cipher import AES from Crypto.Util.Padding import pad from cryptography.fernet import Fernet -from app.runtime.config import settings +from app.application.configuration import TokenRuntimeConfig, get_token_runtime_config from app.runtime.log import logger from app.schemas.token import TokenPayload @@ -25,6 +26,22 @@ BCRYPT_ROUNDS = 12 ALGORITHM = "HS256" +def _token_config() -> TokenRuntimeConfig: + """读取启动快照;未装配时保留旧插件的独立调用兼容。""" + try: + return get_token_runtime_config() + except RuntimeError: + legacy_settings = importlib.import_module("app.runtime.config").settings + return TokenRuntimeConfig( + secret_key=legacy_settings.SECRET_KEY, + resource_secret_key=legacy_settings.RESOURCE_SECRET_KEY, + access_token_expire_minutes=legacy_settings.ACCESS_TOKEN_EXPIRE_MINUTES, + resource_access_token_expire_seconds=( + legacy_settings.RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS + ), + ) + + class PasswordTooLongError(ValueError): """密码的 UTF-8 字节长度超过 bcrypt 可安全处理的上限。""" @@ -58,14 +75,15 @@ def create_access_token( purpose: Optional[str] = "authentication", ) -> str: """创建带身份、权限等级和用途声明的 JWT 访问令牌。""" + config = _token_config() if purpose == "resource": default_expire = timedelta( - seconds=settings.RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS + seconds=config.resource_access_token_expire_seconds ) - secret_key = settings.RESOURCE_SECRET_KEY + secret_key = config.resource_secret_key else: - default_expire = timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) - secret_key = settings.SECRET_KEY + default_expire = timedelta(minutes=config.access_token_expire_minutes) + secret_key = config.secret_key if expires_delta is not None: if expires_delta.total_seconds() <= 0: @@ -92,12 +110,13 @@ def decode_access_token( purpose: str = "authentication", ) -> TokenPayload: """校验 JWT 签名和用途并返回框架无关的令牌载荷。""" + config = _token_config() if not token: raise TokenValidationError(f"{purpose} token not found") secret_key = ( - settings.RESOURCE_SECRET_KEY + config.resource_secret_key if purpose == "resource" - else settings.SECRET_KEY + else config.secret_key ) try: payload = jwt.decode(token, secret_key, algorithms=[ALGORITHM]) diff --git a/app/startup/configuration.py b/app/startup/configuration.py index d7e408262..1e9335cb6 100644 --- a/app/startup/configuration.py +++ b/app/startup/configuration.py @@ -6,6 +6,7 @@ from app.application.configuration import ( ApiRuntimeConfig, ChainRuntimeConfig, SchedulerRuntimeConfig, + TokenRuntimeConfig, ) from app.runtime.config import Settings from app.schemas.types import MediaType @@ -52,6 +53,16 @@ def build_api_runtime_config(settings: Settings) -> ApiRuntimeConfig: ) +def build_token_runtime_config(settings: Settings) -> TokenRuntimeConfig: + """从部署设置构建令牌编解码使用的安全配置快照。""" + return TokenRuntimeConfig( + secret_key=settings.SECRET_KEY, + resource_secret_key=settings.RESOURCE_SECRET_KEY, + access_token_expire_minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES, + resource_access_token_expire_seconds=settings.RESOURCE_ACCESS_TOKEN_EXPIRE_SECONDS, + ) + + def build_scheduler_runtime_config(settings: Settings) -> SchedulerRuntimeConfig: """从可热更新的部署设置构建一次 Scheduler 配置快照。""" return SchedulerRuntimeConfig( diff --git a/app/startup/modules_initializer.py b/app/startup/modules_initializer.py index 2368ade1b..835908825 100644 --- a/app/startup/modules_initializer.py +++ b/app/startup/modules_initializer.py @@ -42,6 +42,7 @@ from app.application.configuration import ( SystemConfigService, get_configured_system_config, TransferRetryConfig, + configure_token_runtime_config, configure_runtime_configuration, configure_runtime_settings, configure_system_config, @@ -51,6 +52,7 @@ from app.startup.configuration import ( build_api_runtime_config, build_chain_runtime_config, build_scheduler_runtime_config, + build_token_runtime_config, ) from app.application.database import configure_database_governance from app.application.service import configure_service_directory @@ -594,6 +596,7 @@ async def init_modules() -> HostRuntime: ) configure_runtime_configuration(host_runtime.configuration) configure_runtime_settings(host_runtime.settings) + configure_token_runtime_config(lambda: build_token_runtime_config(settings)) # 先发布系统配置服务,后续启动组合步骤统一复用同一配置端口。 configure_system_config(SystemConfigService(repository=SystemConfigOper())) # 旧 app.api.data 导入只保留 ABI 转发,正式 API 依赖全部读取 HostRuntime。 diff --git a/docs/refactor/backend-architecture-next-stage.md b/docs/refactor/backend-architecture-next-stage.md index 4b3159718..9dcaebdaf 100644 --- a/docs/refactor/backend-architecture-next-stage.md +++ b/docs/refactor/backend-architecture-next-stage.md @@ -1016,6 +1016,7 @@ MFA/Passkey 专项测试与架构门禁通过,密钥类配置仍保留在安 2026-08-23 将工作流动作 `FetchRssAction`、`ScanFileAction` 和 `AddSubscribeAction` 接入 `ChainRuntimeConfig` 快照,分别移除代理、媒体后缀和超级用户的全局 `settings` 读取;保留动作公开入口与工作流上下文行为,新增快照注入测试覆盖。配置债务由 130 个文件降至 127 个文件,宿主依赖与配置基线已更新。 2026-08-23 将工作流动作 `FetchMediasAction` 和 `SendMessageAction` 接入 `ChainRuntimeConfig` 快照,分别移除内部 API 端口/令牌及工作流链接的全局 `settings` 读取;保留动作公开入口与消息载荷行为,新增快照注入测试覆盖。配置债务由 127 个文件降至 125 个文件,宿主依赖与配置基线已更新。 2026-08-23 将 API 路由前缀作为组合根参数传入 `init_routers`,移除路由初始化模块对全局 `settings` 的直接读取;默认参数保留旧调用兼容性,并补充自定义前缀测试。配置债务由 125 个文件降至 124 个文件。 +2026-08-23 将令牌编解码的密钥与过期策略接入 `TokenRuntimeConfig` 快照;启动组合根统一装配,未装配时保留 SDK/旧插件的动态回退,公开令牌函数签名不变。配置债务由 124 个文件降至 123 个文件,并补充资源/认证令牌回归测试。 **收口记录(2026-08-22)**:`reidentify_cache`、`nettest`、`scrape`、OpenAI `chat_completions/responses`、`get_logging` 和 Web Agent SSE 均改为稳定公开入口委托私有编排实现;四个消息交互 Handler 的公开方法也保留 ABI 并委托私有状态机。复杂度基线已清零,API/Application/Chain 入口预算、异步阻塞 ratchet 均通过;复杂度及兼容专项合计 252 项测试通过。 随后将 `TransferChain.do_transfer` 的公开入口收口为稳定兼容 Facade,先提取媒体身份规范化阶段,保留显式 diff --git a/tests/conftest.py b/tests/conftest.py index aa7efdaef..0ab210300 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -34,6 +34,7 @@ def configure_plugin_system_services(): RuntimeSettingsService, SystemConfigService, TransferRetryConfig, + configure_token_runtime_config, configure_runtime_configuration, configure_runtime_settings, configure_system_config, @@ -44,6 +45,7 @@ def configure_plugin_system_services(): build_api_runtime_config, build_chain_runtime_config, build_scheduler_runtime_config, + build_token_runtime_config, ) from app.application.service import configure_service_directory from app.db.session import ( @@ -68,6 +70,7 @@ def configure_plugin_system_services(): ) ) configure_runtime_settings(RuntimeSettingsService(settings)) + configure_token_runtime_config(lambda: build_token_runtime_config(settings)) configure_system_config(SystemConfigService(repository=SystemConfigOper())) configure_transfer_retry_config( lambda: TransferRetryConfig( diff --git a/tests/fixtures/architecture/configuration-debt-baseline.json b/tests/fixtures/architecture/configuration-debt-baseline.json index cac371264..09f897992 100644 --- a/tests/fixtures/architecture/configuration-debt-baseline.json +++ b/tests/fixtures/architecture/configuration-debt-baseline.json @@ -9,7 +9,7 @@ "root": "app" }, "settings_imports": { - "count": 124, + "count": 123, "files": [ "app/adapters/cache/backends.py", "app/adapters/cache/redis.py", @@ -47,7 +47,6 @@ "app/agent/tools/impl/send_voice_message.py", "app/agent/tools/impl/update_agent_task.py", "app/agent/tools/impl/update_system_settings.py", - "app/application/security/token.py", "app/application/security/url.py", "app/cli.py", "app/db/base.py", diff --git a/tests/fixtures/architecture/dependency-baseline.json b/tests/fixtures/architecture/dependency-baseline.json index 4d5dbf7f3..76b7c1661 100644 --- a/tests/fixtures/architecture/dependency-baseline.json +++ b/tests/fixtures/architecture/dependency-baseline.json @@ -13,8 +13,8 @@ "runtime_to_db": [], "workflow_to_db": [] }, - "edge_count": 6414, - "edge_sha256": "aea2c4a5f65a9800bc69a83990efd368ba0ec7a9b19e40f38f8d0df6c222ae26", + "edge_count": 6415, + "edge_sha256": "d763c4fede395d6ef881e1308b68cfaf484b82ce2b6f0dec808abf4b1d820858", "edges": [ "app -> app.runtime", "app -> app.runtime.compat", @@ -2697,8 +2697,9 @@ "app.application.security.passkey -> app.runtime", "app.application.security.passkey -> app.runtime.cache", "app.application.security.passkey -> app.runtime.log", + "app.application.security.token -> app.application", + "app.application.security.token -> app.application.configuration", "app.application.security.token -> app.runtime", - "app.application.security.token -> app.runtime.config", "app.application.security.token -> app.runtime.log", "app.application.security.token -> app.schemas", "app.application.security.token -> app.schemas.token",