From e165b1f4ab6e0ab76ae67fff2910759acbc6a994 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Thu, 9 Apr 2026 14:31:28 +0800 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E9=99=90=E6=B5=81=E8=A3=85=E9=A5=B0=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ruoyi-fastapi-backend/.env.dev | 4 + ruoyi-fastapi-backend/.env.dockermy | 4 + ruoyi-fastapi-backend/.env.dockerpg | 4 + ruoyi-fastapi-backend/.env.prod | 4 + .../common/annotation/log_annotation.py | 3 +- .../annotation/rate_limit_annotation.py | 712 ++++++++++++++++++ ruoyi-fastapi-backend/common/constant.py | 403 ++++++---- ruoyi-fastapi-backend/common/enums.py | 27 + ruoyi-fastapi-backend/config/env.py | 2 + .../api_response_header_middleware.py | 28 + .../middlewares/demo_mode_middleware.py | 3 +- ruoyi-fastapi-backend/middlewares/handle.py | 3 + .../controller/cache_controller.py | 5 + .../controller/captcha_controller.py | 3 + .../controller/common_controller.py | 3 + .../controller/config_controller.py | 15 +- .../controller/dept_controller.py | 14 +- .../controller/dict_controller.py | 28 +- .../module_admin/controller/job_controller.py | 23 +- .../module_admin/controller/log_controller.py | 9 + .../controller/login_controller.py | 15 +- .../controller/menu_controller.py | 16 +- .../controller/notice_controller.py | 12 +- .../controller/online_controller.py | 3 + .../controller/post_controller.py | 14 +- .../controller/role_controller.py | 33 +- .../controller/server_controller.py | 4 +- .../controller/user_controller.py | 38 +- .../module_admin/service/login_service.py | 3 +- .../controller/ai_chat_controller.py | 9 +- .../controller/ai_model_controller.py | 14 +- .../controller/gen_controller.py | 44 +- .../utils/api_annotation_util.py | 102 +++ .../utils/api_response_header_util.py | 25 + ruoyi-fastapi-backend/utils/client_ip_util.py | 60 ++ ruoyi-fastapi-backend/utils/response_util.py | 46 ++ ruoyi-fastapi-frontend/src/utils/request.js | 9 + 37 files changed, 1476 insertions(+), 268 deletions(-) create mode 100644 ruoyi-fastapi-backend/common/annotation/rate_limit_annotation.py create mode 100644 ruoyi-fastapi-backend/middlewares/api_response_header_middleware.py create mode 100644 ruoyi-fastapi-backend/utils/api_annotation_util.py create mode 100644 ruoyi-fastapi-backend/utils/api_response_header_util.py create mode 100644 ruoyi-fastapi-backend/utils/client_ip_util.py diff --git a/ruoyi-fastapi-backend/.env.dev b/ruoyi-fastapi-backend/.env.dev index f5feade..8cf3c07 100644 --- a/ruoyi-fastapi-backend/.env.dev +++ b/ruoyi-fastapi-backend/.env.dev @@ -25,6 +25,10 @@ APP_DEMO_MODE = false APP_DISABLE_SWAGGER = false # 应用是否禁用ReDoc文档 APP_DISABLE_REDOC = false +# 可信代理IP列表,多个值使用逗号分隔 +APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1' +# 可信代理跳数,单层Nginx代理通常为1 +APP_TRUSTED_PROXY_HOPS = 1 # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/.env.dockermy b/ruoyi-fastapi-backend/.env.dockermy index a471682..9cca11b 100644 --- a/ruoyi-fastapi-backend/.env.dockermy +++ b/ruoyi-fastapi-backend/.env.dockermy @@ -25,6 +25,10 @@ APP_DEMO_MODE = false APP_DISABLE_SWAGGER = true # 应用是否禁用ReDoc文档 APP_DISABLE_REDOC = true +# 可信代理IP列表,多个值使用逗号分隔 +APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1' +# 可信代理跳数,单层Nginx代理通常为1 +APP_TRUSTED_PROXY_HOPS = 1 # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/.env.dockerpg b/ruoyi-fastapi-backend/.env.dockerpg index 5b8619d..b09a9fe 100644 --- a/ruoyi-fastapi-backend/.env.dockerpg +++ b/ruoyi-fastapi-backend/.env.dockerpg @@ -25,6 +25,10 @@ APP_SAME_TIME_LOGIN = true APP_DISABLE_SWAGGER = true # 应用是否禁用ReDoc文档 APP_DISABLE_REDOC = true +# 可信代理IP列表,多个值使用逗号分隔 +APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1' +# 可信代理跳数,单层Nginx代理通常为1 +APP_TRUSTED_PROXY_HOPS = 1 # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/.env.prod b/ruoyi-fastapi-backend/.env.prod index be624c8..7e32a6c 100644 --- a/ruoyi-fastapi-backend/.env.prod +++ b/ruoyi-fastapi-backend/.env.prod @@ -25,6 +25,10 @@ APP_DEMO_MODE = false APP_DISABLE_SWAGGER = true # 应用是否禁用ReDoc文档 APP_DISABLE_REDOC = true +# 可信代理IP列表,多个值使用逗号分隔 +APP_TRUSTED_PROXY_IPS = '127.0.0.1,::1' +# 可信代理跳数,单层Nginx代理通常为1 +APP_TRUSTED_PROXY_HOPS = 1 # -------- Jwt配置 -------- # Jwt秘钥 diff --git a/ruoyi-fastapi-backend/common/annotation/log_annotation.py b/ruoyi-fastapi-backend/common/annotation/log_annotation.py index 1e9ad17..769409c 100644 --- a/ruoyi-fastapi-backend/common/annotation/log_annotation.py +++ b/ruoyi-fastapi-backend/common/annotation/log_annotation.py @@ -20,6 +20,7 @@ from exceptions.exception import LoginException, ServiceException, ServiceWarning from module_admin.entity.vo.log_vo import LogininforModel, OperLogModel from module_admin.service.log_service import LogQueueService +from utils.client_ip_util import ClientIPUtil from utils.dependency_util import DependencyUtil from utils.log_util import logger from utils.response_util import ResponseUtil @@ -69,7 +70,7 @@ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: # 获取请求的url oper_url = request.url.path # 获取请求ip - oper_ip = request.headers.get('X-Forwarded-For') + oper_ip = ClientIPUtil.get_client_ip(request) # 获取请求ip归属区域 oper_location = await self._get_oper_location(oper_ip) # 获取请求参数 diff --git a/ruoyi-fastapi-backend/common/annotation/rate_limit_annotation.py b/ruoyi-fastapi-backend/common/annotation/rate_limit_annotation.py new file mode 100644 index 0000000..345c7bf --- /dev/null +++ b/ruoyi-fastapi-backend/common/annotation/rate_limit_annotation.py @@ -0,0 +1,712 @@ +import hashlib +import json +import time +import uuid +from collections import deque +from collections.abc import Awaitable, Callable, Sequence +from dataclasses import dataclass +from functools import wraps +from typing import Literal, TypeVar + +from fastapi import Request +from redis import asyncio as aioredis +from typing_extensions import ParamSpec + +from common.context import RequestContext +from common.enums import HttpMethod, RedisInitKeyConfig +from exceptions.exception import LoginException +from module_admin.entity.vo.user_vo import CurrentUserModel +from utils.api_annotation_util import ApiAnnotationUtil +from utils.api_response_header_util import ApiResponseHeaderUtil +from utils.client_ip_util import ClientIPUtil +from utils.log_util import logger +from utils.response_util import ResponseUtil + +P = ParamSpec('P') +R = TypeVar('R') +RateLimitScope = Literal['ip', 'user', 'user_or_ip'] +RateLimitAlgorithm = Literal['fixed_window', 'sliding_window'] +RateLimitFailStrategy = Literal['open', 'closed', 'local_fallback'] + + +@dataclass(frozen=True) +class ApiRateLimitPresetConfig: + """ + 接口限流预设配置 + """ + + name: str + limit: int + window_seconds: int + scope: RateLimitScope = 'ip' + algorithm: RateLimitAlgorithm = 'fixed_window' + fail_strategy: RateLimitFailStrategy = 'open' + methods: tuple[HttpMethod, ...] | None = None + message: str = '请求过于频繁,请稍后再试' + + +@dataclass(frozen=True) +class ApiRateLimitBypassConfig: + """ + 接口限流角色豁免配置 + """ + + roles: tuple[str, ...] + + +class ApiRateLimitPreset: + """ + 接口限流预设 + + ANON_AUTH_LOGIN: 匿名登录类接口限流预设 + ANON_AUTH_REGISTER: 匿名注册类接口限流预设 + ANON_AUTH_CAPTCHA: 匿名验证码类接口限流预设 + COMMON_UPLOAD: 通用上传接口限流预设 + USER_INTERACTIVE_HIGH_FREQ: 用户高频交互接口限流预设 + USER_RESOURCE_EXECUTION: 用户执行类接口限流预设 + USER_COMMON_MUTATION: 用户普通写操作接口限流预设 + USER_SECURITY_MUTATION: 用户安全敏感操作接口限流预设 + USER_DESTRUCTIVE_MUTATION: 用户破坏性操作接口限流预设 + USER_RESOURCE_EXPORT: 用户导出类接口限流预设 + USER_RESOURCE_IMPORT: 用户导入类接口限流预设 + USER_RESOURCE_UPLOAD: 用户上传类接口限流预设 + USER_RESOURCE_GENERATE: 用户生成类接口限流预设 + USER_RESOURCE_DOWNLOAD: 用户下载类接口限流预设 + USER_RESOURCE_SYNC: 用户同步类接口限流预设 + """ + + ANON_AUTH_LOGIN = ApiRateLimitPresetConfig( + name='ANON_AUTH_LOGIN', + limit=12, + window_seconds=60, + algorithm='sliding_window', + fail_strategy='local_fallback', + ) + ANON_AUTH_REGISTER = ApiRateLimitPresetConfig( + name='ANON_AUTH_REGISTER', + limit=6, + window_seconds=120, + algorithm='sliding_window', + fail_strategy='local_fallback', + ) + ANON_AUTH_CAPTCHA = ApiRateLimitPresetConfig( + name='ANON_AUTH_CAPTCHA', + limit=36, + window_seconds=60, + algorithm='sliding_window', + fail_strategy='local_fallback', + ) + COMMON_UPLOAD = ApiRateLimitPresetConfig( + name='COMMON_UPLOAD', + limit=24, + window_seconds=60, + scope='user_or_ip', + ) + + USER_INTERACTIVE_HIGH_FREQ = ApiRateLimitPresetConfig( + name='USER_INTERACTIVE_HIGH_FREQ', + limit=40, + window_seconds=60, + scope='user', + ) + USER_RESOURCE_EXECUTION = ApiRateLimitPresetConfig( + name='USER_RESOURCE_EXECUTION', + limit=12, + window_seconds=60, + scope='user', + ) + USER_COMMON_MUTATION = ApiRateLimitPresetConfig( + name='USER_COMMON_MUTATION', + limit=24, + window_seconds=120, + scope='user', + ) + USER_SECURITY_MUTATION = ApiRateLimitPresetConfig( + name='USER_SECURITY_MUTATION', + limit=12, + window_seconds=120, + scope='user', + ) + USER_DESTRUCTIVE_MUTATION = ApiRateLimitPresetConfig( + name='USER_DESTRUCTIVE_MUTATION', + limit=6, + window_seconds=120, + scope='user', + ) + USER_RESOURCE_EXPORT = ApiRateLimitPresetConfig( + name='USER_RESOURCE_EXPORT', + limit=15, + window_seconds=120, + scope='user', + ) + USER_RESOURCE_IMPORT = ApiRateLimitPresetConfig( + name='USER_RESOURCE_IMPORT', + limit=8, + window_seconds=120, + scope='user', + ) + USER_RESOURCE_UPLOAD = ApiRateLimitPresetConfig( + name='USER_RESOURCE_UPLOAD', + limit=12, + window_seconds=120, + scope='user', + ) + USER_RESOURCE_GENERATE = ApiRateLimitPresetConfig( + name='USER_RESOURCE_GENERATE', + limit=8, + window_seconds=120, + scope='user', + ) + USER_RESOURCE_DOWNLOAD = ApiRateLimitPresetConfig( + name='USER_RESOURCE_DOWNLOAD', + limit=12, + window_seconds=60, + scope='user', + ) + USER_RESOURCE_SYNC = ApiRateLimitPresetConfig( + name='USER_RESOURCE_SYNC', + limit=15, + window_seconds=120, + scope='user', + ) + + +class ApiRateLimit: + """ + 接口限流装饰器,支持Redis固定窗口、滑动窗口及本地应急兜底。 + + `local_fallback` 仅作为Redis异常场景下的进程内应急保护,能提供单进程内的 + 基础限流能力,但不保证多 worker / 多实例部署下的全局一致性。 + + `ip` 维度限流通过 `ClientIPUtil` 提取客户端地址,仅当请求来源命中 + `APP_TRUSTED_PROXY_IPS` 且 `APP_TRUSTED_PROXY_HOPS` 大于0时,才会解析 + `X-Forwarded-For` / `X-Real-IP` 请求头;否则回退到直接连接来源地址。 + """ + + _SUPPORTED_SCOPES: tuple[RateLimitScope, ...] = ('ip', 'user', 'user_or_ip') + _SUPPORTED_ALGORITHMS: tuple[RateLimitAlgorithm, ...] = ('fixed_window', 'sliding_window') + _SUPPORTED_FAIL_STRATEGIES: tuple[RateLimitFailStrategy, ...] = ('open', 'closed', 'local_fallback') + _FIXED_WINDOW_LUA_SCRIPT = """ + local current = redis.call('INCR', KEYS[1]) + local ttl = redis.call('PTTL', KEYS[1]) + if current == 1 or ttl < 0 then + redis.call('PEXPIRE', KEYS[1], ARGV[1]) + ttl = redis.call('PTTL', KEYS[1]) + end + local limit = tonumber(ARGV[2]) + local remaining = limit - current + if remaining < 0 then + remaining = 0 + end + local allowed = 0 + if current <= limit then + allowed = 1 + end + return {allowed, current, remaining, ttl} + """ + _SLIDING_WINDOW_LUA_SCRIPT = """ + redis.call('ZREMRANGEBYSCORE', KEYS[1], 0, tonumber(ARGV[1]) - tonumber(ARGV[2])) + local current = redis.call('ZCARD', KEYS[1]) + local limit = tonumber(ARGV[4]) + local ttl = ARGV[2] + if current >= limit then + local earliest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES') + if earliest[2] ~= nil then + ttl = tonumber(ARGV[2]) - (tonumber(ARGV[1]) - tonumber(earliest[2])) + end + if ttl < 1 then + ttl = 1 + end + return {0, current, 0, ttl} + end + redis.call('ZADD', KEYS[1], ARGV[1], ARGV[3]) + redis.call('PEXPIRE', KEYS[1], ARGV[2]) + current = current + 1 + local remaining = limit - current + if remaining < 0 then + remaining = 0 + end + local earliest = redis.call('ZRANGE', KEYS[1], 0, 0, 'WITHSCORES') + if earliest[2] ~= nil then + ttl = tonumber(ARGV[2]) - (tonumber(ARGV[1]) - tonumber(earliest[2])) + end + if ttl < 1 then + ttl = 1 + end + return {1, current, remaining, ttl} + """ + # 仅用于Redis异常时的单进程本地兜底,不承担分布式一致性职责。 + _LOCAL_FALLBACK_STORE: dict[str, deque[int]] = {} + + def __init__( + self, + namespace: str, + limit: int | None = None, + window_seconds: int | None = None, + scope: RateLimitScope | None = None, + algorithm: RateLimitAlgorithm | None = None, + fail_strategy: RateLimitFailStrategy | None = None, + bypass: ApiRateLimitBypassConfig | None = None, + methods: Sequence[HttpMethod] | None = None, + message: str | None = None, + preset: ApiRateLimitPresetConfig | None = None, + ) -> None: + """ + 初始化接口限流装饰器 + + :param namespace: 限流命名空间,用于区分不同接口 + :param limit: 窗口内允许的最大请求次数,可覆盖预设 + :param window_seconds: 限流窗口时长,单位秒,可覆盖预设 + :param scope: 限流作用域,ip: 按客户端IP限流,客户端IP由可信代理配置控制提取,user: 仅按当前登录用户限流,未登录请求跳过限流,user_or_ip: 已登录按用户限流,未登录按客户端IP限流,可覆盖预设 + :param algorithm: 限流算法,fixed_window: 固定窗口,sliding_window: 滑动窗口,可覆盖预设 + :param fail_strategy: 限流组件异常时的故障策略,open: 放行,closed: 直接拦截,local_fallback: 使用进程内内存做应急兜底限流,仅保证单进程内生效,可覆盖预设 + :param bypass: 角色豁免配置,仅在显式传入时生效 + :param methods: 需要限流的HttpMethod枚举列表,为None时默认限制所有方法,可覆盖预设 + :param message: 触发限流后的提示信息,可覆盖预设 + :param preset: 限流预设配置 + """ + resolved_limit = limit if limit is not None else preset.limit if preset else None + resolved_window_seconds = ( + window_seconds if window_seconds is not None else preset.window_seconds if preset else None + ) + resolved_scope = scope if scope is not None else preset.scope if preset else 'ip' + resolved_algorithm = algorithm if algorithm is not None else preset.algorithm if preset else 'fixed_window' + resolved_fail_strategy = ( + fail_strategy if fail_strategy is not None else preset.fail_strategy if preset else 'open' + ) + resolved_methods = methods if methods is not None else preset.methods if preset else None + resolved_message = message if message is not None else preset.message if preset else '请求过于频繁,请稍后再试' + resolved_preset_name = preset.name if preset else 'CUSTOM' + + if not namespace: + raise ValueError('ApiRateLimit的namespace不能为空') + if resolved_limit is None or resolved_limit <= 0: + raise ValueError('ApiRateLimit的limit必须大于0') + if resolved_window_seconds is None or resolved_window_seconds <= 0: + raise ValueError('ApiRateLimit的window_seconds必须大于0') + if resolved_scope not in self._SUPPORTED_SCOPES: + raise ValueError(f'ApiRateLimit的scope仅支持: {", ".join(self._SUPPORTED_SCOPES)}') + if resolved_algorithm not in self._SUPPORTED_ALGORITHMS: + raise ValueError(f'ApiRateLimit的algorithm仅支持: {", ".join(self._SUPPORTED_ALGORITHMS)}') + if resolved_fail_strategy not in self._SUPPORTED_FAIL_STRATEGIES: + raise ValueError(f'ApiRateLimit的fail_strategy仅支持: {", ".join(self._SUPPORTED_FAIL_STRATEGIES)}') + if bypass and resolved_scope == 'ip': + raise ValueError('ApiRateLimit在scope=ip时不支持角色豁免配置') + + self.namespace = namespace + self.preset_name = resolved_preset_name + self.limit = resolved_limit + self.window_seconds = resolved_window_seconds + self.scope = resolved_scope + self.algorithm = resolved_algorithm + self.fail_strategy = resolved_fail_strategy + self.bypass_roles = self._normalize_bypass_roles(bypass.roles if bypass else None) + self.methods = ApiAnnotationUtil.normalize_http_methods(resolved_methods) + self.message = resolved_message + + def __call__(self, func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]]: + """ + 为目标异步接口函数增加接口限流能力 + + :param func: 需要限流的异步接口函数 + :return: 包装后的异步接口函数 + """ + + @wraps(func) + async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: + request = ApiAnnotationUtil.get_request(func, *args, **kwargs) + if request is None: + return await func(*args, **kwargs) + if not self._is_request_method_allowed(request): + return await func(*args, **kwargs) + bypass_role = self._match_bypass_role() + if bypass_role is not None: + self._log_rate_limit_bypass(request, bypass_role) + return await func(*args, **kwargs) + + rate_limit_result: dict[str, int | bool] | None = None + redis = getattr(request.app.state, 'redis', None) + if redis is None: + rate_limit_result = self._resolve_failed_rate_limit( + request, reason='redis_unavailable', error_message='redis client is not initialized' + ) + if rate_limit_result is None: + return await func(*args, **kwargs) + else: + try: + rate_limit_result = await self._acquire_rate_limit(redis, request) + except Exception as exc: + rate_limit_result = self._resolve_failed_rate_limit( + request, reason='redis_error', error_message=str(exc) + ) + if rate_limit_result is None: + return await func(*args, **kwargs) + if rate_limit_result is None: + return await func(*args, **kwargs) + + headers = self._build_rate_limit_headers(rate_limit_result) + if not rate_limit_result['allowed']: + self._log_rate_limit_hit(request, rate_limit_result) + return ResponseUtil.too_many_requests(msg=self.message, headers=headers) # type: ignore[return-value] + + ApiResponseHeaderUtil.merge_headers(request, headers) + result = await func(*args, **kwargs) + return result + + return wrapper + + def _is_request_method_allowed(self, request: Request) -> bool: + """ + 判断当前请求方法是否启用限流 + + :param request: 当前请求对象 + :return: 是否启用限流 + """ + return not self.methods or request.method.upper() in self.methods + + def _normalize_bypass_roles(self, bypass_roles: Sequence[str] | None) -> tuple[str, ...]: + """ + 标准化角色豁免配置 + + :param bypass_roles: 原始角色标识列表 + :return: 去重后的角色标识元组 + """ + if not bypass_roles: + return () + + normalized_roles: list[str] = [] + for role in bypass_roles: + normalized_role = role.strip() + if not normalized_role: + raise ValueError('ApiRateLimit的bypass_roles不能包含空角色标识') + if normalized_role not in normalized_roles: + normalized_roles.append(normalized_role) + + return tuple(normalized_roles) + + def _match_bypass_role(self) -> str | None: + """ + 判断当前登录用户是否命中角色豁免 + + :return: 命中的角色标识,未命中时返回None + """ + if not self.bypass_roles: + return None + + current_user = self._get_current_user() + if current_user is None: + return None + + user_roles = {str(role).strip() for role in current_user.roles if str(role).strip()} + for role in self.bypass_roles: + if role in user_roles: + return role + + return None + + async def _acquire_rate_limit(self, redis: aioredis.Redis, request: Request) -> dict[str, int | bool] | None: + """ + 获取当前请求的限流计数结果 + + :param redis: Redis连接对象 + :param request: 当前请求对象 + :return: 限流结果,当前请求不适用限流时返回None + """ + current_time_ms = int(time.time() * 1000) + rate_limit_key = self._build_rate_limit_key(request, current_time_ms) + if rate_limit_key is None: + return None + if self.algorithm == 'sliding_window': + allowed, current, remaining, reset_after_ms = await redis.eval( + self._SLIDING_WINDOW_LUA_SCRIPT, + 1, + rate_limit_key, + current_time_ms, + self.window_seconds * 1000, + f'{current_time_ms}-{time.time_ns()}-{uuid.uuid4().hex}', + self.limit, + ) + else: + window_ms = self.window_seconds * 1000 + window_bucket = current_time_ms // window_ms + window_end_ms = (window_bucket + 1) * window_ms + ttl_ms = max(window_end_ms - current_time_ms, 1) + allowed, current, remaining, reset_after_ms = await redis.eval( + self._FIXED_WINDOW_LUA_SCRIPT, 1, rate_limit_key, ttl_ms, self.limit + ) + reset_after_ms = max(int(reset_after_ms), 1) + + return { + 'allowed': bool(int(allowed)), + 'current': int(current), + 'remaining': int(remaining), + 'reset_after_seconds': max((reset_after_ms + 999) // 1000, 1), + 'reset_at': (current_time_ms + reset_after_ms + 999) // 1000, + } + + def _build_rate_limit_key( + self, request: Request, current_time_ms: int, include_window_bucket: bool = True + ) -> str | None: + """ + 构建当前请求的限流键 + + :param request: 当前请求对象 + :param current_time_ms: 当前时间戳,单位毫秒 + :return: 限流键,当前请求不适用限流时返回None + """ + scope_value = self._get_scope_value(request) + if scope_value is None: + return None + + key_material = { + 'method': request.method.upper(), + 'path': self._get_route_path(request), + 'scope': self.scope, + 'scope_value': scope_value, + } + key_digest = hashlib.sha256( + json.dumps( + key_material, + ensure_ascii=False, + sort_keys=True, + separators=(',', ':'), + ).encode('utf-8') + ).hexdigest() + + key_prefix = f'{RedisInitKeyConfig.API_RATE_LIMIT.key}:{self.namespace}:{self.algorithm}:{key_digest}' + if self.algorithm == 'fixed_window' and include_window_bucket: + window_bucket = current_time_ms // (self.window_seconds * 1000) + return f'{key_prefix}:{window_bucket}' + + return key_prefix + + def _resolve_failed_rate_limit( + self, request: Request, reason: str, error_message: str + ) -> dict[str, int | bool] | None: + """ + 处理Redis不可用或执行异常时的限流故障策略 + + :param request: 当前请求对象 + :param reason: 降级原因 + :param error_message: 错误详情 + :return: 限流结果,为None时表示按策略放行 + """ + self._log_rate_limit_degrade(request, reason, error_message) + if self.fail_strategy == 'open': + return None + if self.fail_strategy == 'closed': + return self._build_closed_rate_limit_result() + + return self._acquire_local_fallback_rate_limit(request) + + def _acquire_local_fallback_rate_limit(self, request: Request) -> dict[str, int | bool] | None: + """ + 使用进程内内存进行应急兜底限流,仅在Redis异常时启用 + + 该兜底能力仅在当前进程内生效,不保证多 worker / 多实例场景下的 + 全局一致限流,更适合作为短时故障期间的降级保护。 + + :param request: 当前请求对象 + :return: 限流结果,当前请求不适用限流时返回None + """ + current_time_ms = int(time.time() * 1000) + rate_limit_key = self._build_rate_limit_key(request, current_time_ms, include_window_bucket=False) + if rate_limit_key is None: + return None + + window_ms = self.window_seconds * 1000 + window_start_ms = current_time_ms - window_ms + local_window = self._LOCAL_FALLBACK_STORE.setdefault(rate_limit_key, deque()) + while local_window and local_window[0] <= window_start_ms: + local_window.popleft() + if not local_window: + self._LOCAL_FALLBACK_STORE.pop(rate_limit_key, None) + local_window = deque() + self._LOCAL_FALLBACK_STORE[rate_limit_key] = local_window + + current = len(local_window) + if current >= self.limit: + reset_after_ms = max(local_window[0] + window_ms - current_time_ms, 1) if local_window else window_ms + return { + 'allowed': False, + 'current': current, + 'remaining': 0, + 'reset_after_seconds': max((reset_after_ms + 999) // 1000, 1), + 'reset_at': (current_time_ms + reset_after_ms + 999) // 1000, + } + + local_window.append(current_time_ms) + current += 1 + reset_after_ms = max(local_window[0] + window_ms - current_time_ms, 1) + + return { + 'allowed': True, + 'current': current, + 'remaining': max(self.limit - current, 0), + 'reset_after_seconds': max((reset_after_ms + 999) // 1000, 1), + 'reset_at': (current_time_ms + reset_after_ms + 999) // 1000, + } + + def _build_closed_rate_limit_result(self) -> dict[str, int | bool]: + """ + 构建故障关闭策略下的拦截结果 + + :return: 限流结果 + """ + reset_after_seconds = max(self.window_seconds, 1) + current_time_ms = int(time.time() * 1000) + reset_after_ms = reset_after_seconds * 1000 + return { + 'allowed': False, + 'current': self.limit, + 'remaining': 0, + 'reset_after_seconds': reset_after_seconds, + 'reset_at': (current_time_ms + reset_after_ms + 999) // 1000, + } + + def _get_route_path(self, request: Request) -> str: + """ + 获取当前请求的路由模板路径 + + :param request: 当前请求对象 + :return: 路由模板路径 + """ + route = request.scope.get('route') + route_path = getattr(route, 'path', None) + return route_path or request.url.path + + def _get_scope_value(self, request: Request) -> str | None: + """ + 获取当前请求的限流作用域值 + + :param request: 当前请求对象 + :return: 作用域值,当前请求不适用限流时返回None + """ + if self.scope == 'ip': + return f'ip:{self._get_client_ip(request)}' + + current_user_id = self._get_current_user_id() + if current_user_id is not None: + return f'user:{current_user_id}' + + if self.scope == 'user': + return None + + return f'ip:{self._get_client_ip(request)}' + + def _get_current_user_id(self) -> int | None: + """ + 获取当前登录用户ID + + :return: 用户ID,未登录时返回None + """ + current_user = self._get_current_user() + return current_user.user.user_id if current_user and current_user.user else None + + def _get_current_user(self) -> CurrentUserModel | None: + """ + 获取当前登录用户 + + :return: 当前登录用户,未登录时返回None + """ + try: + return RequestContext.get_current_user() + except LoginException: + return None + + def _get_client_ip(self, request: Request) -> str: + """ + 获取客户端IP地址 + + :param request: 当前请求对象 + :return: 客户端IP地址 + """ + return ClientIPUtil.get_client_ip(request) + + def _build_rate_limit_headers(self, rate_limit_result: dict[str, int | bool]) -> dict[str, str]: + """ + 构建限流响应头 + + :param rate_limit_result: 限流结果 + :return: 限流响应头 + """ + headers = { + 'X-RateLimit-Limit': str(self.limit), + 'X-RateLimit-Remaining': str(rate_limit_result['remaining']), + 'X-RateLimit-Reset': str(rate_limit_result['reset_at']), + } + if not rate_limit_result['allowed']: + headers['Retry-After'] = str(rate_limit_result['reset_after_seconds']) + + return headers + + def _log_rate_limit_hit(self, request: Request, rate_limit_result: dict[str, int | bool]) -> None: + """ + 记录限流命中日志 + + :param request: 当前请求对象 + :param rate_limit_result: 限流结果 + :return: None + """ + logger.warning( + '接口限流命中: namespace={} preset={} algorithm={} fail_strategy={} method={} path={} scope={} scope_value={} current={} limit={} retry_after={}s', + self.namespace, + self.preset_name, + self.algorithm, + self.fail_strategy, + request.method.upper(), + self._get_route_path(request), + self.scope, + self._get_scope_value(request), + rate_limit_result['current'], + self.limit, + rate_limit_result['reset_after_seconds'], + ) + + def _log_rate_limit_degrade(self, request: Request, reason: str, error_message: str) -> None: + """ + 记录限流组件异常时的降级日志 + + :param request: 当前请求对象 + :param reason: 降级原因 + :param error_message: 错误详情 + :return: None + """ + log_message = ( + '接口限流降级: namespace={} preset={} algorithm={} fail_strategy={} ' + 'method={} path={} scope={} reason={} error={}' + ) + log_args: list[str] = [ + self.namespace, + self.preset_name, + self.algorithm, + self.fail_strategy, + request.method.upper(), + self._get_route_path(request), + self.scope, + reason, + error_message, + ] + if self.fail_strategy == 'local_fallback': + log_message += ' local_fallback_scope={}' + log_args.append('process_local_only') + + logger.warning(log_message, *log_args) + + def _log_rate_limit_bypass(self, request: Request, bypass_role: str) -> None: + """ + 记录角色豁免限流日志 + + :param request: 当前请求对象 + :param bypass_role: 命中的角色标识 + :return: None + """ + logger.info( + '接口限流绕过: namespace={} preset={} method={} path={} scope={} bypass_role={}', + self.namespace, + self.preset_name, + request.method.upper(), + self._get_route_path(request), + self.scope, + bypass_role, + ) diff --git a/ruoyi-fastapi-backend/common/constant.py b/ruoyi-fastapi-backend/common/constant.py index 932b01b..90cda58 100644 --- a/ruoyi-fastapi-backend/common/constant.py +++ b/ruoyi-fastapi-backend/common/constant.py @@ -53,6 +53,7 @@ class HttpStatusConstant: BAD_METHOD: 不允许的http方法 CONFLICT: 资源冲突,或者资源被锁 UNSUPPORTED_TYPE: 不支持的数据,媒体类型 + TOO_MANY_REQUESTS: 请求过于频繁 ERROR: 系统内部错误 NOT_IMPLEMENTED: 接口未实现 WARN: 系统警告消息 @@ -72,6 +73,7 @@ class HttpStatusConstant: BAD_METHOD = 405 CONFLICT = 409 UNSUPPORTED_TYPE = 415 + TOO_MANY_REQUESTS = 429 ERROR = 500 NOT_IMPLEMENTED = 501 WARN = 601 @@ -143,194 +145,287 @@ class LockConstant: LOCK_RENEWAL_INTERVAL = 20 -class CacheNamespace: +class ApiNamespace: """ - 接口缓存命名空间常量 - - MONITOR_SERVER_INFO: 服务监控信息缓存 - MONITOR_JOB_LIST: 定时任务分页列表缓存 - MONITOR_JOB_DETAIL: 定时任务详情缓存 - LOGIN_USER_INFO: 登录用户信息缓存 - LOGIN_USER_ROUTERS: 登录用户路由缓存 - SYSTEM_MENU_TREE: 菜单树缓存 - SYSTEM_MENU_ROLE_TREE: 角色菜单树缓存 - SYSTEM_MENU_LIST: 菜单分页列表缓存 - SYSTEM_MENU_DETAIL: 菜单详情缓存 - SYSTEM_DEPT_EDIT_TREE: 部门编辑树缓存 - SYSTEM_DEPT_LIST: 部门列表缓存 - SYSTEM_DEPT_DETAIL: 部门详情缓存 - SYSTEM_POST_LIST: 岗位列表缓存 - SYSTEM_POST_DETAIL: 岗位详情缓存 - SYSTEM_NOTICE_LIST: 通知公告列表缓存 - SYSTEM_NOTICE_DETAIL: 通知公告详情缓存 - SYSTEM_ROLE_DEPT_TREE: 角色部门树缓存 - SYSTEM_ROLE_LIST: 角色列表缓存 - SYSTEM_ROLE_DETAIL: 角色详情缓存 - SYSTEM_ROLE_ALLOCATED_USER_LIST: 已分配用户角色列表缓存 - SYSTEM_ROLE_UNALLOCATED_USER_LIST: 未分配用户角色列表缓存 - SYSTEM_USER_DEPT_TREE: 用户部门树缓存 - SYSTEM_USER_LIST: 用户列表缓存 - SYSTEM_USER_PROFILE: 用户个人信息缓存 - SYSTEM_USER_DETAIL: 用户详情缓存 - SYSTEM_CONFIG_LIST: 参数配置列表缓存 - SYSTEM_CONFIG_DETAIL: 参数配置详情缓存 - SYSTEM_DICT_TYPE_LIST: 字典类型列表缓存 - SYSTEM_DICT_TYPE_OPTIONS: 字典类型选项缓存 - SYSTEM_DICT_TYPE_DETAIL: 字典类型详情缓存 - SYSTEM_DICT_DATA_LIST: 字典数据列表缓存 - SYSTEM_DICT_DATA_DETAIL: 字典数据详情缓存 - AI_MODEL_LIST: AI模型列表缓存 - AI_MODEL_ALL: AI模型全量列表缓存 - AI_MODEL_DETAIL: AI模型详情缓存 - AI_CHAT_CONFIG: AI对话配置缓存 - TOOL_GEN_LIST: 代码生成列表缓存 - TOOL_GEN_DB_LIST: 代码生成数据源列表缓存 - TOOL_GEN_DETAIL: 代码生成详情缓存 - TOOL_GEN_PREVIEW: 代码生成预览缓存 + 接口注解通用命名空间常量 + + 这一组常量统一提供给 `ApiCache`、`ApiCacheEvict`、`ApiRateLimit` + 等接口注解使用,用同一套“模块:功能”命名规则收敛缓存和限流场景。 + + LOGIN: 登录接口命名空间 + REGISTER: 注册接口命名空间 + LOGIN_USER_INFO: 登录用户信息接口命名空间 + LOGIN_USER_ROUTERS: 登录用户路由接口命名空间 + CAPTCHA_IMAGE: 图片验证码接口命名空间 + COMMON_UPLOAD: 通用上传接口命名空间 + + MONITOR_SERVER_INFO: 服务监控信息接口命名空间 + MONITOR_CACHE_CLEAR_NAME: 缓存名称清理接口命名空间 + MONITOR_CACHE_CLEAR_KEY: 缓存键清理接口命名空间 + MONITOR_CACHE_CLEAR_ALL: 缓存全量清理接口命名空间 + MONITOR_ONLINE_FORCE_LOGOUT: 在线用户强退接口命名空间 + MONITOR_OPERLOG_CLEAN: 操作日志清空接口命名空间 + MONITOR_OPERLOG_DELETE: 操作日志删除接口命名空间 + MONITOR_OPERLOG_EXPORT: 操作日志导出接口命名空间 + MONITOR_LOGININFO_CLEAN: 登录日志清空接口命名空间 + MONITOR_LOGININFO_DELETE: 登录日志删除接口命名空间 + MONITOR_LOGININFO_UNLOCK: 账户解锁接口命名空间 + MONITOR_LOGININFO_EXPORT: 登录日志导出接口命名空间 + MONITOR_JOB_LIST: 定时任务分页列表接口命名空间 + MONITOR_JOB_DETAIL: 定时任务详情接口命名空间 + MONITOR_JOB_RUN: 定时任务执行接口命名空间 + MONITOR_JOB_DELETE: 定时任务删除接口命名空间 + MONITOR_JOB_EXPORT: 定时任务导出接口命名空间 + MONITOR_JOB_LOG_CLEAN: 定时任务日志清空接口命名空间 + MONITOR_JOB_LOG_DELETE: 定时任务日志删除接口命名空间 + MONITOR_JOB_LOG_EXPORT: 定时任务日志导出接口命名空间 + + SYSTEM_DEPT_EDIT_TREE: 部门编辑树接口命名空间 + SYSTEM_DEPT_LIST: 部门列表接口命名空间 + SYSTEM_DEPT_DETAIL: 部门详情接口命名空间 + SYSTEM_CONFIG_LIST: 参数配置列表接口命名空间 + SYSTEM_CONFIG_DETAIL: 参数配置详情接口命名空间 + SYSTEM_CONFIG_REFRESH_CACHE: 参数缓存刷新接口命名空间 + SYSTEM_CONFIG_EXPORT: 参数导出接口命名空间 + SYSTEM_DICT_TYPE_LIST: 字典类型列表接口命名空间 + SYSTEM_DICT_TYPE_OPTIONS: 字典类型选项接口命名空间 + SYSTEM_DICT_TYPE_DETAIL: 字典类型详情接口命名空间 + SYSTEM_DICT_REFRESH_CACHE: 字典缓存刷新接口命名空间 + SYSTEM_DICT_TYPE_EXPORT: 字典类型导出接口命名空间 + SYSTEM_DICT_DATA_LIST: 字典数据列表接口命名空间 + SYSTEM_DICT_DATA_DETAIL: 字典数据详情接口命名空间 + SYSTEM_DICT_DATA_EXPORT: 字典数据导出接口命名空间 + SYSTEM_MENU_TREE: 菜单树接口命名空间 + SYSTEM_MENU_ROLE_TREE: 角色菜单树接口命名空间 + SYSTEM_MENU_LIST: 菜单分页列表接口命名空间 + SYSTEM_MENU_DETAIL: 菜单详情接口命名空间 + SYSTEM_NOTICE_LIST: 通知公告列表接口命名空间 + SYSTEM_NOTICE_DETAIL: 通知公告详情接口命名空间 + SYSTEM_POST_LIST: 岗位列表接口命名空间 + SYSTEM_POST_DETAIL: 岗位详情接口命名空间 + SYSTEM_POST_EXPORT: 岗位导出接口命名空间 + SYSTEM_ROLE_DEPT_TREE: 角色部门树接口命名空间 + SYSTEM_ROLE_LIST: 角色列表接口命名空间 + SYSTEM_ROLE_DETAIL: 角色详情接口命名空间 + SYSTEM_ROLE_ALLOCATED_USER_LIST: 已分配用户角色列表接口命名空间 + SYSTEM_ROLE_UNALLOCATED_USER_LIST: 未分配用户角色列表接口命名空间 + SYSTEM_ROLE_EXPORT: 角色导出接口命名空间 + SYSTEM_ROLE_AUTH_USER_SELECT_ALL: 角色批量分配用户接口命名空间 + SYSTEM_ROLE_AUTH_USER_CANCEL: 角色取消分配用户接口命名空间 + SYSTEM_ROLE_AUTH_USER_CANCEL_ALL: 角色批量取消分配用户接口命名空间 + SYSTEM_USER_DEPT_TREE: 用户部门树接口命名空间 + SYSTEM_USER_LIST: 用户列表接口命名空间 + SYSTEM_USER_PROFILE: 用户个人信息接口命名空间 + SYSTEM_USER_DETAIL: 用户详情接口命名空间 + SYSTEM_USER_PROFILE_AVATAR: 用户头像上传接口命名空间 + SYSTEM_USER_IMPORT: 用户导入接口命名空间 + SYSTEM_USER_EXPORT: 用户导出接口命名空间 + + AI_MODEL_LIST: AI模型列表接口命名空间 + AI_MODEL_ALL: AI模型全量列表接口命名空间 + AI_MODEL_DETAIL: AI模型详情接口命名空间 + AI_CHAT_CONFIG: AI对话配置接口命名空间 + AI_CHAT_SEND: AI对话发送接口命名空间 + AI_CHAT_CANCEL: AI对话取消接口命名空间 + + TOOL_GEN_LIST: 代码生成列表接口命名空间 + TOOL_GEN_DB_LIST: 代码生成数据源列表接口命名空间 + TOOL_GEN_DETAIL: 代码生成详情接口命名空间 + TOOL_GEN_PREVIEW: 代码生成预览接口命名空间 + TOOL_GEN_IMPORT_TABLE: 代码生成导入表接口命名空间 + TOOL_GEN_CREATE_TABLE: 代码生成建表接口命名空间 + TOOL_GEN_BATCH_GEN_CODE: 代码生成批量下载接口命名空间 + TOOL_GEN_GEN_CODE_LOCAL: 代码生成到本地接口命名空间 + TOOL_GEN_SYNC_DB: 代码生成同步库结构接口命名空间 """ + LOGIN = 'login' + REGISTER = 'register' + LOGIN_USER_INFO = 'login:user:info' + LOGIN_USER_ROUTERS = 'login:user:routers' + CAPTCHA_IMAGE = 'captcha:image' + COMMON_UPLOAD = 'common:upload' + MONITOR_SERVER_INFO = 'monitor:server:info' + MONITOR_CACHE_CLEAR_NAME = 'monitor:cache:clear-name' + MONITOR_CACHE_CLEAR_KEY = 'monitor:cache:clear-key' + MONITOR_CACHE_CLEAR_ALL = 'monitor:cache:clear-all' + MONITOR_ONLINE_FORCE_LOGOUT = 'monitor:online:force-logout' + MONITOR_OPERLOG_CLEAN = 'monitor:operlog:clean' + MONITOR_OPERLOG_DELETE = 'monitor:operlog:delete' + MONITOR_OPERLOG_EXPORT = 'monitor:operlog:export' + MONITOR_LOGININFO_CLEAN = 'monitor:logininfor:clean' + MONITOR_LOGININFO_DELETE = 'monitor:logininfor:delete' + MONITOR_LOGININFO_UNLOCK = 'monitor:logininfor:unlock' + MONITOR_LOGININFO_EXPORT = 'monitor:logininfor:export' MONITOR_JOB_LIST = 'monitor:job:list' MONITOR_JOB_DETAIL = 'monitor:job:detail' + MONITOR_JOB_RUN = 'monitor:job:run' + MONITOR_JOB_DELETE = 'monitor:job:delete' + MONITOR_JOB_EXPORT = 'monitor:job:export' + MONITOR_JOB_LOG_CLEAN = 'monitor:job-log:clean' + MONITOR_JOB_LOG_DELETE = 'monitor:job-log:delete' + MONITOR_JOB_LOG_EXPORT = 'monitor:job-log:export' - LOGIN_USER_INFO = 'login:user:info' - LOGIN_USER_ROUTERS = 'login:user:routers' + SYSTEM_DEPT_EDIT_TREE = 'system:dept:edit-tree' + SYSTEM_DEPT_LIST = 'system:dept:list' + SYSTEM_DEPT_DETAIL = 'system:dept:detail' + + SYSTEM_CONFIG_LIST = 'system:config:list' + SYSTEM_CONFIG_DETAIL = 'system:config:detail' + SYSTEM_CONFIG_REFRESH_CACHE = 'system:config:refresh-cache' + SYSTEM_CONFIG_EXPORT = 'system:config:export' + + SYSTEM_DICT_TYPE_LIST = 'system:dict:type-list' + SYSTEM_DICT_TYPE_OPTIONS = 'system:dict:type-options' + SYSTEM_DICT_TYPE_DETAIL = 'system:dict:type-detail' + SYSTEM_DICT_REFRESH_CACHE = 'system:dict:refresh-cache' + SYSTEM_DICT_TYPE_EXPORT = 'system:dict:type-export' + SYSTEM_DICT_DATA_LIST = 'system:dict:data-list' + SYSTEM_DICT_DATA_DETAIL = 'system:dict:data-detail' + SYSTEM_DICT_DATA_EXPORT = 'system:dict:data-export' SYSTEM_MENU_TREE = 'system:menu:tree' SYSTEM_MENU_ROLE_TREE = 'system:menu:role-tree' SYSTEM_MENU_LIST = 'system:menu:list' SYSTEM_MENU_DETAIL = 'system:menu:detail' - SYSTEM_DEPT_EDIT_TREE = 'system:dept:edit-tree' - SYSTEM_DEPT_LIST = 'system:dept:list' - SYSTEM_DEPT_DETAIL = 'system:dept:detail' + SYSTEM_NOTICE_LIST = 'system:notice:list' + SYSTEM_NOTICE_DETAIL = 'system:notice:detail' SYSTEM_POST_LIST = 'system:post:list' SYSTEM_POST_DETAIL = 'system:post:detail' - - SYSTEM_NOTICE_LIST = 'system:notice:list' - SYSTEM_NOTICE_DETAIL = 'system:notice:detail' + SYSTEM_POST_EXPORT = 'system:post:export' SYSTEM_ROLE_DEPT_TREE = 'system:role:dept-tree' SYSTEM_ROLE_LIST = 'system:role:list' SYSTEM_ROLE_DETAIL = 'system:role:detail' SYSTEM_ROLE_ALLOCATED_USER_LIST = 'system:role:allocated-user-list' SYSTEM_ROLE_UNALLOCATED_USER_LIST = 'system:role:unallocated-user-list' + SYSTEM_ROLE_EXPORT = 'system:role:export' + SYSTEM_ROLE_AUTH_USER_SELECT_ALL = 'system:role:auth-user-select-all' + SYSTEM_ROLE_AUTH_USER_CANCEL = 'system:role:auth-user-cancel' + SYSTEM_ROLE_AUTH_USER_CANCEL_ALL = 'system:role:auth-user-cancel-all' SYSTEM_USER_DEPT_TREE = 'system:user:dept-tree' SYSTEM_USER_LIST = 'system:user:list' SYSTEM_USER_PROFILE = 'system:user:profile' SYSTEM_USER_DETAIL = 'system:user:detail' - - SYSTEM_CONFIG_LIST = 'system:config:list' - SYSTEM_CONFIG_DETAIL = 'system:config:detail' - - SYSTEM_DICT_TYPE_LIST = 'system:dict:type-list' - SYSTEM_DICT_TYPE_OPTIONS = 'system:dict:type-options' - SYSTEM_DICT_TYPE_DETAIL = 'system:dict:type-detail' - SYSTEM_DICT_DATA_LIST = 'system:dict:data-list' - SYSTEM_DICT_DATA_DETAIL = 'system:dict:data-detail' + SYSTEM_USER_PROFILE_AVATAR = 'system:user:profile-avatar' + SYSTEM_USER_IMPORT = 'system:user:import' + SYSTEM_USER_EXPORT = 'system:user:export' AI_MODEL_LIST = 'ai:model:list' AI_MODEL_ALL = 'ai:model:all' AI_MODEL_DETAIL = 'ai:model:detail' AI_CHAT_CONFIG = 'ai:chat:config' + AI_CHAT_SEND = 'ai:chat:send' + AI_CHAT_CANCEL = 'ai:chat:cancel' TOOL_GEN_LIST = 'tool:gen:list' TOOL_GEN_DB_LIST = 'tool:gen:db-list' TOOL_GEN_DETAIL = 'tool:gen:detail' TOOL_GEN_PREVIEW = 'tool:gen:preview' + TOOL_GEN_IMPORT_TABLE = 'tool:gen:import-table' + TOOL_GEN_CREATE_TABLE = 'tool:gen:create-table' + TOOL_GEN_BATCH_GEN_CODE = 'tool:gen:batch-gen-code' + TOOL_GEN_GEN_CODE_LOCAL = 'tool:gen:gen-code-local' + TOOL_GEN_SYNC_DB = 'tool:gen:sync-db' + -class CacheGroup: +class ApiGroup: """ - 接口缓存失效分组常量 - - PERMISSION_MUTATION: 权限及菜单视图关联缓存失效基础分组 - DATA_SCOPE_MUTATION: 数据范围相关视图关联缓存失效基础分组 - MENU_MUTATION: 菜单写操作关联缓存失效分组 - JOB_MUTATION: 定时任务写操作关联缓存失效分组 - POST_MUTATION: 岗位写操作关联缓存失效分组 - NOTICE_MUTATION: 通知公告写操作关联缓存失效分组 - ROLE_ENTITY_MUTATION: 角色实体信息变更关联缓存失效分组 - ROLE_PERMISSION_MUTATION: 角色权限变更关联缓存失效分组 - ROLE_MUTATION: 角色通用写操作关联缓存失效组合分组 - USER_ENTITY_MUTATION: 用户实体信息变更关联缓存失效分组 - USER_PERMISSION_MUTATION: 用户权限变更关联缓存失效分组 - USER_INFO_MUTATION: 用户资料与安全相关写操作关联缓存失效分组 - LOGIN_SUCCESS_MUTATION: 登录成功后关联缓存失效分组 - LOGOUT_MUTATION: 登出后关联缓存失效分组 - CONFIG_MUTATION: 参数配置写操作关联缓存失效分组 - DICT_TYPE_MUTATION: 字典类型写操作关联缓存失效分组 - DICT_DATA_MUTATION: 字典数据写操作关联缓存失效分组 - AI_MODEL_MUTATION: AI模型写操作关联缓存失效分组 - AI_CHAT_CONFIG_MUTATION: AI对话配置写操作关联缓存失效分组 - GEN_MUTATION: 代码生成写操作关联缓存失效分组 + 接口命名空间分组常量 + + 当前主要用于 `ApiCacheEvict` 批量清理关联命名空间,分组成员均来自 + `ApiNamespace`,按业务写操作影响范围收敛管理。 + + PERMISSION_MUTATION: 权限与菜单视图关联命名空间分组 + DATA_SCOPE_MUTATION: 数据范围相关视图关联命名空间分组 + MENU_MUTATION: 菜单写操作关联命名空间分组 + JOB_MUTATION: 定时任务写操作关联命名空间分组 + POST_MUTATION: 岗位写操作关联命名空间分组 + NOTICE_MUTATION: 通知公告写操作关联命名空间分组 + ROLE_ENTITY_MUTATION: 角色实体信息变更关联命名空间分组 + ROLE_PERMISSION_MUTATION: 角色权限变更关联命名空间分组 + ROLE_MUTATION: 角色通用写操作关联命名空间组合分组 + USER_ENTITY_MUTATION: 用户实体信息变更关联命名空间分组 + USER_PERMISSION_MUTATION: 用户权限变更关联命名空间分组 + USER_INFO_MUTATION: 用户资料与安全相关写操作关联命名空间分组 + LOGIN_SUCCESS_MUTATION: 登录成功后关联命名空间分组 + LOGOUT_MUTATION: 登出后关联命名空间分组 + CONFIG_MUTATION: 参数配置写操作关联命名空间分组 + DICT_TYPE_MUTATION: 字典类型写操作关联命名空间分组 + DICT_DATA_MUTATION: 字典数据写操作关联命名空间分组 + AI_MODEL_MUTATION: AI模型写操作关联命名空间分组 + AI_CHAT_CONFIG_MUTATION: AI对话配置写操作关联命名空间分组 + GEN_MUTATION: 代码生成写操作关联命名空间分组 """ PERMISSION_MUTATION = ( - CacheNamespace.LOGIN_USER_INFO, - CacheNamespace.LOGIN_USER_ROUTERS, - CacheNamespace.SYSTEM_MENU_TREE, - CacheNamespace.SYSTEM_MENU_ROLE_TREE, - CacheNamespace.SYSTEM_MENU_LIST, + ApiNamespace.LOGIN_USER_INFO, + ApiNamespace.LOGIN_USER_ROUTERS, + ApiNamespace.SYSTEM_MENU_TREE, + ApiNamespace.SYSTEM_MENU_ROLE_TREE, + ApiNamespace.SYSTEM_MENU_LIST, ) DATA_SCOPE_MUTATION = ( - CacheNamespace.LOGIN_USER_INFO, - CacheNamespace.SYSTEM_DEPT_EDIT_TREE, - CacheNamespace.SYSTEM_DEPT_LIST, - CacheNamespace.SYSTEM_DEPT_DETAIL, - CacheNamespace.SYSTEM_ROLE_DEPT_TREE, - CacheNamespace.SYSTEM_ROLE_LIST, - CacheNamespace.SYSTEM_ROLE_DETAIL, - CacheNamespace.SYSTEM_ROLE_ALLOCATED_USER_LIST, - CacheNamespace.SYSTEM_ROLE_UNALLOCATED_USER_LIST, - CacheNamespace.SYSTEM_USER_DEPT_TREE, - CacheNamespace.SYSTEM_USER_LIST, - CacheNamespace.SYSTEM_USER_DETAIL, - CacheNamespace.SYSTEM_USER_PROFILE, - CacheNamespace.AI_MODEL_LIST, - CacheNamespace.AI_MODEL_ALL, - CacheNamespace.AI_MODEL_DETAIL, + ApiNamespace.LOGIN_USER_INFO, + ApiNamespace.SYSTEM_DEPT_EDIT_TREE, + ApiNamespace.SYSTEM_DEPT_LIST, + ApiNamespace.SYSTEM_DEPT_DETAIL, + ApiNamespace.SYSTEM_ROLE_DEPT_TREE, + ApiNamespace.SYSTEM_ROLE_LIST, + ApiNamespace.SYSTEM_ROLE_DETAIL, + ApiNamespace.SYSTEM_ROLE_ALLOCATED_USER_LIST, + ApiNamespace.SYSTEM_ROLE_UNALLOCATED_USER_LIST, + ApiNamespace.SYSTEM_USER_DEPT_TREE, + ApiNamespace.SYSTEM_USER_LIST, + ApiNamespace.SYSTEM_USER_DETAIL, + ApiNamespace.SYSTEM_USER_PROFILE, + ApiNamespace.AI_MODEL_LIST, + ApiNamespace.AI_MODEL_ALL, + ApiNamespace.AI_MODEL_DETAIL, ) MENU_MUTATION = ( *PERMISSION_MUTATION, - CacheNamespace.SYSTEM_MENU_DETAIL, + ApiNamespace.SYSTEM_MENU_DETAIL, ) JOB_MUTATION = ( - CacheNamespace.MONITOR_JOB_LIST, - CacheNamespace.MONITOR_JOB_DETAIL, + ApiNamespace.MONITOR_JOB_LIST, + ApiNamespace.MONITOR_JOB_DETAIL, ) POST_MUTATION = ( - CacheNamespace.SYSTEM_POST_LIST, - CacheNamespace.SYSTEM_POST_DETAIL, - CacheNamespace.SYSTEM_USER_DETAIL, - CacheNamespace.SYSTEM_USER_PROFILE, + ApiNamespace.SYSTEM_POST_LIST, + ApiNamespace.SYSTEM_POST_DETAIL, + ApiNamespace.SYSTEM_USER_DETAIL, + ApiNamespace.SYSTEM_USER_PROFILE, ) NOTICE_MUTATION = ( - CacheNamespace.SYSTEM_NOTICE_LIST, - CacheNamespace.SYSTEM_NOTICE_DETAIL, + ApiNamespace.SYSTEM_NOTICE_LIST, + ApiNamespace.SYSTEM_NOTICE_DETAIL, ) ROLE_ENTITY_MUTATION = ( - CacheNamespace.SYSTEM_ROLE_DEPT_TREE, - CacheNamespace.SYSTEM_ROLE_LIST, - CacheNamespace.SYSTEM_ROLE_DETAIL, - CacheNamespace.SYSTEM_ROLE_ALLOCATED_USER_LIST, - CacheNamespace.SYSTEM_ROLE_UNALLOCATED_USER_LIST, - CacheNamespace.SYSTEM_MENU_ROLE_TREE, - CacheNamespace.SYSTEM_USER_DETAIL, + ApiNamespace.SYSTEM_ROLE_DEPT_TREE, + ApiNamespace.SYSTEM_ROLE_LIST, + ApiNamespace.SYSTEM_ROLE_DETAIL, + ApiNamespace.SYSTEM_ROLE_ALLOCATED_USER_LIST, + ApiNamespace.SYSTEM_ROLE_UNALLOCATED_USER_LIST, + ApiNamespace.SYSTEM_MENU_ROLE_TREE, + ApiNamespace.SYSTEM_USER_DETAIL, ) ROLE_PERMISSION_MUTATION = ( *ROLE_ENTITY_MUTATION, - CacheNamespace.SYSTEM_USER_PROFILE, - CacheNamespace.LOGIN_USER_INFO, + ApiNamespace.SYSTEM_USER_PROFILE, + ApiNamespace.LOGIN_USER_INFO, *PERMISSION_MUTATION, ) @@ -340,10 +435,10 @@ class CacheGroup: ) USER_ENTITY_MUTATION = ( - CacheNamespace.SYSTEM_USER_LIST, - CacheNamespace.SYSTEM_USER_DETAIL, - CacheNamespace.SYSTEM_ROLE_ALLOCATED_USER_LIST, - CacheNamespace.SYSTEM_ROLE_UNALLOCATED_USER_LIST, + ApiNamespace.SYSTEM_USER_LIST, + ApiNamespace.SYSTEM_USER_DETAIL, + ApiNamespace.SYSTEM_ROLE_ALLOCATED_USER_LIST, + ApiNamespace.SYSTEM_ROLE_UNALLOCATED_USER_LIST, ) USER_PERMISSION_MUTATION = ( @@ -352,56 +447,56 @@ class CacheGroup: ) USER_INFO_MUTATION = ( - CacheNamespace.SYSTEM_USER_LIST, - CacheNamespace.SYSTEM_USER_DETAIL, - CacheNamespace.SYSTEM_USER_PROFILE, - CacheNamespace.LOGIN_USER_INFO, + ApiNamespace.SYSTEM_USER_LIST, + ApiNamespace.SYSTEM_USER_DETAIL, + ApiNamespace.SYSTEM_USER_PROFILE, + ApiNamespace.LOGIN_USER_INFO, ) LOGIN_SUCCESS_MUTATION = ( - CacheNamespace.SYSTEM_USER_LIST, - CacheNamespace.LOGIN_USER_INFO, - CacheNamespace.LOGIN_USER_ROUTERS, - CacheNamespace.SYSTEM_USER_PROFILE, - CacheNamespace.SYSTEM_USER_DETAIL, + ApiNamespace.SYSTEM_USER_LIST, + ApiNamespace.LOGIN_USER_INFO, + ApiNamespace.LOGIN_USER_ROUTERS, + ApiNamespace.SYSTEM_USER_PROFILE, + ApiNamespace.SYSTEM_USER_DETAIL, ) LOGOUT_MUTATION = ( - CacheNamespace.LOGIN_USER_INFO, - CacheNamespace.LOGIN_USER_ROUTERS, + ApiNamespace.LOGIN_USER_INFO, + ApiNamespace.LOGIN_USER_ROUTERS, ) CONFIG_MUTATION = ( - CacheNamespace.SYSTEM_CONFIG_LIST, - CacheNamespace.SYSTEM_CONFIG_DETAIL, + ApiNamespace.SYSTEM_CONFIG_LIST, + ApiNamespace.SYSTEM_CONFIG_DETAIL, ) DICT_TYPE_MUTATION = ( - CacheNamespace.SYSTEM_DICT_TYPE_LIST, - CacheNamespace.SYSTEM_DICT_TYPE_OPTIONS, - CacheNamespace.SYSTEM_DICT_TYPE_DETAIL, - CacheNamespace.SYSTEM_DICT_DATA_LIST, - CacheNamespace.SYSTEM_DICT_DATA_DETAIL, + ApiNamespace.SYSTEM_DICT_TYPE_LIST, + ApiNamespace.SYSTEM_DICT_TYPE_OPTIONS, + ApiNamespace.SYSTEM_DICT_TYPE_DETAIL, + ApiNamespace.SYSTEM_DICT_DATA_LIST, + ApiNamespace.SYSTEM_DICT_DATA_DETAIL, ) DICT_DATA_MUTATION = ( - CacheNamespace.SYSTEM_DICT_DATA_LIST, - CacheNamespace.SYSTEM_DICT_DATA_DETAIL, + ApiNamespace.SYSTEM_DICT_DATA_LIST, + ApiNamespace.SYSTEM_DICT_DATA_DETAIL, ) AI_MODEL_MUTATION = ( - CacheNamespace.AI_MODEL_LIST, - CacheNamespace.AI_MODEL_ALL, - CacheNamespace.AI_MODEL_DETAIL, + ApiNamespace.AI_MODEL_LIST, + ApiNamespace.AI_MODEL_ALL, + ApiNamespace.AI_MODEL_DETAIL, ) - AI_CHAT_CONFIG_MUTATION = (CacheNamespace.AI_CHAT_CONFIG,) + AI_CHAT_CONFIG_MUTATION = (ApiNamespace.AI_CHAT_CONFIG,) GEN_MUTATION = ( - CacheNamespace.TOOL_GEN_LIST, - CacheNamespace.TOOL_GEN_DB_LIST, - CacheNamespace.TOOL_GEN_DETAIL, - CacheNamespace.TOOL_GEN_PREVIEW, + ApiNamespace.TOOL_GEN_LIST, + ApiNamespace.TOOL_GEN_DB_LIST, + ApiNamespace.TOOL_GEN_DETAIL, + ApiNamespace.TOOL_GEN_PREVIEW, ) diff --git a/ruoyi-fastapi-backend/common/enums.py b/ruoyi-fastapi-backend/common/enums.py index 1e0868c..de86877 100644 --- a/ruoyi-fastapi-backend/common/enums.py +++ b/ruoyi-fastapi-backend/common/enums.py @@ -1,6 +1,32 @@ from enum import Enum +class HttpMethod(str, Enum): + """ + HTTP请求方法枚举 + + GET: 获取资源 + POST: 创建资源 + PUT: 整体更新资源 + DELETE: 删除资源 + PATCH: 局部更新资源 + HEAD: 获取响应头 + OPTIONS: 获取允许的方法信息 + TRACE: 回显诊断请求 + CONNECT: 建立隧道连接 + """ + + GET = 'GET' + POST = 'POST' + PUT = 'PUT' + DELETE = 'DELETE' + PATCH = 'PATCH' + HEAD = 'HEAD' + OPTIONS = 'OPTIONS' + TRACE = 'TRACE' + CONNECT = 'CONNECT' + + class BusinessType(Enum): """ 业务操作类型 @@ -46,6 +72,7 @@ def remark(self) -> str | None: SYS_DICT = {'key': 'sys_dict', 'remark': '数据字典'} SYS_CONFIG = {'key': 'sys_config', 'remark': '配置信息'} API_CACHE = {'key': 'api_cache', 'remark': '接口响应缓存'} + API_RATE_LIMIT = {'key': 'api_rate_limit', 'remark': '接口限流'} CAPTCHA_CODES = {'key': 'captcha_codes', 'remark': '图片验证码'} ACCOUNT_LOCK = {'key': 'account_lock', 'remark': '用户锁定'} PASSWORD_ERROR_COUNT = {'key': 'password_error_count', 'remark': '密码错误次数'} diff --git a/ruoyi-fastapi-backend/config/env.py b/ruoyi-fastapi-backend/config/env.py index 89baec5..7c89153 100644 --- a/ruoyi-fastapi-backend/config/env.py +++ b/ruoyi-fastapi-backend/config/env.py @@ -27,6 +27,8 @@ class AppSettings(BaseSettings): app_demo_mode: bool = False app_disable_swagger: bool = False app_disable_redoc: bool = False + app_trusted_proxy_ips: str = '127.0.0.1,::1' + app_trusted_proxy_hops: int = 1 class JwtSettings(BaseSettings): diff --git a/ruoyi-fastapi-backend/middlewares/api_response_header_middleware.py b/ruoyi-fastapi-backend/middlewares/api_response_header_middleware.py new file mode 100644 index 0000000..5393eaf --- /dev/null +++ b/ruoyi-fastapi-backend/middlewares/api_response_header_middleware.py @@ -0,0 +1,28 @@ +from fastapi import FastAPI, Request +from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint +from starlette.responses import Response + + +class ApiResponseHeaderMiddleware(BaseHTTPMiddleware): + """ + 接口响应头追加中间件 + """ + + async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) -> Response: + """ + 在响应返回前统一追加接口响应头 + """ + response = await call_next(request) + api_response_headers = getattr(request.state, 'api_response_headers', None) + if api_response_headers: + response.headers.update(api_response_headers) + return response + + +def add_api_response_header_middleware(app: FastAPI) -> None: + """ + 添加接口响应头追加中间件 + + :param app: FastAPI对象 + """ + app.add_middleware(ApiResponseHeaderMiddleware) diff --git a/ruoyi-fastapi-backend/middlewares/demo_mode_middleware.py b/ruoyi-fastapi-backend/middlewares/demo_mode_middleware.py index 9ee92d6..f2eb03f 100644 --- a/ruoyi-fastapi-backend/middlewares/demo_mode_middleware.py +++ b/ruoyi-fastapi-backend/middlewares/demo_mode_middleware.py @@ -2,6 +2,7 @@ from starlette.middleware.base import BaseHTTPMiddleware, RequestResponseEndpoint from starlette.responses import Response +from utils.client_ip_util import ClientIPUtil from utils.log_util import logger from utils.response_util import ResponseUtil @@ -44,7 +45,7 @@ async def dispatch(self, request: Request, call_next: RequestResponseEndpoint) - f'{request.base_url!s}tool/gen/createTable', ) ): - operate_ip = request.headers.get('X-Forwarded-For') + operate_ip = ClientIPUtil.get_client_ip(request) logger.warning( '请求IP:{}||请求API:{}||请求方法:{}||请求结果:演示模式,不允许操作!', operate_ip, url_path, method ) diff --git a/ruoyi-fastapi-backend/middlewares/handle.py b/ruoyi-fastapi-backend/middlewares/handle.py index 4b5f9e6..d9cebaf 100644 --- a/ruoyi-fastapi-backend/middlewares/handle.py +++ b/ruoyi-fastapi-backend/middlewares/handle.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from config.env import AppConfig +from middlewares.api_response_header_middleware import add_api_response_header_middleware from middlewares.context_middleware import add_context_cleanup_middleware from middlewares.cors_middleware import add_cors_middleware from middlewares.demo_mode_middleware import add_demo_mode_middleware @@ -18,6 +19,8 @@ def handle_middleware(app: FastAPI) -> None: add_cors_middleware(app) # 加载gzip压缩中间件 add_gzip_middleware(app) + # 加载接口响应头追加中间件 + add_api_response_header_middleware(app) # 加载trace中间件 add_trace_middleware(app) if AppConfig.app_demo_mode: diff --git a/ruoyi-fastapi-backend/module_admin/controller/cache_controller.py b/ruoyi-fastapi-backend/module_admin/controller/cache_controller.py index e1f04e3..8cb11d7 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/cache_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/cache_controller.py @@ -2,8 +2,10 @@ from fastapi import Path, Request, Response +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import PreAuthDependency +from common.constant import ApiNamespace from common.router import APIRouterPro from common.vo import DataResponseModel, ResponseBaseModel from module_admin.entity.vo.cache_vo import CacheInfoModel, CacheMonitorModel @@ -87,6 +89,7 @@ async def get_monitor_cache_value( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:cache:list')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_CACHE_CLEAR_NAME, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION) async def clear_monitor_cache_name( request: Request, cache_name: Annotated[str, Path(description='缓存名称')] ) -> Response: @@ -103,6 +106,7 @@ async def clear_monitor_cache_name( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:cache:list')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_CACHE_CLEAR_KEY, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION) async def clear_monitor_cache_key(request: Request, cache_key: Annotated[str, Path(description='缓存键')]) -> Response: clear_cache_key_result = await CacheService.clear_cache_monitor_cache_key_services(request, cache_key) logger.info(clear_cache_key_result.message) @@ -117,6 +121,7 @@ async def clear_monitor_cache_key(request: Request, cache_key: Annotated[str, Pa response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:cache:list')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_CACHE_CLEAR_ALL, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION) async def clear_monitor_cache_all(request: Request) -> Response: clear_cache_all_result = await CacheService.clear_cache_monitor_all_services(request) logger.info(clear_cache_all_result.message) diff --git a/ruoyi-fastapi-backend/module_admin/controller/captcha_controller.py b/ruoyi-fastapi-backend/module_admin/controller/captcha_controller.py index e7f6cb5..5e3f28a 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/captcha_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/captcha_controller.py @@ -3,6 +3,8 @@ from fastapi import Request, Response +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset +from common.constant import ApiNamespace from common.enums import RedisInitKeyConfig from common.router import APIRouterPro from common.vo import DynamicResponseModel @@ -20,6 +22,7 @@ description='用于获取图片验证码', response_model=DynamicResponseModel[CaptchaCode], ) +@ApiRateLimit(namespace=ApiNamespace.CAPTCHA_IMAGE, preset=ApiRateLimitPreset.ANON_AUTH_CAPTCHA) async def get_captcha_image(request: Request) -> Response: captcha_enabled = ( await request.app.state.redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.account.captchaEnabled') == 'true' diff --git a/ruoyi-fastapi-backend/module_admin/controller/common_controller.py b/ruoyi-fastapi-backend/module_admin/controller/common_controller.py index 9e48252..bfa4282 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/common_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/common_controller.py @@ -3,7 +3,9 @@ from fastapi import BackgroundTasks, File, Query, Request, Response, UploadFile from fastapi.responses import StreamingResponse +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset from common.aspect.pre_auth import PreAuthDependency +from common.constant import ApiNamespace from common.router import APIRouterPro from common.vo import DynamicResponseModel from module_admin.entity.vo.common_vo import UploadResponseModel @@ -20,6 +22,7 @@ description='用于上传文件', response_model=DynamicResponseModel[UploadResponseModel], ) +@ApiRateLimit(namespace=ApiNamespace.COMMON_UPLOAD, preset=ApiRateLimitPreset.COMMON_UPLOAD) async def common_upload(request: Request, file: Annotated[UploadFile, File(...)]) -> Response: upload_result = await CommonService.upload_service(request, file) logger.info('上传成功') diff --git a/ruoyi-fastapi-backend/module_admin/controller/config_controller.py b/ruoyi-fastapi-backend/module_admin/controller/config_controller.py index 7da4c80..bbe6cf2 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/config_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/config_controller.py @@ -8,10 +8,11 @@ from common.annotation.cache_annotation import ApiCache, ApiCacheEvict from common.annotation.log_annotation import Log +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel @@ -34,7 +35,7 @@ response_model=PageResponseModel[ConfigModel], dependencies=[UserInterfaceAuthDependency('system:config:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_CONFIG_LIST) +@ApiCache(namespace=ApiNamespace.SYSTEM_CONFIG_LIST) async def get_system_config_list( request: Request, config_page_query: Annotated[ConfigPageQueryModel, Query()], @@ -55,7 +56,7 @@ async def get_system_config_list( dependencies=[UserInterfaceAuthDependency('system:config:add')], ) @ValidateFields(validate_model='add_config') -@ApiCacheEvict(namespaces=CacheGroup.CONFIG_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.CONFIG_MUTATION) @Log(title='参数管理', business_type=BusinessType.INSERT) async def add_system_config( request: Request, @@ -81,7 +82,7 @@ async def add_system_config( dependencies=[UserInterfaceAuthDependency('system:config:edit')], ) @ValidateFields(validate_model='edit_config') -@ApiCacheEvict(namespaces=CacheGroup.CONFIG_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.CONFIG_MUTATION) @Log(title='参数管理', business_type=BusinessType.UPDATE) async def edit_system_config( request: Request, @@ -104,6 +105,7 @@ async def edit_system_config( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:config:remove')], ) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_CONFIG_REFRESH_CACHE, preset=ApiRateLimitPreset.USER_COMMON_MUTATION) @Log(title='参数管理', business_type=BusinessType.UPDATE) async def refresh_system_config( request: Request, @@ -122,7 +124,7 @@ async def refresh_system_config( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:config:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.CONFIG_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.CONFIG_MUTATION) @Log(title='参数管理', business_type=BusinessType.DELETE) async def delete_system_config( request: Request, @@ -143,7 +145,7 @@ async def delete_system_config( response_model=DataResponseModel[ConfigModel], dependencies=[UserInterfaceAuthDependency('system:config:query')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_CONFIG_DETAIL) +@ApiCache(namespace=ApiNamespace.SYSTEM_CONFIG_DETAIL) async def query_detail_system_config( request: Request, config_id: Annotated[int, Path(description='参数主键')], @@ -184,6 +186,7 @@ async def query_system_config(request: Request, config_key: str) -> Response: }, dependencies=[UserInterfaceAuthDependency('system:config:export')], ) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_CONFIG_EXPORT, preset=ApiRateLimitPreset.USER_RESOURCE_EXPORT) @Log(title='参数管理', business_type=BusinessType.EXPORT) async def export_system_config_list( request: Request, diff --git a/ruoyi-fastapi-backend/module_admin/controller/dept_controller.py b/ruoyi-fastapi-backend/module_admin/controller/dept_controller.py index 942e326..d40d946 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/dept_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/dept_controller.py @@ -12,7 +12,7 @@ from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, ResponseBaseModel @@ -35,7 +35,7 @@ response_model=DataResponseModel[list[DeptModel]], dependencies=[UserInterfaceAuthDependency('system:dept:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_DEPT_EDIT_TREE) +@ApiCache(namespace=ApiNamespace.SYSTEM_DEPT_EDIT_TREE) async def get_system_dept_tree_for_edit_option( request: Request, dept_id: Annotated[int, Path(description='部门id')], @@ -56,7 +56,7 @@ async def get_system_dept_tree_for_edit_option( response_model=DataResponseModel[list[DeptModel]], dependencies=[UserInterfaceAuthDependency('system:dept:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_DEPT_LIST) +@ApiCache(namespace=ApiNamespace.SYSTEM_DEPT_LIST) async def get_system_dept_list( request: Request, dept_query: Annotated[DeptQueryModel, Query()], @@ -77,7 +77,7 @@ async def get_system_dept_list( dependencies=[UserInterfaceAuthDependency('system:dept:add')], ) @ValidateFields(validate_model='add_dept') -@ApiCacheEvict(namespaces=CacheGroup.DATA_SCOPE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.DATA_SCOPE_MUTATION) @Log(title='部门管理', business_type=BusinessType.INSERT) async def add_system_dept( request: Request, @@ -103,7 +103,7 @@ async def add_system_dept( dependencies=[UserInterfaceAuthDependency('system:dept:edit')], ) @ValidateFields(validate_model='edit_dept') -@ApiCacheEvict(namespaces=CacheGroup.DATA_SCOPE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.DATA_SCOPE_MUTATION) @Log(title='部门管理', business_type=BusinessType.UPDATE) async def edit_system_dept( request: Request, @@ -129,7 +129,7 @@ async def edit_system_dept( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:dept:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.DATA_SCOPE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.DATA_SCOPE_MUTATION) @Log(title='部门管理', business_type=BusinessType.DELETE) async def delete_system_dept( request: Request, @@ -159,7 +159,7 @@ async def delete_system_dept( response_model=DataResponseModel[DeptModel], dependencies=[UserInterfaceAuthDependency('system:dept:query')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_DEPT_DETAIL) +@ApiCache(namespace=ApiNamespace.SYSTEM_DEPT_DETAIL) async def query_detail_system_dept( request: Request, dept_id: Annotated[int, Path(description='部门id')], diff --git a/ruoyi-fastapi-backend/module_admin/controller/dict_controller.py b/ruoyi-fastapi-backend/module_admin/controller/dict_controller.py index 2098902..cd72983 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/dict_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/dict_controller.py @@ -8,10 +8,11 @@ from common.annotation.cache_annotation import ApiCache, ApiCacheEvict from common.annotation.log_annotation import Log +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel @@ -41,7 +42,7 @@ response_model=PageResponseModel[DictTypeModel], dependencies=[UserInterfaceAuthDependency('system:dict:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_DICT_TYPE_LIST) +@ApiCache(namespace=ApiNamespace.SYSTEM_DICT_TYPE_LIST) async def get_system_dict_type_list( request: Request, dict_type_page_query: Annotated[DictTypePageQueryModel, Query()], @@ -64,7 +65,7 @@ async def get_system_dict_type_list( dependencies=[UserInterfaceAuthDependency('system:dict:add')], ) @ValidateFields(validate_model='add_dict_type') -@ApiCacheEvict(namespaces=CacheGroup.DICT_TYPE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.DICT_TYPE_MUTATION) @Log(title='字典类型', business_type=BusinessType.INSERT) async def add_system_dict_type( request: Request, @@ -90,7 +91,7 @@ async def add_system_dict_type( dependencies=[UserInterfaceAuthDependency('system:dict:edit')], ) @ValidateFields(validate_model='edit_dict_type') -@ApiCacheEvict(namespaces=CacheGroup.DICT_TYPE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.DICT_TYPE_MUTATION) @Log(title='字典类型', business_type=BusinessType.UPDATE) async def edit_system_dict_type( request: Request, @@ -113,6 +114,7 @@ async def edit_system_dict_type( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:dict:remove')], ) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_DICT_REFRESH_CACHE, preset=ApiRateLimitPreset.USER_COMMON_MUTATION) @Log(title='字典类型', business_type=BusinessType.UPDATE) async def refresh_system_dict(request: Request, query_db: Annotated[AsyncSession, DBSessionDependency()]) -> Response: refresh_dict_result = await DictTypeService.refresh_sys_dict_services(request, query_db) @@ -128,7 +130,7 @@ async def refresh_system_dict(request: Request, query_db: Annotated[AsyncSession response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:dict:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.DICT_TYPE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.DICT_TYPE_MUTATION) @Log(title='字典类型', business_type=BusinessType.DELETE) async def delete_system_dict_type( request: Request, @@ -148,7 +150,7 @@ async def delete_system_dict_type( description='用于获取字典类型下拉列表', response_model=DataResponseModel[list[DictTypeModel]], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_DICT_TYPE_OPTIONS) +@ApiCache(namespace=ApiNamespace.SYSTEM_DICT_TYPE_OPTIONS) async def query_system_dict_type_options( request: Request, query_db: Annotated[AsyncSession, DBSessionDependency()] ) -> Response: @@ -167,7 +169,7 @@ async def query_system_dict_type_options( response_model=DataResponseModel[DictTypeModel], dependencies=[UserInterfaceAuthDependency('system:dict:query')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_DICT_TYPE_DETAIL) +@ApiCache(namespace=ApiNamespace.SYSTEM_DICT_TYPE_DETAIL) async def query_detail_system_dict_type( request: Request, dict_id: Annotated[int, Path(description='字典主键')], @@ -194,6 +196,7 @@ async def query_detail_system_dict_type( }, dependencies=[UserInterfaceAuthDependency('system:dict:export')], ) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_DICT_TYPE_EXPORT, preset=ApiRateLimitPreset.USER_RESOURCE_EXPORT) @Log(title='字典类型', business_type=BusinessType.EXPORT) async def export_system_dict_type_list( request: Request, @@ -237,7 +240,7 @@ async def query_system_dict_type_data( response_model=PageResponseModel[DictDataModel], dependencies=[UserInterfaceAuthDependency('system:dict:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_DICT_DATA_LIST) +@ApiCache(namespace=ApiNamespace.SYSTEM_DICT_DATA_LIST) async def get_system_dict_data_list( request: Request, dict_data_page_query: Annotated[DictDataPageQueryModel, Query()], @@ -260,7 +263,7 @@ async def get_system_dict_data_list( dependencies=[UserInterfaceAuthDependency('system:dict:add')], ) @ValidateFields(validate_model='add_dict_data') -@ApiCacheEvict(namespaces=CacheGroup.DICT_DATA_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.DICT_DATA_MUTATION) @Log(title='字典数据', business_type=BusinessType.INSERT) async def add_system_dict_data( request: Request, @@ -286,7 +289,7 @@ async def add_system_dict_data( dependencies=[UserInterfaceAuthDependency('system:dict:edit')], ) @ValidateFields(validate_model='edit_dict_data') -@ApiCacheEvict(namespaces=CacheGroup.DICT_DATA_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.DICT_DATA_MUTATION) @Log(title='字典数据', business_type=BusinessType.UPDATE) async def edit_system_dict_data( request: Request, @@ -309,7 +312,7 @@ async def edit_system_dict_data( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:dict:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.DICT_DATA_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.DICT_DATA_MUTATION) @Log(title='字典数据', business_type=BusinessType.DELETE) async def delete_system_dict_data( request: Request, @@ -330,7 +333,7 @@ async def delete_system_dict_data( response_model=DataResponseModel[DictDataModel], dependencies=[UserInterfaceAuthDependency('system:dict:query')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_DICT_DATA_DETAIL) +@ApiCache(namespace=ApiNamespace.SYSTEM_DICT_DATA_DETAIL) async def query_detail_system_dict_data( request: Request, dict_code: Annotated[int, Path(description='字典编码')], @@ -357,6 +360,7 @@ async def query_detail_system_dict_data( }, dependencies=[UserInterfaceAuthDependency('system:dict:export')], ) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_DICT_DATA_EXPORT, preset=ApiRateLimitPreset.USER_RESOURCE_EXPORT) @Log(title='字典数据', business_type=BusinessType.EXPORT) async def export_system_dict_data_list( request: Request, diff --git a/ruoyi-fastapi-backend/module_admin/controller/job_controller.py b/ruoyi-fastapi-backend/module_admin/controller/job_controller.py index b7dc32d..38db601 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/job_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/job_controller.py @@ -8,10 +8,11 @@ from common.annotation.cache_annotation import ApiCache, ApiCacheEvict from common.annotation.log_annotation import Log +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel @@ -43,7 +44,7 @@ response_model=PageResponseModel[JobModel], dependencies=[UserInterfaceAuthDependency('monitor:job:list')], ) -@ApiCache(namespace=CacheNamespace.MONITOR_JOB_LIST) +@ApiCache(namespace=ApiNamespace.MONITOR_JOB_LIST) async def get_system_job_list( request: Request, job_page_query: Annotated[JobPageQueryModel, Query()], @@ -64,7 +65,7 @@ async def get_system_job_list( dependencies=[UserInterfaceAuthDependency('monitor:job:add')], ) @ValidateFields(validate_model='add_job') -@ApiCacheEvict(namespaces=CacheGroup.JOB_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.JOB_MUTATION) @Log(title='定时任务', business_type=BusinessType.INSERT) async def add_system_job( request: Request, @@ -90,7 +91,7 @@ async def add_system_job( dependencies=[UserInterfaceAuthDependency('monitor:job:edit')], ) @ValidateFields(validate_model='edit_job') -@ApiCacheEvict(namespaces=CacheGroup.JOB_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.JOB_MUTATION) @Log(title='定时任务', business_type=BusinessType.UPDATE) async def edit_system_job( request: Request, @@ -113,7 +114,7 @@ async def edit_system_job( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:job:changeStatus')], ) -@ApiCacheEvict(namespaces=CacheGroup.JOB_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.JOB_MUTATION) @Log(title='定时任务', business_type=BusinessType.UPDATE) async def change_system_job_status( request: Request, @@ -141,7 +142,8 @@ async def change_system_job_status( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:job:changeStatus')], ) -@ApiCacheEvict(namespaces=CacheGroup.JOB_MUTATION) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_JOB_RUN, preset=ApiRateLimitPreset.USER_RESOURCE_EXECUTION) +@ApiCacheEvict(namespaces=ApiGroup.JOB_MUTATION) @Log(title='定时任务', business_type=BusinessType.UPDATE) async def execute_system_job( request: Request, @@ -161,7 +163,8 @@ async def execute_system_job( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:job:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.JOB_MUTATION) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_JOB_DELETE, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.JOB_MUTATION) @Log(title='定时任务', business_type=BusinessType.DELETE) async def delete_system_job( request: Request, @@ -182,7 +185,7 @@ async def delete_system_job( response_model=DataResponseModel[JobModel], dependencies=[UserInterfaceAuthDependency('monitor:job:query')], ) -@ApiCache(namespace=CacheNamespace.MONITOR_JOB_DETAIL) +@ApiCache(namespace=ApiNamespace.MONITOR_JOB_DETAIL) async def query_detail_system_job( request: Request, job_id: Annotated[int, Path(description='任务ID')], @@ -209,6 +212,7 @@ async def query_detail_system_job( }, dependencies=[UserInterfaceAuthDependency('monitor:job:export')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_JOB_EXPORT, preset=ApiRateLimitPreset.USER_RESOURCE_EXPORT) @Log(title='定时任务', business_type=BusinessType.EXPORT) async def export_system_job_list( request: Request, @@ -251,6 +255,7 @@ async def get_system_job_log_list( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:job:remove')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_JOB_LOG_CLEAN, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION) @Log(title='定时任务调度日志', business_type=BusinessType.CLEAN) async def clear_system_job_log( request: Request, @@ -269,6 +274,7 @@ async def clear_system_job_log( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:job:remove')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_JOB_LOG_DELETE, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION) @Log(title='定时任务调度日志', business_type=BusinessType.DELETE) async def delete_system_job_log( request: Request, @@ -297,6 +303,7 @@ async def delete_system_job_log( }, dependencies=[UserInterfaceAuthDependency('monitor:job:export')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_JOB_LOG_EXPORT, preset=ApiRateLimitPreset.USER_RESOURCE_EXPORT) @Log(title='定时任务调度日志', business_type=BusinessType.EXPORT) async def export_system_job_log_list( request: Request, diff --git a/ruoyi-fastapi-backend/module_admin/controller/log_controller.py b/ruoyi-fastapi-backend/module_admin/controller/log_controller.py index fde3f9d..e84d0f8 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/log_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/log_controller.py @@ -5,9 +5,11 @@ from sqlalchemy.ext.asyncio import AsyncSession from common.annotation.log_annotation import Log +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import PreAuthDependency +from common.constant import ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import PageResponseModel, ResponseBaseModel @@ -58,6 +60,7 @@ async def get_system_operation_log_list( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:operlog:remove')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_OPERLOG_CLEAN, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION) @Log(title='操作日志', business_type=BusinessType.CLEAN) async def clear_system_operation_log( request: Request, query_db: Annotated[AsyncSession, DBSessionDependency()] @@ -75,6 +78,7 @@ async def clear_system_operation_log( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:operlog:remove')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_OPERLOG_DELETE, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION) @Log(title='操作日志', business_type=BusinessType.DELETE) async def delete_system_operation_log( request: Request, @@ -105,6 +109,7 @@ async def delete_system_operation_log( }, dependencies=[UserInterfaceAuthDependency('monitor:operlog:export')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_OPERLOG_EXPORT, preset=ApiRateLimitPreset.USER_RESOURCE_EXPORT) @Log(title='操作日志', business_type=BusinessType.EXPORT) async def export_system_operation_log_list( request: Request, @@ -151,6 +156,7 @@ async def get_system_login_log_list( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:logininfor:remove')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_LOGININFO_CLEAN, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION) @Log(title='登录日志', business_type=BusinessType.CLEAN) async def clear_system_login_log( request: Request, query_db: Annotated[AsyncSession, DBSessionDependency()] @@ -168,6 +174,7 @@ async def clear_system_login_log( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:logininfor:remove')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_LOGININFO_DELETE, preset=ApiRateLimitPreset.USER_DESTRUCTIVE_MUTATION) @Log(title='登录日志', business_type=BusinessType.DELETE) async def delete_system_login_log( request: Request, @@ -188,6 +195,7 @@ async def delete_system_login_log( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:logininfor:unlock')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_LOGININFO_UNLOCK, preset=ApiRateLimitPreset.USER_SECURITY_MUTATION) @Log(title='账户解锁', business_type=BusinessType.OTHER) async def unlock_system_user( request: Request, @@ -216,6 +224,7 @@ async def unlock_system_user( }, dependencies=[UserInterfaceAuthDependency('monitor:logininfor:export')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_LOGININFO_EXPORT, preset=ApiRateLimitPreset.USER_RESOURCE_EXPORT) @Log(title='登录日志', business_type=BusinessType.EXPORT) async def export_system_login_log_list( request: Request, diff --git a/ruoyi-fastapi-backend/module_admin/controller/login_controller.py b/ruoyi-fastapi-backend/module_admin/controller/login_controller.py index b7c6d17..4eadc4a 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/login_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/login_controller.py @@ -8,9 +8,10 @@ from common.annotation.cache_annotation import ApiCache, ApiCacheEvict from common.annotation.log_annotation import Log +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset from common.aspect.db_seesion import DBSessionDependency from common.aspect.pre_auth import CurrentUserDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType, RedisInitKeyConfig from common.router import APIRouterPro from common.vo import CrudResponseModel, DataResponseModel, DynamicResponseModel, ResponseBaseModel @@ -31,7 +32,8 @@ description='用于用户登录', response_model=DynamicResponseModel[LoginToken] | Token, ) -@ApiCacheEvict(namespaces=CacheGroup.LOGIN_SUCCESS_MUTATION) +@ApiRateLimit(namespace=ApiNamespace.LOGIN, preset=ApiRateLimitPreset.ANON_AUTH_LOGIN) +@ApiCacheEvict(namespaces=ApiGroup.LOGIN_SUCCESS_MUTATION) @Log(title='用户登录', business_type=BusinessType.OTHER, log_type='login') async def login( request: Request, @@ -93,7 +95,7 @@ async def login( description='用于获取当前登录用户的信息', response_model=DynamicResponseModel[CurrentUserModel], ) -@ApiCache(namespace=CacheNamespace.LOGIN_USER_INFO) +@ApiCache(namespace=ApiNamespace.LOGIN_USER_INFO) async def get_login_user_info( request: Request, current_user: Annotated[CurrentUserModel, CurrentUserDependency()] ) -> Response: @@ -108,7 +110,7 @@ async def get_login_user_info( description='用于获取当前登录用户的路由信息', response_model=DataResponseModel[list[RouterModel]], ) -@ApiCache(namespace=CacheNamespace.LOGIN_USER_ROUTERS) +@ApiCache(namespace=ApiNamespace.LOGIN_USER_ROUTERS) async def get_login_user_routers( request: Request, current_user: Annotated[CurrentUserModel, CurrentUserDependency()], @@ -126,7 +128,8 @@ async def get_login_user_routers( description='用于用户注册', response_model=DataResponseModel[CrudResponseModel], ) -@ApiCacheEvict(namespaces=CacheGroup.USER_ENTITY_MUTATION) +@ApiRateLimit(namespace=ApiNamespace.REGISTER, preset=ApiRateLimitPreset.ANON_AUTH_REGISTER) +@ApiCacheEvict(namespaces=ApiGroup.USER_ENTITY_MUTATION) async def register_user( request: Request, user_register: UserRegister, @@ -174,7 +177,7 @@ async def register_user( description='用于用户退出登录', response_model=ResponseBaseModel, ) -@ApiCacheEvict(namespaces=CacheGroup.LOGOUT_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.LOGOUT_MUTATION) async def logout(request: Request, token: Annotated[str | None, Depends(oauth2_scheme)]) -> Response: payload = jwt.decode( token, JwtConfig.jwt_secret_key, algorithms=[JwtConfig.jwt_algorithm], options={'verify_exp': False} diff --git a/ruoyi-fastapi-backend/module_admin/controller/menu_controller.py b/ruoyi-fastapi-backend/module_admin/controller/menu_controller.py index 80b9dd1..5747ee8 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/menu_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/menu_controller.py @@ -10,7 +10,7 @@ from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, DynamicResponseModel, ResponseBaseModel @@ -32,7 +32,7 @@ description='用于获取当前用户可见的菜单树', response_model=DataResponseModel[list[MenuTreeModel]], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_MENU_TREE) +@ApiCache(namespace=ApiNamespace.SYSTEM_MENU_TREE) async def get_system_menu_tree( request: Request, query_db: Annotated[AsyncSession, DBSessionDependency()], @@ -50,7 +50,7 @@ async def get_system_menu_tree( description='用于获取指定角色可见的菜单树', response_model=DynamicResponseModel[RoleMenuQueryModel], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_MENU_ROLE_TREE) +@ApiCache(namespace=ApiNamespace.SYSTEM_MENU_ROLE_TREE) async def get_system_role_menu_tree( request: Request, role_id: Annotated[int, Path(description='角色ID')], @@ -70,7 +70,7 @@ async def get_system_role_menu_tree( response_model=DataResponseModel[list[MenuModel]], dependencies=[UserInterfaceAuthDependency('system:menu:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_MENU_LIST) +@ApiCache(namespace=ApiNamespace.SYSTEM_MENU_LIST) async def get_system_menu_list( request: Request, menu_query: Annotated[MenuQueryModel, Query()], @@ -91,7 +91,7 @@ async def get_system_menu_list( dependencies=[UserInterfaceAuthDependency('system:menu:add')], ) @ValidateFields(validate_model='add_menu') -@ApiCacheEvict(namespaces=CacheGroup.MENU_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.MENU_MUTATION) @Log(title='菜单管理', business_type=BusinessType.INSERT) async def add_system_menu( request: Request, @@ -117,7 +117,7 @@ async def add_system_menu( dependencies=[UserInterfaceAuthDependency('system:menu:edit')], ) @ValidateFields(validate_model='edit_menu') -@ApiCacheEvict(namespaces=CacheGroup.MENU_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.MENU_MUTATION) @Log(title='菜单管理', business_type=BusinessType.UPDATE) async def edit_system_menu( request: Request, @@ -140,7 +140,7 @@ async def edit_system_menu( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:menu:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.MENU_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.MENU_MUTATION) @Log(title='菜单管理', business_type=BusinessType.DELETE) async def delete_system_menu( request: Request, @@ -161,7 +161,7 @@ async def delete_system_menu( response_model=DataResponseModel[MenuModel], dependencies=[UserInterfaceAuthDependency('system:menu:query')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_MENU_DETAIL) +@ApiCache(namespace=ApiNamespace.SYSTEM_MENU_DETAIL) async def query_detail_system_menu( request: Request, menu_id: Annotated[int, Path(description='菜单ID')], diff --git a/ruoyi-fastapi-backend/module_admin/controller/notice_controller.py b/ruoyi-fastapi-backend/module_admin/controller/notice_controller.py index 4a536a7..c78d787 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/notice_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/notice_controller.py @@ -10,7 +10,7 @@ from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel @@ -32,7 +32,7 @@ response_model=PageResponseModel[NoticeModel], dependencies=[UserInterfaceAuthDependency('system:notice:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_NOTICE_LIST) +@ApiCache(namespace=ApiNamespace.SYSTEM_NOTICE_LIST) async def get_system_notice_list( request: Request, notice_page_query: Annotated[NoticePageQueryModel, Query()], @@ -53,7 +53,7 @@ async def get_system_notice_list( dependencies=[UserInterfaceAuthDependency('system:notice:add')], ) @ValidateFields(validate_model='add_notice') -@ApiCacheEvict(namespaces=CacheGroup.NOTICE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.NOTICE_MUTATION) @Log(title='通知公告', business_type=BusinessType.INSERT) async def add_system_notice( request: Request, @@ -79,7 +79,7 @@ async def add_system_notice( dependencies=[UserInterfaceAuthDependency('system:notice:edit')], ) @ValidateFields(validate_model='edit_notice') -@ApiCacheEvict(namespaces=CacheGroup.NOTICE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.NOTICE_MUTATION) @Log(title='通知公告', business_type=BusinessType.UPDATE) async def edit_system_notice( request: Request, @@ -102,7 +102,7 @@ async def edit_system_notice( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:notice:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.NOTICE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.NOTICE_MUTATION) @Log(title='通知公告', business_type=BusinessType.DELETE) async def delete_system_notice( request: Request, @@ -123,7 +123,7 @@ async def delete_system_notice( response_model=DataResponseModel[NoticeModel], dependencies=[UserInterfaceAuthDependency('system:notice:query')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_NOTICE_DETAIL) +@ApiCache(namespace=ApiNamespace.SYSTEM_NOTICE_DETAIL) async def query_detail_system_post( request: Request, notice_id: Annotated[int, Path(description='公告ID')], diff --git a/ruoyi-fastapi-backend/module_admin/controller/online_controller.py b/ruoyi-fastapi-backend/module_admin/controller/online_controller.py index ac51647..ff152bb 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/online_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/online_controller.py @@ -4,9 +4,11 @@ from sqlalchemy.ext.asyncio import AsyncSession from common.annotation.log_annotation import Log +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import PreAuthDependency +from common.constant import ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import ResponseBaseModel @@ -47,6 +49,7 @@ async def get_monitor_online_list( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('monitor:online:forceLogout')], ) +@ApiRateLimit(namespace=ApiNamespace.MONITOR_ONLINE_FORCE_LOGOUT, preset=ApiRateLimitPreset.USER_SECURITY_MUTATION) @Log(title='在线用户', business_type=BusinessType.FORCE) async def delete_monitor_online( request: Request, diff --git a/ruoyi-fastapi-backend/module_admin/controller/post_controller.py b/ruoyi-fastapi-backend/module_admin/controller/post_controller.py index 391527c..ff84f96 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/post_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/post_controller.py @@ -8,10 +8,11 @@ from common.annotation.cache_annotation import ApiCache, ApiCacheEvict from common.annotation.log_annotation import Log +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel @@ -34,7 +35,7 @@ response_model=PageResponseModel[PostModel], dependencies=[UserInterfaceAuthDependency('system:post:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_POST_LIST) +@ApiCache(namespace=ApiNamespace.SYSTEM_POST_LIST) async def get_system_post_list( request: Request, post_page_query: Annotated[PostPageQueryModel, Query()], @@ -55,7 +56,7 @@ async def get_system_post_list( dependencies=[UserInterfaceAuthDependency('system:post:add')], ) @ValidateFields(validate_model='add_post') -@ApiCacheEvict(namespaces=CacheGroup.POST_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.POST_MUTATION) @Log(title='岗位管理', business_type=BusinessType.INSERT) async def add_system_post( request: Request, @@ -81,7 +82,7 @@ async def add_system_post( dependencies=[UserInterfaceAuthDependency('system:post:edit')], ) @ValidateFields(validate_model='edit_post') -@ApiCacheEvict(namespaces=CacheGroup.POST_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.POST_MUTATION) @Log(title='岗位管理', business_type=BusinessType.UPDATE) async def edit_system_post( request: Request, @@ -104,7 +105,7 @@ async def edit_system_post( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:post:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.POST_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.POST_MUTATION) @Log(title='岗位管理', business_type=BusinessType.DELETE) async def delete_system_post( request: Request, @@ -125,7 +126,7 @@ async def delete_system_post( response_model=DataResponseModel[PostModel], dependencies=[UserInterfaceAuthDependency('system:post:query')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_POST_DETAIL) +@ApiCache(namespace=ApiNamespace.SYSTEM_POST_DETAIL) async def query_detail_system_post( request: Request, post_id: Annotated[int, Path(description='岗位ID')], @@ -152,6 +153,7 @@ async def query_detail_system_post( }, dependencies=[UserInterfaceAuthDependency('system:post:export')], ) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_POST_EXPORT, preset=ApiRateLimitPreset.USER_RESOURCE_EXPORT) @Log(title='岗位管理', business_type=BusinessType.EXPORT) async def export_system_post_list( request: Request, diff --git a/ruoyi-fastapi-backend/module_admin/controller/role_controller.py b/ruoyi-fastapi-backend/module_admin/controller/role_controller.py index c908560..c86dc00 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/role_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/role_controller.py @@ -9,11 +9,12 @@ from common.annotation.cache_annotation import ApiCache, ApiCacheEvict from common.annotation.log_annotation import Log +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset from common.aspect.data_scope import DataScopeDependency from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, DynamicResponseModel, PageResponseModel, ResponseBaseModel @@ -47,7 +48,7 @@ response_model=DynamicResponseModel[RoleDeptQueryModel], dependencies=[UserInterfaceAuthDependency('system:role:query')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_ROLE_DEPT_TREE) +@ApiCache(namespace=ApiNamespace.SYSTEM_ROLE_DEPT_TREE) async def get_system_role_dept_tree( request: Request, role_id: Annotated[int, Path(description='角色ID')], @@ -69,7 +70,7 @@ async def get_system_role_dept_tree( response_model=PageResponseModel[RoleModel], dependencies=[UserInterfaceAuthDependency('system:role:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_ROLE_LIST) +@ApiCache(namespace=ApiNamespace.SYSTEM_ROLE_LIST) async def get_system_role_list( request: Request, role_page_query: Annotated[RolePageQueryModel, Query()], @@ -92,7 +93,7 @@ async def get_system_role_list( dependencies=[UserInterfaceAuthDependency('system:role:add')], ) @ValidateFields(validate_model='add_role') -@ApiCacheEvict(namespaces=CacheGroup.ROLE_ENTITY_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.ROLE_ENTITY_MUTATION) @Log(title='角色管理', business_type=BusinessType.INSERT) async def add_system_role( request: Request, @@ -118,7 +119,7 @@ async def add_system_role( dependencies=[UserInterfaceAuthDependency('system:role:edit')], ) @ValidateFields(validate_model='edit_role') -@ApiCacheEvict(namespaces=CacheGroup.ROLE_PERMISSION_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.ROLE_PERMISSION_MUTATION) @Log(title='角色管理', business_type=BusinessType.UPDATE) async def edit_system_role( request: Request, @@ -145,7 +146,7 @@ async def edit_system_role( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:role:edit')], ) -@ApiCacheEvict(namespaces=CacheGroup.DATA_SCOPE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.DATA_SCOPE_MUTATION) @Log(title='角色管理', business_type=BusinessType.GRANT) async def edit_system_role_datascope( request: Request, @@ -178,7 +179,7 @@ async def edit_system_role_datascope( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:role:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.ROLE_ENTITY_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.ROLE_ENTITY_MUTATION) @Log(title='角色管理', business_type=BusinessType.DELETE) async def delete_system_role( request: Request, @@ -207,7 +208,7 @@ async def delete_system_role( response_model=DataResponseModel[RoleModel], dependencies=[UserInterfaceAuthDependency('system:role:query')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_ROLE_DETAIL) +@ApiCache(namespace=ApiNamespace.SYSTEM_ROLE_DETAIL) async def query_detail_system_role( request: Request, role_id: Annotated[int, Path(description='角色ID')], @@ -238,6 +239,7 @@ async def query_detail_system_role( }, dependencies=[UserInterfaceAuthDependency('system:role:export')], ) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_ROLE_EXPORT, preset=ApiRateLimitPreset.USER_RESOURCE_EXPORT) @Log(title='角色管理', business_type=BusinessType.EXPORT) async def export_system_role_list( request: Request, @@ -262,7 +264,7 @@ async def export_system_role_list( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:role:edit')], ) -@ApiCacheEvict(namespaces=CacheGroup.ROLE_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.ROLE_MUTATION) @Log(title='角色管理', business_type=BusinessType.UPDATE) async def reset_system_role_status( request: Request, @@ -294,7 +296,7 @@ async def reset_system_role_status( response_model=PageResponseModel[UserInfoModel], dependencies=[UserInterfaceAuthDependency('system:role:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_ROLE_ALLOCATED_USER_LIST) +@ApiCache(namespace=ApiNamespace.SYSTEM_ROLE_ALLOCATED_USER_LIST) async def get_system_allocated_user_list( request: Request, user_role: Annotated[UserRolePageQueryModel, Query()], @@ -316,7 +318,7 @@ async def get_system_allocated_user_list( response_model=PageResponseModel[UserInfoModel], dependencies=[UserInterfaceAuthDependency('system:role:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_ROLE_UNALLOCATED_USER_LIST) +@ApiCache(namespace=ApiNamespace.SYSTEM_ROLE_UNALLOCATED_USER_LIST) async def get_system_unallocated_user_list( request: Request, user_role: Annotated[UserRolePageQueryModel, Query()], @@ -338,7 +340,8 @@ async def get_system_unallocated_user_list( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:role:edit')], ) -@ApiCacheEvict(namespaces=CacheGroup.ROLE_MUTATION) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_ROLE_AUTH_USER_SELECT_ALL, preset=ApiRateLimitPreset.USER_SECURITY_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.ROLE_MUTATION) @Log(title='角色管理', business_type=BusinessType.GRANT) async def add_system_role_user( request: Request, @@ -362,7 +365,8 @@ async def add_system_role_user( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:role:edit')], ) -@ApiCacheEvict(namespaces=CacheGroup.ROLE_MUTATION) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_ROLE_AUTH_USER_CANCEL, preset=ApiRateLimitPreset.USER_SECURITY_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.ROLE_MUTATION) @Log(title='角色管理', business_type=BusinessType.GRANT) async def cancel_system_role_user( request: Request, @@ -382,7 +386,8 @@ async def cancel_system_role_user( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:role:edit')], ) -@ApiCacheEvict(namespaces=CacheGroup.ROLE_MUTATION) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_ROLE_AUTH_USER_CANCEL_ALL, preset=ApiRateLimitPreset.USER_SECURITY_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.ROLE_MUTATION) @Log(title='角色管理', business_type=BusinessType.GRANT) async def batch_cancel_system_role_user( request: Request, diff --git a/ruoyi-fastapi-backend/module_admin/controller/server_controller.py b/ruoyi-fastapi-backend/module_admin/controller/server_controller.py index 8f24816..a68f218 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/server_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/server_controller.py @@ -3,7 +3,7 @@ from common.annotation.cache_annotation import ApiCache from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import PreAuthDependency -from common.constant import CacheNamespace +from common.constant import ApiNamespace from common.router import APIRouterPro from common.vo import DataResponseModel from module_admin.entity.vo.server_vo import ServerMonitorModel @@ -23,7 +23,7 @@ response_model=DataResponseModel[ServerMonitorModel], dependencies=[UserInterfaceAuthDependency('monitor:server:list')], ) -@ApiCache(namespace=CacheNamespace.MONITOR_SERVER_INFO) +@ApiCache(namespace=ApiNamespace.MONITOR_SERVER_INFO) async def get_monitor_server_info(request: Request) -> Response: # 获取全量数据 server_info_query_result = await ServerService.get_server_monitor_info() diff --git a/ruoyi-fastapi-backend/module_admin/controller/user_controller.py b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py index 91649af..7ea969c 100644 --- a/ruoyi-fastapi-backend/module_admin/controller/user_controller.py +++ b/ruoyi-fastapi-backend/module_admin/controller/user_controller.py @@ -11,11 +11,12 @@ from common.annotation.cache_annotation import ApiCache, ApiCacheEvict from common.annotation.log_annotation import Log +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitBypassConfig, ApiRateLimitPreset from common.aspect.data_scope import DataScopeDependency from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, DynamicResponseModel, PageResponseModel, ResponseBaseModel @@ -62,7 +63,7 @@ response_model=DataResponseModel[list[DeptTreeModel]], dependencies=[UserInterfaceAuthDependency('system:user:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_USER_DEPT_TREE) +@ApiCache(namespace=ApiNamespace.SYSTEM_USER_DEPT_TREE) async def get_system_dept_tree( request: Request, query_db: Annotated[AsyncSession, DBSessionDependency()], @@ -81,7 +82,7 @@ async def get_system_dept_tree( response_model=PageResponseModel[UserRowModel], dependencies=[UserInterfaceAuthDependency('system:user:list')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_USER_LIST) +@ApiCache(namespace=ApiNamespace.SYSTEM_USER_LIST) async def get_system_user_list( request: Request, user_page_query: Annotated[UserPageQueryModel, Query()], @@ -105,7 +106,7 @@ async def get_system_user_list( dependencies=[UserInterfaceAuthDependency('system:user:add')], ) @ValidateFields(validate_model='add_user') -@ApiCacheEvict(namespaces=CacheGroup.USER_ENTITY_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.USER_ENTITY_MUTATION) @Log(title='用户管理', business_type=BusinessType.INSERT) async def add_system_user( request: Request, @@ -139,7 +140,7 @@ async def add_system_user( dependencies=[UserInterfaceAuthDependency('system:user:edit')], ) @ValidateFields(validate_model='edit_user') -@ApiCacheEvict(namespaces=CacheGroup.USER_PERMISSION_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.USER_PERMISSION_MUTATION) @Log(title='用户管理', business_type=BusinessType.UPDATE) async def edit_system_user( request: Request, @@ -172,7 +173,7 @@ async def edit_system_user( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:user:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.USER_ENTITY_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.USER_ENTITY_MUTATION) @Log(title='用户管理', business_type=BusinessType.DELETE) async def delete_system_user( request: Request, @@ -205,7 +206,7 @@ async def delete_system_user( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:user:resetPwd')], ) -@ApiCacheEvict(namespaces=CacheGroup.USER_INFO_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.USER_INFO_MUTATION) @Log(title='用户管理', business_type=BusinessType.UPDATE) async def reset_system_user_pwd( request: Request, @@ -238,7 +239,7 @@ async def reset_system_user_pwd( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:user:edit')], ) -@ApiCacheEvict(namespaces=CacheGroup.USER_INFO_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.USER_INFO_MUTATION) @Log(title='用户管理', business_type=BusinessType.UPDATE) async def change_system_user_status( request: Request, @@ -269,7 +270,7 @@ async def change_system_user_status( description='用于获取当前登录用户的个人信息', response_model=DynamicResponseModel[UserProfileModel], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_USER_PROFILE) +@ApiCache(namespace=ApiNamespace.SYSTEM_USER_PROFILE) async def query_detail_system_user_profile( request: Request, query_db: Annotated[AsyncSession, DBSessionDependency()], @@ -295,7 +296,7 @@ async def query_detail_system_user_profile( response_model=DynamicResponseModel[UserDetailModel], dependencies=[UserInterfaceAuthDependency('system:user:query')], ) -@ApiCache(namespace=CacheNamespace.SYSTEM_USER_DETAIL) +@ApiCache(namespace=ApiNamespace.SYSTEM_USER_DETAIL) async def query_detail_system_user( request: Request, query_db: Annotated[AsyncSession, DBSessionDependency()], @@ -317,7 +318,8 @@ async def query_detail_system_user( description='用于修改当前登录用户的头像', response_model=DynamicResponseModel[AvatarModel], ) -@ApiCacheEvict(namespaces=CacheGroup.USER_INFO_MUTATION) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_USER_PROFILE_AVATAR, preset=ApiRateLimitPreset.USER_RESOURCE_UPLOAD) +@ApiCacheEvict(namespaces=ApiGroup.USER_INFO_MUTATION) @Log(title='个人信息', business_type=BusinessType.UPDATE) async def change_system_user_profile_avatar( request: Request, @@ -358,7 +360,7 @@ async def change_system_user_profile_avatar( description='用于修改当前登录用户的个人信息', response_model=ResponseBaseModel, ) -@ApiCacheEvict(namespaces=CacheGroup.USER_INFO_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.USER_INFO_MUTATION) @Log(title='个人信息', business_type=BusinessType.UPDATE) async def change_system_user_profile_info( request: Request, @@ -388,7 +390,7 @@ async def change_system_user_profile_info( description='用于修改当前登录用户的密码', response_model=ResponseBaseModel, ) -@ApiCacheEvict(namespaces=CacheGroup.USER_INFO_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.USER_INFO_MUTATION) @Log(title='个人信息', business_type=BusinessType.UPDATE) async def reset_system_user_password( request: Request, @@ -417,7 +419,12 @@ async def reset_system_user_password( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:user:import')], ) -@ApiCacheEvict(namespaces=CacheGroup.DATA_SCOPE_MUTATION) +@ApiRateLimit( + namespace=ApiNamespace.SYSTEM_USER_IMPORT, + preset=ApiRateLimitPreset.USER_RESOURCE_IMPORT, + bypass=ApiRateLimitBypassConfig(roles=('admin',)), +) +@ApiCacheEvict(namespaces=ApiGroup.DATA_SCOPE_MUTATION) @Log(title='用户管理', business_type=BusinessType.IMPORT) async def batch_import_system_user( request: Request, @@ -475,6 +482,7 @@ async def export_system_user_template( }, dependencies=[UserInterfaceAuthDependency('system:user:export')], ) +@ApiRateLimit(namespace=ApiNamespace.SYSTEM_USER_EXPORT, preset=ApiRateLimitPreset.USER_RESOURCE_EXPORT) @Log(title='用户管理', business_type=BusinessType.EXPORT) async def export_system_user_list( request: Request, @@ -520,7 +528,7 @@ async def get_system_allocated_role_list( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('system:user:edit')], ) -@ApiCacheEvict(namespaces=CacheGroup.USER_PERMISSION_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.USER_PERMISSION_MUTATION) @Log(title='用户管理', business_type=BusinessType.GRANT) async def update_system_role_user( request: Request, diff --git a/ruoyi-fastapi-backend/module_admin/service/login_service.py b/ruoyi-fastapi-backend/module_admin/service/login_service.py index e7bc1bf..a136d90 100644 --- a/ruoyi-fastapi-backend/module_admin/service/login_service.py +++ b/ruoyi-fastapi-backend/module_admin/service/login_service.py @@ -25,6 +25,7 @@ from module_admin.entity.vo.login_vo import MenuTreeModel, MetaModel, RouterModel, SmsCode, UserLogin, UserRegister from module_admin.entity.vo.user_vo import AddUserModel, CurrentUserModel, ResetUserModel, TokenData, UserInfoModel from module_admin.service.user_service import UserService +from utils.client_ip_util import ClientIPUtil from utils.common_util import CamelCaseUtil from utils.log_util import logger from utils.message_util import message_service @@ -147,7 +148,7 @@ async def __check_login_ip(cls, request: Request) -> bool: """ black_ip_value = await request.app.state.redis.get(f'{RedisInitKeyConfig.SYS_CONFIG.key}:sys.login.blackIPList') black_ip_list = black_ip_value.split(',') if black_ip_value else [] - if request.headers.get('X-Forwarded-For') in black_ip_list: + if ClientIPUtil.get_client_ip(request) in black_ip_list: logger.warning('当前IP禁止登录') raise LoginException(data='', message='当前IP禁止登录') return True diff --git a/ruoyi-fastapi-backend/module_ai/controller/ai_chat_controller.py b/ruoyi-fastapi-backend/module_ai/controller/ai_chat_controller.py index c23a0a4..4255c7b 100644 --- a/ruoyi-fastapi-backend/module_ai/controller/ai_chat_controller.py +++ b/ruoyi-fastapi-backend/module_ai/controller/ai_chat_controller.py @@ -6,9 +6,10 @@ from common.annotation.cache_annotation import ApiCache, ApiCacheEvict from common.annotation.log_annotation import Log +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitPreset from common.aspect.db_seesion import DBSessionDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, ResponseBaseModel @@ -42,6 +43,7 @@ } }, ) +@ApiRateLimit(namespace=ApiNamespace.AI_CHAT_SEND, preset=ApiRateLimitPreset.USER_INTERACTIVE_HIGH_FREQ) async def send_chat_message( request: Request, chat_req: AiChatRequestModel, @@ -61,7 +63,7 @@ async def send_chat_message( description='获取当前用户的AI对话配置', response_model=DataResponseModel[AiChatConfigModel], ) -@ApiCache(namespace=CacheNamespace.AI_CHAT_CONFIG) +@ApiCache(namespace=ApiNamespace.AI_CHAT_CONFIG) async def get_user_chat_config( request: Request, query_db: Annotated[AsyncSession, DBSessionDependency()], @@ -80,7 +82,7 @@ async def get_user_chat_config( description='保存当前用户的AI对话配置', response_model=DataResponseModel[AiChatConfigModel], ) -@ApiCacheEvict(namespaces=CacheGroup.AI_CHAT_CONFIG_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.AI_CHAT_CONFIG_MUTATION) @Log(title='AI对话配置管理', business_type=BusinessType.INSERT) async def save_user_chat_config( request: Request, @@ -151,6 +153,7 @@ async def get_chat_session_detail( description='取消正在进行的对话', response_model=ResponseBaseModel, ) +@ApiRateLimit(namespace=ApiNamespace.AI_CHAT_CANCEL, preset=ApiRateLimitPreset.USER_INTERACTIVE_HIGH_FREQ) async def cancel_chat_run( request: Request, run_id: Annotated[str, Body(embed=True, description='运行ID', alias='runId')], diff --git a/ruoyi-fastapi-backend/module_ai/controller/ai_model_controller.py b/ruoyi-fastapi-backend/module_ai/controller/ai_model_controller.py index efd89eb..839d09c 100644 --- a/ruoyi-fastapi-backend/module_ai/controller/ai_model_controller.py +++ b/ruoyi-fastapi-backend/module_ai/controller/ai_model_controller.py @@ -12,7 +12,7 @@ from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import UserInterfaceAuthDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel @@ -35,7 +35,7 @@ response_model=PageResponseModel[AiModelModel], dependencies=[UserInterfaceAuthDependency('ai:model:list')], ) -@ApiCache(namespace=CacheNamespace.AI_MODEL_LIST) +@ApiCache(namespace=ApiNamespace.AI_MODEL_LIST) async def get_ai_model_list( request: Request, ai_model_page_query: Annotated[AiModelPageQueryModel, Query()], @@ -57,7 +57,7 @@ async def get_ai_model_list( description='用于获取AI模型不分页列表', response_model=DataResponseModel[AiModelModel], ) -@ApiCache(namespace=CacheNamespace.AI_MODEL_ALL) +@ApiCache(namespace=ApiNamespace.AI_MODEL_ALL) async def get_ai_model_all( request: Request, query_db: Annotated[AsyncSession, DBSessionDependency()], @@ -81,7 +81,7 @@ async def get_ai_model_all( dependencies=[UserInterfaceAuthDependency('ai:model:add')], ) @ValidateFields(validate_model='add_ai_model') -@ApiCacheEvict(namespaces=CacheGroup.AI_MODEL_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.AI_MODEL_MUTATION) @Log(title='AI模型管理', business_type=BusinessType.INSERT) async def add_ai_model( request: Request, @@ -109,7 +109,7 @@ async def add_ai_model( dependencies=[UserInterfaceAuthDependency('ai:model:edit')], ) @ValidateFields(validate_model='edit_ai_model') -@ApiCacheEvict(namespaces=CacheGroup.AI_MODEL_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.AI_MODEL_MUTATION) @Log(title='AI模型管理', business_type=BusinessType.UPDATE) async def edit_ai_model( request: Request, @@ -135,7 +135,7 @@ async def edit_ai_model( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('ai:model:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.AI_MODEL_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.AI_MODEL_MUTATION) @Log(title='AI模型管理', business_type=BusinessType.DELETE) async def delete_ai_model( request: Request, @@ -162,7 +162,7 @@ async def delete_ai_model( response_model=DataResponseModel[AiModelModel], dependencies=[UserInterfaceAuthDependency('ai:model:query')], ) -@ApiCache(namespace=CacheNamespace.AI_MODEL_DETAIL) +@ApiCache(namespace=ApiNamespace.AI_MODEL_DETAIL) async def get_ai_model_detail( request: Request, model_id: Annotated[int, Path(description='模型ID')], diff --git a/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py b/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py index 63ee838..a62e82c 100644 --- a/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py +++ b/ruoyi-fastapi-backend/module_generator/controller/gen_controller.py @@ -8,10 +8,11 @@ from common.annotation.cache_annotation import ApiCache, ApiCacheEvict from common.annotation.log_annotation import Log +from common.annotation.rate_limit_annotation import ApiRateLimit, ApiRateLimitBypassConfig, ApiRateLimitPreset from common.aspect.db_seesion import DBSessionDependency from common.aspect.interface_auth import RoleInterfaceAuthDependency, UserInterfaceAuthDependency from common.aspect.pre_auth import CurrentUserDependency, PreAuthDependency -from common.constant import CacheGroup, CacheNamespace +from common.constant import ApiGroup, ApiNamespace from common.enums import BusinessType from common.router import APIRouterPro from common.vo import DataResponseModel, PageResponseModel, ResponseBaseModel @@ -40,7 +41,7 @@ response_model=PageResponseModel[GenTableRowModel], dependencies=[UserInterfaceAuthDependency('tool:gen:list')], ) -@ApiCache(namespace=CacheNamespace.TOOL_GEN_LIST) +@ApiCache(namespace=ApiNamespace.TOOL_GEN_LIST) async def get_gen_table_list( request: Request, gen_page_query: Annotated[GenTablePageQueryModel, Query()], @@ -60,7 +61,7 @@ async def get_gen_table_list( response_model=PageResponseModel[GenTableDbRowModel], dependencies=[UserInterfaceAuthDependency('tool:gen:list')], ) -@ApiCache(namespace=CacheNamespace.TOOL_GEN_DB_LIST) +@ApiCache(namespace=ApiNamespace.TOOL_GEN_DB_LIST) async def get_gen_db_table_list( request: Request, gen_page_query: Annotated[GenTablePageQueryModel, Query()], @@ -80,7 +81,12 @@ async def get_gen_db_table_list( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('tool:gen:import')], ) -@ApiCacheEvict(namespaces=CacheGroup.GEN_MUTATION) +@ApiRateLimit( + namespace=ApiNamespace.TOOL_GEN_IMPORT_TABLE, + preset=ApiRateLimitPreset.USER_RESOURCE_GENERATE, + bypass=ApiRateLimitBypassConfig(roles=('admin',)), +) +@ApiCacheEvict(namespaces=ApiGroup.GEN_MUTATION) @Log(title='代码生成', business_type=BusinessType.IMPORT) async def import_gen_table( request: Request, @@ -104,7 +110,7 @@ async def import_gen_table( dependencies=[UserInterfaceAuthDependency('tool:gen:edit')], ) @ValidateFields(validate_model='edit_gen_table') -@ApiCacheEvict(namespaces=CacheGroup.GEN_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.GEN_MUTATION) @Log(title='代码生成', business_type=BusinessType.UPDATE) async def edit_gen_table( request: Request, @@ -128,7 +134,7 @@ async def edit_gen_table( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('tool:gen:remove')], ) -@ApiCacheEvict(namespaces=CacheGroup.GEN_MUTATION) +@ApiCacheEvict(namespaces=ApiGroup.GEN_MUTATION) @Log(title='代码生成', business_type=BusinessType.DELETE) async def delete_gen_table( request: Request, @@ -149,7 +155,12 @@ async def delete_gen_table( response_model=ResponseBaseModel, dependencies=[RoleInterfaceAuthDependency('admin')], ) -@ApiCacheEvict(namespaces=CacheGroup.GEN_MUTATION) +@ApiRateLimit( + namespace=ApiNamespace.TOOL_GEN_CREATE_TABLE, + preset=ApiRateLimitPreset.USER_RESOURCE_GENERATE, + bypass=ApiRateLimitBypassConfig(roles=('admin',)), +) +@ApiCacheEvict(namespaces=ApiGroup.GEN_MUTATION) @Log(title='创建表', business_type=BusinessType.OTHER) async def create_table( request: Request, @@ -178,6 +189,11 @@ async def create_table( }, dependencies=[UserInterfaceAuthDependency('tool:gen:code')], ) +@ApiRateLimit( + namespace=ApiNamespace.TOOL_GEN_BATCH_GEN_CODE, + preset=ApiRateLimitPreset.USER_RESOURCE_DOWNLOAD, + bypass=ApiRateLimitBypassConfig(roles=('admin',)), +) @Log(title='代码生成', business_type=BusinessType.GENCODE) async def batch_gen_code( request: Request, @@ -198,7 +214,12 @@ async def batch_gen_code( response_model=ResponseBaseModel, dependencies=[UserInterfaceAuthDependency('tool:gen:code')], ) -@ApiCacheEvict(namespaces=CacheGroup.GEN_MUTATION) +@ApiRateLimit( + namespace=ApiNamespace.TOOL_GEN_GEN_CODE_LOCAL, + preset=ApiRateLimitPreset.USER_RESOURCE_GENERATE, + bypass=ApiRateLimitBypassConfig(roles=('admin',)), +) +@ApiCacheEvict(namespaces=ApiGroup.GEN_MUTATION) @Log(title='代码生成', business_type=BusinessType.GENCODE) async def gen_code_local( request: Request, @@ -221,7 +242,7 @@ async def gen_code_local( response_model=DataResponseModel[GenTableDetailModel], dependencies=[UserInterfaceAuthDependency('tool:gen:query')], ) -@ApiCache(namespace=CacheNamespace.TOOL_GEN_DETAIL) +@ApiCache(namespace=ApiNamespace.TOOL_GEN_DETAIL) async def query_detail_gen_table( request: Request, table_id: Annotated[int, Path(description='表编号')], @@ -243,7 +264,7 @@ async def query_detail_gen_table( response_model=DataResponseModel[dict[str, str]], dependencies=[UserInterfaceAuthDependency('tool:gen:preview')], ) -@ApiCache(namespace=CacheNamespace.TOOL_GEN_PREVIEW) +@ApiCache(namespace=ApiNamespace.TOOL_GEN_PREVIEW) async def preview_code( request: Request, table_id: Annotated[int, Path(description='表编号')], @@ -262,7 +283,8 @@ async def preview_code( response_model=DataResponseModel[str], dependencies=[UserInterfaceAuthDependency('tool:gen:edit')], ) -@ApiCacheEvict(namespaces=CacheGroup.GEN_MUTATION) +@ApiRateLimit(namespace=ApiNamespace.TOOL_GEN_SYNC_DB, preset=ApiRateLimitPreset.USER_RESOURCE_SYNC) +@ApiCacheEvict(namespaces=ApiGroup.GEN_MUTATION) @Log(title='代码生成', business_type=BusinessType.UPDATE) async def sync_db( request: Request, diff --git a/ruoyi-fastapi-backend/utils/api_annotation_util.py b/ruoyi-fastapi-backend/utils/api_annotation_util.py new file mode 100644 index 0000000..09c1454 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/api_annotation_util.py @@ -0,0 +1,102 @@ +import inspect +from collections.abc import Awaitable, Callable, Sequence +from typing import TypeVar + +from fastapi import Request +from redis import asyncio as aioredis +from typing_extensions import ParamSpec + +from common.enums import HttpMethod +from utils.log_util import logger + +P = ParamSpec('P') +R = TypeVar('R') + + +class ApiAnnotationUtil: + """ + 接口装饰器通用工具类 + """ + + @classmethod + def get_request(cls, func: Callable[P, Awaitable[R]], *args: P.args, **kwargs: P.kwargs) -> Request | None: + """ + 从被装饰函数的入参中提取Request对象 + + :param func: 被装饰的异步接口函数 + :param args: 位置参数 + :param kwargs: 关键字参数 + :return: Request对象,未找到时返回None + """ + signature = inspect.signature(func) + bound_arguments = signature.bind_partial(*args, **kwargs) + for argument in bound_arguments.arguments.values(): + if isinstance(argument, Request): + return argument + + return None + + @classmethod + def get_redis_client(cls, request: Request, skip_message: str) -> aioredis.Redis | None: + """ + 从应用状态中获取Redis连接 + + :param request: 当前请求对象 + :param skip_message: 未初始化Redis连接时的日志信息 + :return: Redis连接对象,未初始化时返回None + """ + redis = getattr(request.app.state, 'redis', None) + if redis is None: + logger.warning(skip_message) + + return redis + + @classmethod + def resolve_request_redis( + cls, + func: Callable[P, Awaitable[R]], + skip_message: str, + *args: P.args, + **kwargs: P.kwargs, + ) -> tuple[Request | None, aioredis.Redis | None]: + """ + 从被装饰函数入参中同时解析Request与Redis连接 + + :param func: 被装饰的异步接口函数 + :param skip_message: 未初始化Redis连接时的日志信息 + :param args: 位置参数 + :param kwargs: 关键字参数 + :return: Request对象与Redis连接对象组成的元组 + """ + request = cls.get_request(func, *args, **kwargs) + if request is None: + return None, None + + return request, cls.get_redis_client(request, skip_message) + + @classmethod + def normalize_http_methods( + cls, + methods: Sequence[HttpMethod] | None, + default_methods: Sequence[HttpMethod] | None = None, + ) -> tuple[str, ...]: + """ + 标准化HTTP请求方法配置 + + :param methods: 显式配置的HTTP请求方法 + :param default_methods: methods为空时使用的默认HTTP请求方法 + :return: 去重且标准化后的HTTP请求方法元组 + """ + target_methods = methods if methods is not None else default_methods + if not target_methods: + return () + + normalized_methods: list[str] = [] + for method in target_methods: + if not isinstance(method, HttpMethod): + raise TypeError('methods参数仅支持HttpMethod枚举') + normalized_method = method.value + if normalized_method not in normalized_methods: + normalized_methods.append(normalized_method) + + return tuple(normalized_methods) diff --git a/ruoyi-fastapi-backend/utils/api_response_header_util.py b/ruoyi-fastapi-backend/utils/api_response_header_util.py new file mode 100644 index 0000000..40824e3 --- /dev/null +++ b/ruoyi-fastapi-backend/utils/api_response_header_util.py @@ -0,0 +1,25 @@ +from collections.abc import Mapping + +from fastapi import Request + + +class ApiResponseHeaderUtil: + """ + 接口响应头通用工具类 + """ + + @classmethod + def merge_headers(cls, request: Request, headers: Mapping[str, str] | None) -> None: + """ + 将响应头暂存到请求上下文中,供出站中间件统一追加 + + :param request: 当前请求对象 + :param headers: 需要追加的响应头 + :return: None + """ + if not headers: + return + + api_response_headers = dict(getattr(request.state, 'api_response_headers', {})) + api_response_headers.update(headers) + request.state.api_response_headers = api_response_headers diff --git a/ruoyi-fastapi-backend/utils/client_ip_util.py b/ruoyi-fastapi-backend/utils/client_ip_util.py new file mode 100644 index 0000000..34b7fdc --- /dev/null +++ b/ruoyi-fastapi-backend/utils/client_ip_util.py @@ -0,0 +1,60 @@ +from fastapi import Request + +from config.env import AppConfig + + +class ClientIPUtil: + """ + 客户端IP提取工具 + """ + + @classmethod + def get_client_ip(cls, request: Request) -> str: + """ + 获取客户端真实IP + + 仅当请求来源命中可信代理列表,且可信代理跳数大于0时,才会解析 + X-Forwarded-For / X-Real-IP 请求头;否则回退到直接连接来源地址。 + + :param request: 当前请求对象 + :return: 客户端IP + """ + remote_addr = request.client.host if request.client else 'unknown' + if AppConfig.app_trusted_proxy_hops <= 0: + return remote_addr + if not cls._should_trust_proxy_headers(remote_addr): + return remote_addr + + forwarded_for = request.headers.get('X-Forwarded-For', '') + if forwarded_for: + forwarded_chain = [item.strip() for item in forwarded_for.split(',') if item.strip()] + if forwarded_chain: + if len(forwarded_chain) > AppConfig.app_trusted_proxy_hops: + return forwarded_chain[-(AppConfig.app_trusted_proxy_hops + 1)] + return forwarded_chain[0] + + real_ip = request.headers.get('X-Real-IP', '').strip() + if real_ip: + return real_ip + + return remote_addr + + @classmethod + def _should_trust_proxy_headers(cls, remote_addr: str) -> bool: + """ + 判断当前连接来源是否属于可信代理 + + :param remote_addr: 与应用直接建立连接的来源IP + :return: 是否信任代理头 + """ + trusted_proxy_ips = cls._get_trusted_proxy_ips() + return '*' in trusted_proxy_ips or remote_addr in trusted_proxy_ips + + @classmethod + def _get_trusted_proxy_ips(cls) -> set[str]: + """ + 获取可信代理IP集合 + + :return: 可信代理IP集合 + """ + return {item.strip() for item in AppConfig.app_trusted_proxy_ips.split(',') if item.strip()} diff --git a/ruoyi-fastapi-backend/utils/response_util.py b/ruoyi-fastapi-backend/utils/response_util.py index 13f2687..d281501 100644 --- a/ruoyi-fastapi-backend/utils/response_util.py +++ b/ruoyi-fastapi-backend/utils/response_util.py @@ -246,6 +246,52 @@ def error( background=background, ) + @classmethod + def too_many_requests( + cls, + msg: str = '请求过于频繁,请稍后再试', + data: Any | None = None, + rows: Any | None = None, + dict_content: dict | None = None, + model_content: BaseModel | None = None, + headers: Mapping[str, str] | None = None, + media_type: str | None = None, + background: BackgroundTask | None = None, + ) -> Response: + """ + 接口限流响应方法 + + :param msg: 可选,自定义限流响应信息 + :param data: 可选,限流响应结果中属性为data的值 + :param rows: 可选,限流响应结果中属性为rows的值 + :param dict_content: 可选,dict类型,限流响应结果中自定义属性的值 + :param model_content: 可选,BaseModel类型,限流响应结果中自定义属性的值 + :param headers: 可选,响应头信息 + :param media_type: 可选,响应结果媒体类型 + :param background: 可选,响应返回后执行的后台任务 + :return: 限流响应结果 + """ + result = {'code': HttpStatusConstant.TOO_MANY_REQUESTS, 'msg': msg} + + if data is not None: + result['data'] = data + if rows is not None: + result['rows'] = rows + if dict_content is not None: + result.update(dict_content) + if model_content is not None: + result.update(model_content.model_dump(by_alias=True)) + + result.update({'success': False, 'time': datetime.now()}) + + return JSONResponse( + status_code=status.HTTP_429_TOO_MANY_REQUESTS, + content=jsonable_encoder(result), + headers=headers, + media_type=media_type, + background=background, + ) + @classmethod def streaming( cls, diff --git a/ruoyi-fastapi-frontend/src/utils/request.js b/ruoyi-fastapi-frontend/src/utils/request.js index dec166f..a9ec88f 100644 --- a/ruoyi-fastapi-frontend/src/utils/request.js +++ b/ruoyi-fastapi-frontend/src/utils/request.js @@ -110,6 +110,15 @@ service.interceptors.response.use(res => { }, error => { console.log('err' + error) + const response = error.response + const responseStatus = response?.status + const responseCode = response?.data?.code + const responseMsg = response?.data?.msg + if (responseMsg) { + const messageType = responseStatus === 429 || responseCode === 429 ? 'warning' : 'error' + Message({ message: responseMsg, type: messageType, duration: 5 * 1000 }) + return Promise.reject(new Error(responseMsg)) + } let { message } = error; if (message == "Network Error") { message = "后端接口连接异常"; From 17a65366b4ebc2b6a07067aac820bed1031319a6 Mon Sep 17 00:00:00 2001 From: insistence <3055204202@qq.com> Date: Thu, 9 Apr 2026 14:31:40 +0800 Subject: [PATCH 2/2] =?UTF-8?q?perf:=20=E4=BC=98=E5=8C=96=E6=8E=A5?= =?UTF-8?q?=E5=8F=A3=E7=BC=93=E5=AD=98=E8=A3=85=E9=A5=B0=E5=99=A8=E5=92=8C?= =?UTF-8?q?=E6=8E=A5=E5=8F=A3=E7=BC=93=E5=AD=98=E5=A4=B1=E6=95=88=E8=A3=85?= =?UTF-8?q?=E9=A5=B0=E5=99=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../common/annotation/cache_annotation.py | 83 +++---------------- 1 file changed, 11 insertions(+), 72 deletions(-) diff --git a/ruoyi-fastapi-backend/common/annotation/cache_annotation.py b/ruoyi-fastapi-backend/common/annotation/cache_annotation.py index b7851b5..aabeefa 100644 --- a/ruoyi-fastapi-backend/common/annotation/cache_annotation.py +++ b/ruoyi-fastapi-backend/common/annotation/cache_annotation.py @@ -1,5 +1,4 @@ import hashlib -import inspect import json from collections.abc import Awaitable, Callable, Sequence from datetime import datetime @@ -14,8 +13,10 @@ from common.constant import HttpStatusConstant from common.context import RequestContext -from common.enums import RedisInitKeyConfig +from common.enums import HttpMethod, RedisInitKeyConfig from exceptions.exception import LoginException +from utils.api_annotation_util import ApiAnnotationUtil +from utils.api_response_header_util import ApiResponseHeaderUtil from utils.log_util import logger P = ParamSpec('P') @@ -134,59 +135,6 @@ class _ApiCacheSupport: 接口缓存装饰器共用工具基类 """ - def _get_request(self, func: Callable[P, Awaitable[R]], *args: P.args, **kwargs: P.kwargs) -> Request | None: - """ - 从被装饰函数的入参中提取Request对象 - - :param func: 被装饰的异步接口函数 - :param args: 位置参数 - :param kwargs: 关键字参数 - :return: Request对象,未找到时返回None - """ - signature = inspect.signature(func) - bound_arguments = signature.bind_partial(*args, **kwargs) - for argument in bound_arguments.arguments.values(): - if isinstance(argument, Request): - return argument - - return None - - def _get_redis_client(self, request: Request, skip_message: str) -> aioredis.Redis | None: - """ - 从应用状态中获取Redis连接 - - :param request: 当前请求对象 - :param skip_message: 未初始化Redis连接时的日志信息 - :return: Redis连接对象,未初始化时返回None - """ - redis = getattr(request.app.state, 'redis', None) - if redis is None: - logger.warning(skip_message) - - return redis - - def _resolve_request_redis( - self, - func: Callable[P, Awaitable[R]], - skip_message: str, - *args: P.args, - **kwargs: P.kwargs, - ) -> tuple[Request | None, aioredis.Redis | None]: - """ - 从被装饰函数入参中同时解析Request与Redis连接 - - :param func: 被装饰的异步接口函数 - :param skip_message: 未初始化Redis连接时的日志信息 - :param args: 位置参数 - :param kwargs: 关键字参数 - :return: Request对象与Redis连接对象组成的元组 - """ - request = self._get_request(func, *args, **kwargs) - if request is None: - return None, None - - return request, self._get_redis_client(request, skip_message) - def _load_json_content(self, response_body: str) -> Any | None: """ 解析JSON响应体内容 @@ -259,7 +207,7 @@ def __init__( namespace: str, expire_seconds: int = 10, vary_by_user: bool = True, - methods: Sequence[str] | None = None, + methods: Sequence[HttpMethod] | None = None, cache_response_codes: set[int] | None = None, ) -> None: """ @@ -268,13 +216,13 @@ def __init__( :param namespace: 缓存命名空间,用于区分不同接口类型 :param expire_seconds: 缓存过期时间,单位秒 :param vary_by_user: 是否按当前登录用户隔离缓存 - :param methods: 允许启用缓存的HTTP方法列表,为None时默认仅缓存GET请求 + :param methods: 允许启用缓存的HttpMethod枚举列表,为None时默认仅缓存GET请求 :param cache_response_codes: 允许缓存的业务响应码,为None时默认仅缓存成功响应 """ self.namespace = namespace self.expire_seconds = expire_seconds self.vary_by_user = vary_by_user - self.methods = tuple(dict.fromkeys(method.upper() for method in methods)) if methods else ('GET',) + self.methods = ApiAnnotationUtil.normalize_http_methods(methods, default_methods=(HttpMethod.GET,)) self.cache_response_codes = ( cache_response_codes if cache_response_codes is not None else {HttpStatusConstant.SUCCESS} ) @@ -289,7 +237,7 @@ def __call__(self, func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]] @wraps(func) async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: - request, redis = self._resolve_request_redis( + request, redis = ApiAnnotationUtil.resolve_request_redis( func, '当前应用未初始化Redis连接,跳过接口缓存', *args, **kwargs ) if request is None or redis is None: @@ -304,7 +252,7 @@ async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: result = await func(*args, **kwargs) await self._cache_response(redis, cache_key, result) - self._append_cache_header(result, cache_status='MISS') + ApiResponseHeaderUtil.merge_headers(request, {'X-Api-Cache': 'MISS'}) return result @@ -452,17 +400,6 @@ async def _cache_response(self, redis: aioredis.Redis, cache_key: str, result: A await redis.set(cache_key, serialized_response, ex=self.expire_seconds) logger.debug(f'接口缓存写入成功: {cache_key}') - def _append_cache_header(self, result: Any, cache_status: str) -> None: - """ - 为响应对象追加接口缓存命中状态响应头 - - :param result: 原始接口返回结果 - :param cache_status: 缓存状态标识 - :return: None - """ - if isinstance(result, Response): - result.headers['X-Api-Cache'] = cache_status - def _filter_response_headers(self, headers: dict[str, str]) -> dict[str, str]: """ 过滤不适合直接回放的响应头 @@ -529,7 +466,9 @@ def __call__(self, func: Callable[P, Awaitable[R]]) -> Callable[P, Awaitable[R]] @wraps(func) async def wrapper(*args: P.args, **kwargs: P.kwargs) -> R: result = await func(*args, **kwargs) - _, redis = self._resolve_request_redis(func, '当前应用未初始化Redis连接,跳过接口缓存失效', *args, **kwargs) + _, redis = ApiAnnotationUtil.resolve_request_redis( + func, '当前应用未初始化Redis连接,跳过接口缓存失效', *args, **kwargs + ) if redis is None: return result