Skip to content

Commit db24da5

Browse files
author
zhangtao
committed
merge: 安全修复与权限补齐
2 parents b17bb2a + 5bf69e4 commit db24da5

11 files changed

Lines changed: 687 additions & 35 deletions

File tree

‎backend/app/config/db_url.py‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""数据库 / Redis 连接串构造。
2+
3+
统一使用百分号编码,避免密码中的 ``@ ! # / :`` 等特殊字符被当作
4+
URL 分隔符;不要用字符串拼接后再手写 ``quote_plus``,否则用户名、
5+
库名、host 与密码的编码规则容易不一致。
6+
"""
7+
8+
from __future__ import annotations
9+
10+
from urllib.parse import quote
11+
12+
from sqlalchemy.engine import URL
13+
14+
15+
def build_sqlalchemy_url(
16+
drivername: str,
17+
*,
18+
username: str,
19+
password: str,
20+
host: str,
21+
port: int,
22+
database: str | None = None,
23+
query: dict[str, str] | None = None,
24+
) -> str:
25+
"""构造完整 SQLAlchemy URL(显式保留密码,供 SQLAlchemy 解析)。"""
26+
return URL.create(
27+
drivername=drivername,
28+
username=username or None,
29+
password=password or None,
30+
host=host or None,
31+
port=port or None,
32+
database=database or None,
33+
query=query or {},
34+
).render_as_string(hide_password=False)
35+
36+
37+
def build_redis_url(*, username: str, password: str, host: str, port: int, db: int) -> str:
38+
"""构造 Redis URL;用户名/密码按 RFC 3986 百分号编码。"""
39+
auth = ""
40+
if username and password:
41+
auth = f"{quote(username, safe='')}:{quote(password, safe='')}@"
42+
elif password:
43+
auth = f":{quote(password, safe='')}@"
44+
elif username:
45+
auth = f"{quote(username, safe='')}@"
46+
return f"redis://{auth}{host}:{port}/{db}"

‎backend/app/config/setting.py‎

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22
from functools import lru_cache
33
from pathlib import Path
44
from typing import Any, Literal
5-
from urllib.parse import quote_plus
65

76
from pydantic import model_validator
87
from pydantic_settings import BaseSettings, SettingsConfigDict
98

109
from app.common.enums import EnvironmentEnum
10+
from app.config.db_url import build_redis_url, build_sqlalchemy_url
1111
from app.config.path_conf import ENV_DIR
1212

1313

@@ -313,12 +313,13 @@ def _warn_insecure_cors(message: str) -> None:
313313
@property
314314
def REDIS_URI(self) -> str:
315315
"""构建 Redis 连接 URI(供 slowapi / 其他模块复用)。"""
316-
auth_part = ""
317-
if self.REDIS_USER and self.REDIS_PASSWORD:
318-
auth_part = f"{self.REDIS_USER}:{self.REDIS_PASSWORD}@"
319-
elif self.REDIS_PASSWORD:
320-
auth_part = f":{self.REDIS_PASSWORD}@"
321-
return f"redis://{auth_part}{self.REDIS_HOST}:{self.REDIS_PORT}/{self.REDIS_DB_NAME}"
316+
return build_redis_url(
317+
username=self.REDIS_USER,
318+
password=self.REDIS_PASSWORD,
319+
host=self.REDIS_HOST,
320+
port=self.REDIS_PORT,
321+
db=self.REDIS_DB_NAME,
322+
)
322323

323324
@property
324325
def MIDDLEWARE_LIST(self) -> list[str | None]:
@@ -338,9 +339,24 @@ def ASYNC_DB_URI(self) -> str:
338339
raise ValueError(f"数据库驱动不支持: {self.DATABASE_TYPE}, 异步数据库请选择 mysql、postgres、sqlite")
339340
db_connect: str = ""
340341
if self.DATABASE_TYPE == "mysql":
341-
db_connect = f"mysql+aiomysql://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
342+
db_connect = build_sqlalchemy_url(
343+
"mysql+aiomysql",
344+
username=self.DATABASE_USER,
345+
password=self.DATABASE_PASSWORD,
346+
host=self.DATABASE_HOST,
347+
port=self.DATABASE_PORT,
348+
database=self.DATABASE_NAME,
349+
query={"charset": "utf8mb4"},
350+
)
342351
elif self.DATABASE_TYPE == "postgres":
343-
db_connect = f"postgresql+asyncpg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
352+
db_connect = build_sqlalchemy_url(
353+
"postgresql+asyncpg",
354+
username=self.DATABASE_USER,
355+
password=self.DATABASE_PASSWORD,
356+
host=self.DATABASE_HOST,
357+
port=self.DATABASE_PORT,
358+
database=self.DATABASE_NAME,
359+
)
344360
else:
345361
name = self.DATABASE_NAME if self.DATABASE_NAME.endswith(".db") else f"{self.DATABASE_NAME}.db"
346362
db_connect = f"sqlite+aiosqlite:///{name}"
@@ -352,9 +368,24 @@ def DB_URI(self) -> str:
352368
raise ValueError(f"数据库驱动不支持: {self.DATABASE_TYPE}, 同步数据库请选择 mysql、postgres、sqlite")
353369
db_connect: str = ""
354370
if self.DATABASE_TYPE == "mysql":
355-
db_connect = f"mysql+pymysql://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}?charset=utf8mb4"
371+
db_connect = build_sqlalchemy_url(
372+
"mysql+pymysql",
373+
username=self.DATABASE_USER,
374+
password=self.DATABASE_PASSWORD,
375+
host=self.DATABASE_HOST,
376+
port=self.DATABASE_PORT,
377+
database=self.DATABASE_NAME,
378+
query={"charset": "utf8mb4"},
379+
)
356380
elif self.DATABASE_TYPE == "postgres":
357-
db_connect = f"postgresql+psycopg://{self.DATABASE_USER}:{quote_plus(self.DATABASE_PASSWORD)}@{self.DATABASE_HOST}:{self.DATABASE_PORT}/{self.DATABASE_NAME}"
381+
db_connect = build_sqlalchemy_url(
382+
"postgresql+psycopg",
383+
username=self.DATABASE_USER,
384+
password=self.DATABASE_PASSWORD,
385+
host=self.DATABASE_HOST,
386+
port=self.DATABASE_PORT,
387+
database=self.DATABASE_NAME,
388+
)
358389
else:
359390
name = self.DATABASE_NAME if self.DATABASE_NAME.endswith(".db") else f"{self.DATABASE_NAME}.db"
360391
db_connect = f"sqlite:///{name}"
@@ -379,7 +410,7 @@ def FASTAPI_CONFIG(self) -> dict[str, Any]:
379410
404: {"description": "资源不存在"},
380411
422: {"description": "请求参数验证错误"},
381412
500: {"description": "服务器内部错误"},
382-
}
413+
},
383414
}
384415

385416

‎backend/app/modules/dbconsole/service.py‎

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
import asyncio
1212
import time
1313
from typing import Any
14-
from urllib.parse import quote_plus
1514

1615
from sqlalchemy import text
1716
from sqlalchemy.ext.asyncio import AsyncSession, create_async_engine
1817

18+
from app.config.db_url import build_sqlalchemy_url
1919
from app.core.base_schema import AuthSchema
2020
from app.core.exceptions import CustomException
2121
from app.utils.crypto_util import CryptoUtil
@@ -43,9 +43,14 @@ def build_url(db_type: str, host: str, port: int, username: str, password: str,
4343
driver = _DRIVERS.get(db_type)
4444
if driver is None:
4545
raise CustomException(msg=f"不支持的数据库类型: {db_type}(仅 mysql/postgres)")
46-
auth = f"{quote_plus(username)}:{quote_plus(password)}"
47-
db_part = f"/{quote_plus(database)}" if database else "/"
48-
return f"{driver}://{auth}@{host}:{port}{db_part}"
46+
return build_sqlalchemy_url(
47+
driver,
48+
username=username,
49+
password=password,
50+
host=host,
51+
port=port,
52+
database=database,
53+
)
4954

5055

5156
class DbConsoleService:

‎backend/app/modules/monitor/cache/controller.py‎

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ async def get_monitor_cache_key_controller(cache_name: Annotated[str, Path(descr
4141
return SuccessResponse(data=result, msg=f"获取缓存{cache_name}的键名列表成功")
4242

4343

44-
@CacheRouter.get("/get/value/{cache_name}/{cache_key}", summary="获取缓存值", response_model=ResponseSchema[CacheInfoSchema], dependencies=[Security(AuthPermission(["module_monitor:cache:query"]))])
44+
@CacheRouter.get("/get/value/{cache_name}/{cache_key}", summary="获取缓存值", response_model=ResponseSchema[CacheInfoSchema], dependencies=[Security(AuthPermission(["module_monitor:cache:detail"]))])
4545
async def get_monitor_cache_value_controller(
4646
cache_name: Annotated[str, Path(description="缓存名称")],
4747
cache_key: Annotated[str, Path(description="缓存键名")],
@@ -74,7 +74,9 @@ async def clear_monitor_cache_all_controller(
7474
# ── Redis 通用键空间浏览(只读)───────────────────────────────────────
7575

7676

77-
@CacheRouter.get("/redis/databases", summary="Redis 逻辑库列表(只读)", response_model=ResponseSchema[list[RedisDatabaseSchema]], dependencies=[Security(AuthPermission(["module_monitor:cache:query"]))])
77+
@CacheRouter.get(
78+
"/redis/databases", summary="Redis 逻辑库列表(只读)", response_model=ResponseSchema[list[RedisDatabaseSchema]], dependencies=[Security(AuthPermission(["module_monitor:cache:query"]))]
79+
)
7880
async def list_redis_databases_controller(
7981
redis: Annotated[Redis, Depends(redis_getter)],
8082
) -> JSONResponse:
@@ -90,13 +92,13 @@ async def scan_redis_keys_controller(
9092
cursor: Annotated[int, Query(ge=0, description="SCAN 游标,首页传 0")] = 0,
9193
count: Annotated[int, Query(ge=1, le=500, description="单页数量提示")] = 100,
9294
) -> JSONResponse:
93-
result: RedisKeyPageSchema = await RedisBrowserService.scan_keys(
94-
redis=redis, db=db, pattern=pattern, cursor=cursor, count=count
95-
)
95+
result: RedisKeyPageSchema = await RedisBrowserService.scan_keys(redis=redis, db=db, pattern=pattern, cursor=cursor, count=count)
9696
return SuccessResponse(data=result, msg="扫描键成功")
9797

9898

99-
@CacheRouter.get("/redis/value", summary="读取 Redis 单键详情(只读,敏感键脱敏)", response_model=ResponseSchema[RedisValueSchema], dependencies=[Security(AuthPermission(["module_monitor:cache:query"]))])
99+
@CacheRouter.get(
100+
"/redis/value", summary="读取 Redis 单键详情(只读,敏感键脱敏)", response_model=ResponseSchema[RedisValueSchema], dependencies=[Security(AuthPermission(["module_monitor:cache:detail"]))]
101+
)
100102
async def get_redis_value_controller(
101103
redis: Annotated[Redis, Depends(redis_getter)],
102104
key: Annotated[str, Query(min_length=1, max_length=512, description="键名")],

‎backend/app/modules/system/log/controller.py‎

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,7 @@ async def delete_log_controller(
5959
return SuccessResponse(msg="删除登录日志成功")
6060

6161

62-
@LogRouter.get("/operation/detail/{id}", summary="获取操作日志详情", response_model=ResponseSchema[OperationLogDetailOutSchema], dependencies=[Security(AuthPermission(["module_system:log:query"]))])
62+
@LogRouter.get("/operation/detail/{id}", summary="获取操作日志详情", response_model=ResponseSchema[OperationLogDetailOutSchema], dependencies=[Security(AuthPermission(["module_system:log:detail"]))])
6363
async def get_operation_log_detail_controller(
6464
auth: Annotated[AuthSchema, Security(get_current_user)],
6565
db: Annotated[AsyncSession, Depends(db_getter)],
@@ -70,7 +70,10 @@ async def get_operation_log_detail_controller(
7070

7171

7272
@LogRouter.get(
73-
"/operation/list", summary="获取操作日志列表", response_model=ResponseSchema[PageResultSchema[OperationLogOutSchema]], dependencies=[Security(AuthPermission(["module_system:log:query"]))],
73+
"/operation/list",
74+
summary="获取操作日志列表",
75+
response_model=ResponseSchema[PageResultSchema[OperationLogOutSchema]],
76+
dependencies=[Security(AuthPermission(["module_system:log:query"]))],
7477
)
7578
async def get_operation_log_list_controller(
7679
auth: Annotated[AuthSchema, Security(get_current_user)],
@@ -104,9 +107,7 @@ async def export_operation_log_list_controller(
104107
search: Annotated[OperationLogQueryParam, Body()],
105108
) -> StreamingResponse:
106109
operation_log_query_result: list[OperationLogOutSchema] = await OperationLogService(auth, db).get_list(search=search)
107-
operation_log_export_result: bytes = await OperationLogService.export_list(
108-
operation_log_list=[item.model_dump() for item in operation_log_query_result]
109-
)
110+
operation_log_export_result: bytes = await OperationLogService.export_list(operation_log_list=[item.model_dump() for item in operation_log_query_result])
110111

111112
return StreamResponse(
112113
data=bytes2file_response(operation_log_export_result),

0 commit comments

Comments
 (0)