From b13410c7d1ce0816722ca152f390e87d954afe4f Mon Sep 17 00:00:00 2001 From: Wu Clan Date: Mon, 9 Sep 2024 11:50:22 +0800 Subject: [PATCH] Optimize the naming of setting params (#410) --- backend/app/admin/service/role_service.py | 2 +- backend/app/admin/service/user_service.py | 16 +-- backend/app/admin/tests/api_v1/test_auth.py | 2 +- backend/app/admin/tests/utils/get_headers.py | 2 +- backend/app/generator/api/v1/gen.py | 2 +- backend/app/generator/conf.py | 2 +- backend/app/router.py | 2 +- backend/app/task/conf.py | 1 + backend/common/security/rbac.py | 6 +- backend/core/conf.py | 116 +++++++++---------- backend/core/registrar.py | 18 +-- backend/middleware/jwt_auth_middleware.py | 8 +- backend/middleware/opera_log_middleware.py | 12 +- backend/utils/request_parse.py | 4 +- 14 files changed, 96 insertions(+), 97 deletions(-) diff --git a/backend/app/admin/service/role_service.py b/backend/app/admin/service/role_service.py index 45eefeb0..d1335936 100644 --- a/backend/app/admin/service/role_service.py +++ b/backend/app/admin/service/role_service.py @@ -74,7 +74,7 @@ class RoleService: count = await role_dao.update_menus(db, pk, menu_ids) if pk in [role.id for role in request.user.roles]: await redis_client.delete_prefix(f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}') - await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') return count @staticmethod diff --git a/backend/app/admin/service/user_service.py b/backend/app/admin/service/user_service.py index 592c01ce..be4e4b67 100644 --- a/backend/app/admin/service/user_service.py +++ b/backend/app/admin/service/user_service.py @@ -82,7 +82,7 @@ class UserService: key_prefix = [ f'{settings.TOKEN_REDIS_PREFIX}:{request.user.id}', f'{settings.TOKEN_REFRESH_REDIS_PREFIX}:{request.user.id}', - f'{settings.USER_REDIS_PREFIX}:{request.user.id}', + f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}', ] for key in key_prefix: await redis_client.delete_prefix(key) @@ -118,7 +118,7 @@ class UserService: if email: raise errors.ForbiddenError(msg='邮箱已注册') count = await user_dao.update_userinfo(db, input_user.id, obj) - await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') return count @staticmethod @@ -136,7 +136,7 @@ class UserService: raise errors.NotFoundError(msg='角色不存在') await user_dao.update_role(db, input_user, obj) await redis_client.delete_prefix(f'{settings.PERMISSION_REDIS_PREFIX}:{request.user.uuid}') - await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') @staticmethod async def update_avatar(*, request: Request, username: str, avatar: AvatarParam) -> int: @@ -148,7 +148,7 @@ class UserService: if not input_user: raise errors.NotFoundError(msg='用户不存在') count = await user_dao.update_avatar(db, input_user.id, avatar) - await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') return count @staticmethod @@ -166,7 +166,7 @@ class UserService: raise errors.ForbiddenError(msg='非法操作') super_status = await user_dao.get_super(db, pk) count = await user_dao.set_super(db, pk, False if super_status else True) - await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{pk}') + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}') return count @staticmethod @@ -180,7 +180,7 @@ class UserService: raise errors.ForbiddenError(msg='非法操作') staff_status = await user_dao.get_staff(db, pk) count = await user_dao.set_staff(db, pk, False if staff_status else True) - await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{pk}') + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}') return count @staticmethod @@ -194,7 +194,7 @@ class UserService: raise errors.ForbiddenError(msg='非法操作') status = await user_dao.get_status(db, pk) count = await user_dao.set_status(db, pk, False if status else True) - await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{pk}') + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{pk}') return count @staticmethod @@ -207,7 +207,7 @@ class UserService: user_id = request.user.id multi_login = await user_dao.get_multi_login(db, pk) if pk != user_id else request.user.is_multi_login count = await user_dao.set_multi_login(db, pk, False if multi_login else True) - await redis_client.delete(f'{settings.USER_REDIS_PREFIX}:{request.user.id}') + await redis_client.delete(f'{settings.JWT_USER_REDIS_PREFIX}:{request.user.id}') token = await get_token(request) latest_multi_login = await user_dao.get_multi_login(db, pk) # TODO: 删除用户 refresh token, 此操作需要传参,暂时不考虑实现 diff --git a/backend/app/admin/tests/api_v1/test_auth.py b/backend/app/admin/tests/api_v1/test_auth.py index 4eb9e529..f74b8807 100644 --- a/backend/app/admin/tests/api_v1/test_auth.py +++ b/backend/app/admin/tests/api_v1/test_auth.py @@ -6,6 +6,6 @@ from backend.core.conf import settings def test_logout(client: TestClient, token_headers: dict[str, str]) -> None: - response = client.post(f'{settings.API_V1_STR}/auth/logout', headers=token_headers) + response = client.post(f'{settings.FASTAPI_API_V1_PATH}/auth/logout', headers=token_headers) assert response.status_code == 200 assert response.json()['code'] == 200 diff --git a/backend/app/admin/tests/utils/get_headers.py b/backend/app/admin/tests/utils/get_headers.py index 8c281020..59a2f030 100644 --- a/backend/app/admin/tests/utils/get_headers.py +++ b/backend/app/admin/tests/utils/get_headers.py @@ -9,7 +9,7 @@ def get_token_headers(client: TestClient, username: str, password: str) -> dict[ 'username': username, 'password': password, } - response = client.post(f'{settings.API_V1_STR}/auth/login/swagger', params=data) + response = client.post(f'{settings.FASTAPI_API_V1_PATH}/auth/login/swagger', params=data) response.raise_for_status() token_type = response.json()['token_type'] access_token = response.json()['access_token'] diff --git a/backend/app/generator/api/v1/gen.py b/backend/app/generator/api/v1/gen.py index e43212a9..0c21399f 100644 --- a/backend/app/generator/api/v1/gen.py +++ b/backend/app/generator/api/v1/gen.py @@ -194,5 +194,5 @@ async def download_code(pk: Annotated[int, Path(..., description='业务ID')]): return StreamingResponse( bio, media_type='application/x-zip-compressed', - headers={'Content-Disposition': f'attachment; filename={generator_settings.ZIP_FILENAME}.zip'}, + headers={'Content-Disposition': f'attachment; filename={generator_settings.DOWNLOAD_ZIP_FILENAME}.zip'}, ) diff --git a/backend/app/generator/conf.py b/backend/app/generator/conf.py index a9ee86a6..cf756830 100644 --- a/backend/app/generator/conf.py +++ b/backend/app/generator/conf.py @@ -16,7 +16,7 @@ class GeneratorSettings(BaseSettings): TEMPLATE_BACKEND_DIR_NAME: str = 'py' # 代码下载 - ZIP_FILENAME: str = 'fba_generator' + DOWNLOAD_ZIP_FILENAME: str = 'fba_generator' @lru_cache diff --git a/backend/app/router.py b/backend/app/router.py index c51dece1..3fb1cfe6 100644 --- a/backend/app/router.py +++ b/backend/app/router.py @@ -7,7 +7,7 @@ from backend.app.generator.api.router import v1 as generator_v1 from backend.app.task.api.router import v1 as task_v1 from backend.core.conf import settings -route = APIRouter(prefix=settings.API_V1_STR) +route = APIRouter(prefix=settings.FASTAPI_API_V1_PATH) route.include_router(admin_v1) route.include_router(generator_v1) diff --git a/backend/app/task/conf.py b/backend/app/task/conf.py index 08b604d2..659ce276 100644 --- a/backend/app/task/conf.py +++ b/backend/app/task/conf.py @@ -54,6 +54,7 @@ class TaskSettings(BaseSettings): } @model_validator(mode='before') + @classmethod def validate_celery_broker(cls, values): if values['ENVIRONMENT'] == 'pro': values['CELERY_BROKER'] = 'rabbitmq' diff --git a/backend/common/security/rbac.py b/backend/common/security/rbac.py index fa92695f..a62ae5d6 100644 --- a/backend/common/security/rbac.py +++ b/backend/common/security/rbac.py @@ -54,7 +54,7 @@ class RBAC: """ path = request.url.path # 鉴权白名单 - if path in settings.TOKEN_EXCLUDE: + if path in settings.TOKEN_REQUEST_PATH_EXCLUDE: return # JWT 授权状态强制校验 if not request.auth.scopes: @@ -83,7 +83,7 @@ class RBAC: # 没有菜单权限标识不校验 if not path_auth_perm: return - if path_auth_perm in set(settings.ROLE_MENU_EXCLUDE): + if path_auth_perm in set(settings.RBAC_ROLE_MENU_EXCLUDE): return allow_perms = [] for role in user_roles: @@ -94,7 +94,7 @@ class RBAC: raise AuthorizationError else: # casbin 权限校验 - if (method, path) in settings.CASBIN_EXCLUDE: + if (method, path) in settings.RBAC_CASBIN_EXCLUDE: return enforcer = await self.enforcer() if not enforcer.enforce(user_uuid, path, method): diff --git a/backend/core/conf.py b/backend/core/conf.py index 75b86a7f..e3735d25 100644 --- a/backend/core/conf.py +++ b/backend/core/conf.py @@ -36,13 +36,14 @@ class Settings(BaseSettings): OPERA_LOG_ENCRYPT_SECRET_KEY: str # 密钥 os.urandom(32), 需使用 bytes.hex() 方法转换为 str # FastAPI - API_V1_STR: str = '/api/v1' - TITLE: str = 'FastAPI' - VERSION: str = '0.0.1' - DESCRIPTION: str = 'FastAPI Best Architecture' - DOCS_URL: str | None = f'{API_V1_STR}/docs' - REDOCS_URL: str | None = f'{API_V1_STR}/redocs' - OPENAPI_URL: str | None = f'{API_V1_STR}/openapi' + FASTAPI_API_V1_PATH: str = '/api/v1' + FASTAPI_TITLE: str = 'FastAPI' + FASTAPI_VERSION: str = '0.0.1' + FASTAPI_DESCRIPTION: str = 'FastAPI Best Architecture' + FASTAPI_DOCS_URL: str | None = f'{FASTAPI_API_V1_PATH}/docs' + FASTAPI_REDOCS_URL: str | None = f'{FASTAPI_API_V1_PATH}/redocs' + FASTAPI_OPENAPI_URL: str | None = f'{FASTAPI_API_V1_PATH}/openapi' + FASTAPI_STATIC_FILES: bool = False @model_validator(mode='before') @classmethod @@ -51,28 +52,6 @@ class Settings(BaseSettings): values['OPENAPI_URL'] = None return values - # Demo mode - # Only GET, OPTIONS requests are allowed - DEMO_MODE: bool = False - DEMO_MODE_EXCLUDE: set[tuple[str, str]] = { - ('POST', f'{API_V1_STR}/auth/login'), - ('POST', f'{API_V1_STR}/auth/logout'), - ('GET', f'{API_V1_STR}/auth/captcha'), - } - - # Static Server - STATIC_FILES: bool = False - - # Location Parse - LOCATION_PARSE: Literal['online', 'offline', 'false'] = 'offline' - - # Limiter - LIMITER_REDIS_PREFIX: str = 'fba:limiter' - - # DateTime - DATETIME_TIMEZONE: str = 'Asia/Shanghai' - DATETIME_FORMAT: str = '%Y-%m-%d %H:%M:%S' - # MySQL MYSQL_ECHO: bool = False MYSQL_DATABASE: str = 'fba' @@ -87,18 +66,35 @@ class Settings(BaseSettings): TOKEN_REFRESH_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 # refresh token 过期时间,单位:秒 TOKEN_REDIS_PREFIX: str = 'fba:token' TOKEN_REFRESH_REDIS_PREFIX: str = 'fba:refresh_token' - TOKEN_EXCLUDE: list[str] = [ # JWT / RBAC 白名单 - f'{API_V1_STR}/auth/login', + TOKEN_REQUEST_PATH_EXCLUDE: list[str] = [ # JWT / RBAC 白名单 + f'{FASTAPI_API_V1_PATH}/auth/login', + ] + + # JWT + JWT_USER_REDIS_PREFIX: str = 'fba:user' + JWT_USER_REDIS_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 + + # Permission (RBAC) + PERMISSION_MODE: Literal['casbin', 'role-menu'] = 'casbin' + PERMISSION_REDIS_PREFIX: str = 'fba:permission' + + # RBAC + # Casbin + RBAC_CASBIN_EXCLUDE: set[tuple[str, str]] = { + ('POST', f'{FASTAPI_API_V1_PATH}/auth/logout'), + ('POST', f'{FASTAPI_API_V1_PATH}/auth/token/new'), + } + + # Role-Menu + RBAC_ROLE_MENU_EXCLUDE: list[str] = [ + 'sys:monitor:redis', + 'sys:monitor:server', ] # Cookies COOKIE_REFRESH_TOKEN_KEY: str = 'fba_refresh_token' COOKIE_REFRESH_TOKEN_EXPIRE_SECONDS: int = TOKEN_REFRESH_EXPIRE_SECONDS - # Sys User - USER_REDIS_PREFIX: str = 'fba:user' - USER_REDIS_EXPIRE_SECONDS: int = 60 * 60 * 24 * 7 - # Log LOG_ROOT_LEVEL: str = 'NOTSET' LOG_STD_FORMAT: str = ( @@ -131,44 +127,44 @@ class Settings(BaseSettings): TRACE_ID_REQUEST_HEADER_KEY, ] - # RBAC Permission - PERMISSION_MODE: Literal['casbin', 'role-menu'] = 'casbin' - PERMISSION_REDIS_PREFIX: str = 'fba:permission' + # DateTime + DATETIME_TIMEZONE: str = 'Asia/Shanghai' + DATETIME_FORMAT: str = '%Y-%m-%d %H:%M:%S' - # Casbin Auth - CASBIN_EXCLUDE: set[tuple[str, str]] = { - ('POST', f'{API_V1_STR}/auth/logout'), - ('POST', f'{API_V1_STR}/auth/token/new'), + # Request limiter + REQUEST_LIMITER_REDIS_PREFIX: str = 'fba:limiter' + + # Demo mode (Only GET, OPTIONS requests are allowed) + DEMO_MODE: bool = False + DEMO_MODE_EXCLUDE: set[tuple[str, str]] = { + ('POST', f'{FASTAPI_API_V1_PATH}/auth/login'), + ('POST', f'{FASTAPI_API_V1_PATH}/auth/logout'), + ('GET', f'{FASTAPI_API_V1_PATH}/auth/captcha'), } - # Role Menu Auth - ROLE_MENU_EXCLUDE: list[str] = [ - 'sys:monitor:redis', - 'sys:monitor:server', - ] + # Ip location + IP_LOCATION_PARSE: Literal['online', 'offline', 'false'] = 'offline' + IP_LOCATION_REDIS_PREFIX: str = 'fba:ip:location' + IP_LOCATION_EXPIRE_SECONDS: int = 60 * 60 * 24 * 1 # 过期时间,单位:秒 # Opera log - OPERA_LOG_EXCLUDE: list[str] = [ + OPERA_LOG_PATH_EXCLUDE: list[str] = [ '/favicon.ico', - DOCS_URL, - REDOCS_URL, - OPENAPI_URL, - f'{API_V1_STR}/auth/login/swagger', - f'{API_V1_STR}/oauth2/github/callback', - f'{API_V1_STR}/oauth2/linux-do/callback', + FASTAPI_DOCS_URL, + FASTAPI_REDOCS_URL, + FASTAPI_OPENAPI_URL, + f'{FASTAPI_API_V1_PATH}/auth/login/swagger', + f'{FASTAPI_API_V1_PATH}/oauth2/github/callback', + f'{FASTAPI_API_V1_PATH}/oauth2/linux-do/callback', ] - OPERA_LOG_ENCRYPT: int = 1 # 0: AES (性能损耗); 1: md5; 2: ItsDangerous; 3: 不加密, others: 替换为 ****** - OPERA_LOG_ENCRYPT_INCLUDE: list[str] = [ + OPERA_LOG_ENCRYPT_TYPE: int = 1 # 0: AES (性能损耗); 1: md5; 2: ItsDangerous; 3: 不加密, others: 替换为 ****** + OPERA_LOG_ENCRYPT_KEY_INCLUDE: list[str] = [ # 将加密接口入参参数对应的值 'password', 'old_password', 'new_password', 'confirm_password', ] - # Ip location - IP_LOCATION_REDIS_PREFIX: str = 'fba:ip:location' - IP_LOCATION_EXPIRE_SECONDS: int = 60 * 60 * 24 * 1 # 过期时间,单位:秒 - @lru_cache def get_settings() -> Settings: diff --git a/backend/core/registrar.py b/backend/core/registrar.py index 01a6f09f..3b221ee1 100644 --- a/backend/core/registrar.py +++ b/backend/core/registrar.py @@ -35,7 +35,9 @@ async def register_init(app: FastAPI): # 连接 redis await redis_client.open() # 初始化 limiter - await FastAPILimiter.init(redis_client, prefix=settings.LIMITER_REDIS_PREFIX, http_callback=http_limit_callback) + await FastAPILimiter.init( + redis=redis_client, prefix=settings.REQUEST_LIMITER_REDIS_PREFIX, http_callback=http_limit_callback + ) yield @@ -48,12 +50,12 @@ async def register_init(app: FastAPI): def register_app(): # FastAPI app = FastAPI( - title=settings.TITLE, - version=settings.VERSION, - description=settings.DESCRIPTION, - docs_url=settings.DOCS_URL, - redoc_url=settings.REDOCS_URL, - openapi_url=settings.OPENAPI_URL, + title=settings.FASTAPI_TITLE, + version=settings.FASTAPI_VERSION, + description=settings.FASTAPI_DESCRIPTION, + docs_url=settings.FASTAPI_DOCS_URL, + redoc_url=settings.FASTAPI_REDOCS_URL, + openapi_url=settings.FASTAPI_OPENAPI_URL, default_response_class=MsgSpecJSONResponse, lifespan=register_init, ) @@ -96,7 +98,7 @@ def register_static_file(app: FastAPI): :param app: :return: """ - if settings.STATIC_FILES: + if settings.FASTAPI_STATIC_FILES: import os from fastapi.staticfiles import StaticFiles diff --git a/backend/middleware/jwt_auth_middleware.py b/backend/middleware/jwt_auth_middleware.py index d05d5a16..67e672af 100644 --- a/backend/middleware/jwt_auth_middleware.py +++ b/backend/middleware/jwt_auth_middleware.py @@ -40,7 +40,7 @@ class JwtAuthMiddleware(AuthenticationBackend): if not token: return - if request.url.path in settings.TOKEN_EXCLUDE: + if request.url.path in settings.TOKEN_REQUEST_PATH_EXCLUDE: return scheme, token = get_authorization_scheme_param(token) @@ -49,14 +49,14 @@ class JwtAuthMiddleware(AuthenticationBackend): try: sub = await jwt.jwt_authentication(token) - cache_user = await redis_client.get(f'{settings.USER_REDIS_PREFIX}:{sub}') + cache_user = await redis_client.get(f'{settings.JWT_USER_REDIS_PREFIX}:{sub}') if not cache_user: async with async_db_session() as db: current_user = await jwt.get_current_user(db, sub) user = CurrentUserIns(**select_as_dict(current_user)) await redis_client.setex( - f'{settings.USER_REDIS_PREFIX}:{sub}', - settings.USER_REDIS_EXPIRE_SECONDS, + f'{settings.JWT_USER_REDIS_PREFIX}:{sub}', + settings.JWT_USER_REDIS_EXPIRE_SECONDS, user.model_dump_json(), ) else: diff --git a/backend/middleware/opera_log_middleware.py b/backend/middleware/opera_log_middleware.py index 65dc9202..7b314143 100644 --- a/backend/middleware/opera_log_middleware.py +++ b/backend/middleware/opera_log_middleware.py @@ -25,7 +25,7 @@ class OperaLogMiddleware(BaseHTTPMiddleware): async def dispatch(self, request: Request, call_next) -> Response: # 排除记录白名单 path = request.url.path - if path in settings.OPERA_LOG_EXCLUDE or not path.startswith(f'{settings.API_V1_STR}'): + if path in settings.OPERA_LOG_PATH_EXCLUDE or not path.startswith(f'{settings.FASTAPI_API_V1_PATH}'): return await call_next(request) # 请求解析 @@ -166,23 +166,23 @@ class OperaLogMiddleware(BaseHTTPMiddleware): if not args: args = None else: - match settings.OPERA_LOG_ENCRYPT: + match settings.OPERA_LOG_ENCRYPT_TYPE: case OperaLogCipherType.aes: for key in args.keys(): - if key in settings.OPERA_LOG_ENCRYPT_INCLUDE: + if key in settings.OPERA_LOG_ENCRYPT_KEY_INCLUDE: args[key] = (AESCipher(settings.OPERA_LOG_ENCRYPT_SECRET_KEY).encrypt(args[key])).hex() case OperaLogCipherType.md5: for key in args.keys(): - if key in settings.OPERA_LOG_ENCRYPT_INCLUDE: + if key in settings.OPERA_LOG_ENCRYPT_KEY_INCLUDE: args[key] = Md5Cipher.encrypt(args[key]) case OperaLogCipherType.itsdangerous: for key in args.keys(): - if key in settings.OPERA_LOG_ENCRYPT_INCLUDE: + if key in settings.OPERA_LOG_ENCRYPT_KEY_INCLUDE: args[key] = ItsDCipher(settings.OPERA_LOG_ENCRYPT_SECRET_KEY).encrypt(args[key]) case OperaLogCipherType.plan: pass case _: for key in args.keys(): - if key in settings.OPERA_LOG_ENCRYPT_INCLUDE: + if key in settings.OPERA_LOG_ENCRYPT_KEY_INCLUDE: args[key] = '******' return args diff --git a/backend/utils/request_parse.py b/backend/utils/request_parse.py index c73b4e34..35540ccd 100644 --- a/backend/utils/request_parse.py +++ b/backend/utils/request_parse.py @@ -83,9 +83,9 @@ async def parse_ip_info(request: Request) -> IpInfo: if location: country, region, city = location.split(' ') return IpInfo(ip=ip, country=country, region=region, city=city) - if settings.LOCATION_PARSE == 'online': + if settings.IP_LOCATION_PARSE == 'online': location_info = await get_location_online(ip, request.headers.get('User-Agent')) - elif settings.LOCATION_PARSE == 'offline': + elif settings.IP_LOCATION_PARSE == 'offline': location_info = await get_location_offline(ip) else: location_info = None