Skip to content

Commit 7a34f06

Browse files
committed
[FIX] 补充修复上一次提交隐藏问题:原来的 default_factory 确实会在配置缺失且未及时持久化时生成新密钥,导致重启后旧 JWT 失效。
现在改为: WebConfig.secret_key 默认保持为空。 应用启动时仅在密钥为空时生成 64 字符随机密钥。 立即通过现有 ConfigLoader 写入 data/config.yaml。 后续启动直接复用已有密钥,不再轮换。 已存在的非空密钥不会被覆盖。
1 parent 1cb7a31 commit 7a34f06

5 files changed

Lines changed: 45 additions & 30 deletions

File tree

kirara_ai/config/global_config.py

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import secrets
21
from typing import Any, Dict, List, Optional
32

43
from pydantic import BaseModel, ConfigDict, Field, model_validator
@@ -109,10 +108,7 @@ class MemoryConfig(BaseModel):
109108
class WebConfig(BaseModel):
110109
host: str = Field(default="127.0.0.1", description="Web服务绑定的IP地址")
111110
port: int = Field(default=8080, description="Web服务端口号")
112-
secret_key: str = Field(
113-
default_factory=lambda: secrets.token_hex(32),
114-
description="Web服务的密钥,用于JWT等加密",
115-
)
111+
secret_key: str = Field(default="", description="Web服务的密钥,用于JWT等加密")
116112
password_file: str = Field(
117113
default="./data/web/password.hash", description="密码哈希存储路径"
118114
)

kirara_ai/entry.py

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import asyncio
22
import os
3+
import secrets
34
import signal
45
import time
56

67
from packaging import version
78

8-
from kirara_ai.config.config_loader import ConfigLoader
9+
from kirara_ai.config.config_loader import CONFIG_FILE, ConfigLoader
910
from kirara_ai.config.global_config import GlobalConfig
1011
from kirara_ai.database import DatabaseManager
1112
from kirara_ai.events.application import ApplicationStarted, ApplicationStopping
@@ -112,30 +113,35 @@ def init_tracing_system(container: DependencyContainer):
112113
logger.info("Tracing system initialized")
113114
return tracing_manager
114115

116+
117+
def _ensure_web_secret(config: GlobalConfig, config_path: str) -> None:
118+
if config.web.secret_key and os.path.exists(config_path):
119+
return
120+
if not config.web.secret_key:
121+
config.web.secret_key = secrets.token_hex(32)
122+
ConfigLoader.save_config_with_backup(config_path, config)
123+
124+
115125
def init_application() -> DependencyContainer:
116126
"""初始化应用程序"""
117127
logger.info("Initializing application...")
118128

119129
# 配置文件路径
120-
config_path = "./data/config.yaml"
130+
config_path = CONFIG_FILE
121131

122132
# 加载配置文件
123133
logger.info(f"Loading configuration from {config_path}")
124-
# check data directory
125-
if not os.path.exists("./data"):
126-
os.makedirs("./data")
127134
if os.path.exists(config_path):
128135
config: GlobalConfig = ConfigLoader.load_config(config_path, GlobalConfig)
129136
logger.info("Configuration loaded successfully")
130137
else:
131138
logger.warning(
132-
f"Configuration file {config_path} not found, using default configuration"
133-
)
134-
logger.warning(
135-
"Please create a configuration file by copying config.yaml.example to config.yaml and modify it according to your needs"
139+
f"Configuration file {config_path} not found, creating default configuration"
136140
)
137141
config = GlobalConfig()
138142

143+
_ensure_web_secret(config, config_path)
144+
139145
# 设置时区
140146
os.environ["TZ"] = config.system.timezone
141147
if hasattr(time, "tzset"):

tests/web/api/im/test_im.py

Lines changed: 12 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -170,14 +170,15 @@ async def test_get_adapter(self, test_client, auth_headers):
170170
assert adapter.get("config") == TEST_ADAPTER_CONFIG
171171

172172
@pytest.mark.asyncio
173-
async def test_create_adapter(self, test_client, auth_headers):
173+
async def test_create_adapter(self, test_client, auth_headers, monkeypatch):
174174
"""测试创建适配器"""
175175
adapter_data = IMAdapterConfig(
176176
name="new-adapter", adapter=TEST_ADAPTER_TYPE, config=TEST_ADAPTER_CONFIG
177177
)
178178

179179
# Mock 配置文件保存
180-
ConfigLoader.save_config_with_backup = MagicMock()
180+
mock_save = MagicMock()
181+
monkeypatch.setattr(ConfigLoader, "save_config_with_backup", mock_save)
181182
response = test_client.post(
182183
"/backend-api/api/im/adapters",
183184
headers=auth_headers,
@@ -192,10 +193,10 @@ async def test_create_adapter(self, test_client, auth_headers):
192193
assert adapter.get("config") == TEST_ADAPTER_CONFIG
193194

194195
# 验证配置保存
195-
ConfigLoader.save_config_with_backup.assert_called_once()
196+
mock_save.assert_called_once()
196197

197198
@pytest.mark.asyncio
198-
async def test_update_adapter(self, test_client, auth_headers):
199+
async def test_update_adapter(self, test_client, auth_headers, monkeypatch):
199200
"""测试更新适配器"""
200201
adapter_data = IMAdapterConfig(
201202
name=TEST_ADAPTER_ID,
@@ -204,7 +205,8 @@ async def test_update_adapter(self, test_client, auth_headers):
204205
)
205206

206207
# Mock 配置文件保存
207-
ConfigLoader.save_config_with_backup = MagicMock()
208+
mock_save = MagicMock()
209+
monkeypatch.setattr(ConfigLoader, "save_config_with_backup", mock_save)
208210
response = test_client.put(
209211
f"/backend-api/api/im/adapters/{TEST_ADAPTER_ID}",
210212
headers=auth_headers,
@@ -220,7 +222,7 @@ async def test_update_adapter(self, test_client, auth_headers):
220222
assert adapter.get("config").get("name") == "Updated Bot"
221223

222224
# 验证配置保存
223-
ConfigLoader.save_config_with_backup.assert_called_once()
225+
mock_save.assert_called_once()
224226

225227
@pytest.mark.asyncio
226228
async def test_stop_adapter(self, test_client, auth_headers):
@@ -260,7 +262,7 @@ async def test_start_adapter(self, test_client, auth_headers):
260262
assert data.get("adapter").get("is_running") is True
261263

262264
@pytest.mark.asyncio
263-
async def test_delete_adapter(self, test_client, auth_headers):
265+
async def test_delete_adapter(self, test_client, auth_headers, monkeypatch):
264266
"""测试删除适配器"""
265267
# 先启动适配器
266268
test_client.post(
@@ -269,7 +271,8 @@ async def test_delete_adapter(self, test_client, auth_headers):
269271
)
270272

271273
# Mock 配置文件保存
272-
ConfigLoader.save_config_with_backup = MagicMock()
274+
mock_save = MagicMock()
275+
monkeypatch.setattr(ConfigLoader, "save_config_with_backup", mock_save)
273276
response = test_client.delete(
274277
f"/backend-api/api/im/adapters/{TEST_ADAPTER_ID}", headers=auth_headers
275278
)
@@ -279,7 +282,7 @@ async def test_delete_adapter(self, test_client, auth_headers):
279282
assert data.get("message") == "Adapter deleted successfully"
280283

281284
# 验证配置保存
282-
ConfigLoader.save_config_with_backup.assert_called_once()
285+
mock_save.assert_called_once()
283286

284287
@pytest.mark.asyncio
285288
async def test_get_adapter_config_schema(self, test_client, auth_headers):

tests/web/api/llm/test_llm.py

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -177,7 +177,7 @@ async def test_create_backend(self, test_client, auth_headers):
177177
mock_save.assert_called_once()
178178

179179
@pytest.mark.asyncio
180-
async def test_update_backend(self, test_client, auth_headers):
180+
async def test_update_backend(self, test_client, auth_headers, monkeypatch):
181181
"""测试更新后端"""
182182
updated_config = LLMBackendConfig(
183183
name=TEST_BACKEND_NAME,
@@ -188,7 +188,8 @@ async def test_update_backend(self, test_client, auth_headers):
188188
)
189189

190190
# Mock 配置文件保存
191-
ConfigLoader.save_config_with_backup = MagicMock()
191+
mock_save = MagicMock()
192+
monkeypatch.setattr(ConfigLoader, "save_config_with_backup", mock_save)
192193
response = test_client.put(
193194
f"/backend-api/api/llm/backends/{TEST_BACKEND_NAME}",
194195
headers=auth_headers,
@@ -203,12 +204,13 @@ async def test_update_backend(self, test_client, auth_headers):
203204
assert backend.get("config").get("api_key") == "updated-key"
204205

205206
# 验证配置保存
206-
ConfigLoader.save_config_with_backup.assert_called_once()
207+
mock_save.assert_called_once()
207208

208209
@pytest.mark.asyncio
209-
async def test_delete_backend(self, test_client, auth_headers):
210+
async def test_delete_backend(self, test_client, auth_headers, monkeypatch):
210211
"""测试删除后端"""
211-
ConfigLoader.save_config_with_backup = MagicMock()
212+
mock_save = MagicMock()
213+
monkeypatch.setattr(ConfigLoader, "save_config_with_backup", mock_save)
212214
response = test_client.delete(
213215
f"/backend-api/api/llm/backends/{TEST_BACKEND_NAME}", headers=auth_headers
214216
)
@@ -218,7 +220,7 @@ async def test_delete_backend(self, test_client, auth_headers):
218220
assert "data" in data
219221
backend = data.get("data")
220222
assert backend.get("name") == TEST_BACKEND_NAME
221-
ConfigLoader.save_config_with_backup.assert_called_once()
223+
mock_save.assert_called_once()
222224

223225
# 验证后端已被删除
224226
response = test_client.get(

tests/web/auth/test_auth.py

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22
import pytest_asyncio
33
from fastapi.testclient import TestClient
44

5+
from kirara_ai.config.config_loader import ConfigLoader
56
from kirara_ai.config.global_config import GlobalConfig, WebConfig
7+
from kirara_ai.entry import _ensure_web_secret
68
from kirara_ai.ioc.container import DependencyContainer
79
from kirara_ai.web.app import WebServer
810
from kirara_ai.web.auth.services import AuthService
@@ -32,11 +34,13 @@ def test_client(app):
3234
return TestClient(app)
3335

3436

35-
def test_first_login_with_default_secret(tmp_path):
37+
def test_first_login_with_persisted_default_secret(tmp_path):
3638
container = DependencyContainer()
39+
config_path = tmp_path / "config.yaml"
3740
config = GlobalConfig(
3841
web=WebConfig(password_file=str(tmp_path / "password.hash"))
3942
)
43+
_ensure_web_secret(config, str(config_path))
4044
container.register(GlobalConfig, config)
4145
web_server = WebServer(container)
4246

@@ -47,6 +51,10 @@ def test_first_login_with_default_secret(tmp_path):
4751
assert response.status_code == 200
4852
token = response.json()["access_token"]
4953
assert container.resolve(AuthService).verify_token(token)
54+
saved = ConfigLoader.load_config(str(config_path), GlobalConfig)
55+
assert saved.web.secret_key == config.web.secret_key
56+
_ensure_web_secret(saved, str(config_path))
57+
assert saved.web.secret_key == config.web.secret_key
5058

5159

5260
@pytest_asyncio.fixture

0 commit comments

Comments
 (0)