diff --git a/.env.example b/.env.example index 5afd779..78dcf78 100644 --- a/.env.example +++ b/.env.example @@ -28,6 +28,7 @@ DEBUG=false CORS_URLS='[]' SERVER_URL="http://localhost:8000" FRONTEND_URL= +ENABLE_RATE_LIMIT=true # SignalR Settings SIGNALR_NEGOTIATE_TIMEOUT=30 diff --git a/app/config.py b/app/config.py index 51a6810..2d7db07 100644 --- a/app/config.py +++ b/app/config.py @@ -93,6 +93,7 @@ class Settings(BaseSettings): cors_urls: list[HttpUrl] = [] server_url: HttpUrl = HttpUrl("http://localhost:8000") frontend_url: HttpUrl | None = None + enable_rate_limit: bool = True @property def web_url(self): diff --git a/main.py b/main.py index 7d701f2..d5b5fb0 100644 --- a/main.py +++ b/main.py @@ -4,7 +4,7 @@ from contextlib import asynccontextmanager from pathlib import Path from app.config import settings -from app.dependencies.database import engine, redis_client +from app.dependencies.database import engine, get_redis, redis_client from app.dependencies.fetcher import get_fetcher from app.dependencies.scheduler import start_scheduler, stop_scheduler from app.log import logger @@ -37,16 +37,19 @@ from app.service.osu_rx_statistics import create_rx_statistics from app.service.redis_message_system import redis_message_system from app.utils import bg_tasks, utcnow -from fastapi import FastAPI, HTTPException, Request +from fastapi import Depends, FastAPI, HTTPException, Request from fastapi.exceptions import RequestValidationError from fastapi.middleware.cors import CORSMiddleware from fastapi.responses import JSONResponse +from fastapi_limiter import FastAPILimiter +from fastapi_limiter.depends import RateLimiter import sentry_sdk @asynccontextmanager async def lifespan(app: FastAPI): # on startup + await FastAPILimiter.init(get_redis()) await get_fetcher() # 初始化 fetcher await init_geoip() # 初始化 GeoIP 数据库 await create_rx_statistics() @@ -80,7 +83,7 @@ async def lifespan(app: FastAPI): await redis_client.aclose() -desc = """osu! API 模拟服务器,支持 osu! API v1, v2 和 osu!lazer 的绝大部分功能。 +desc = f"""osu! API 模拟服务器,支持 osu! API v1, v2 和 osu!lazer 的绝大部分功能。 ## 端点说明 @@ -100,6 +103,19 @@ v2 API 采用 OAuth 2.0 鉴权,支持以下鉴权方式: v1 API 采用 API Key 鉴权,将 API Key 放入 Query `k` 中。 +{ + ''' +## 速率限制 + +所有 API 请求均受到速率限制,具体限制规则如下: + +- 每分钟最多可以发送 1200 个请求 +- 突发请求限制为每秒最多 200 个请求 +''' + if settings.enable_rate_limit + else "" +} + ## 参考 - v2 API 文档:[osu-web 文档](https://osu.ppy.sh/docs/index.html) @@ -136,6 +152,14 @@ app = FastAPI( lifespan=lifespan, description=desc, ) +if settings.enable_rate_limit: + app.router.dependencies.extend( + [ + Depends(RateLimiter(times=1200, minutes=1)), + Depends(RateLimiter(times=200, seconds=1)), + ] + ) + app.include_router(api_v2_router) app.include_router(api_v1_router) diff --git a/pyproject.toml b/pyproject.toml index 1f45d3c..eda1458 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -13,6 +13,7 @@ dependencies = [ "bcrypt>=4.1.2", "cryptography>=41.0.7", "fastapi>=0.104.1", + "fastapi-limiter>=0.1.6", "httpx>=0.28.1", "loguru>=0.7.3", "maxminddb>=2.8.2", diff --git a/uv.lock b/uv.lock index 1cfb608..bf12d60 100644 --- a/uv.lock +++ b/uv.lock @@ -459,6 +459,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e5/47/d63c60f59a59467fda0f93f46335c9d18526d7071f025cb5b89d5353ea42/fastapi-0.116.1-py3-none-any.whl", hash = "sha256:c46ac7c312df840f0c9e220f7964bada936781bc4e2e6eb71f1c4d7553786565", size = 95631, upload-time = "2025-07-11T16:22:30.485Z" }, ] +[[package]] +name = "fastapi-limiter" +version = "0.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "fastapi" }, + { name = "redis" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7f/99/c7903234488d4dca5f9bccb4f88c2f582a234f0dca33348781c9cf8a48c6/fastapi_limiter-0.1.6.tar.gz", hash = "sha256:6f5fde8efebe12eb33861bdffb91009f699369a3c2862cdc7c1d9acf912ff443", size = 8307, upload-time = "2024-01-05T09:14:48.628Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cd/b5/6f6b4d18bee1cafc857eae12738b3a03b7d1102b833668be868938c57b9d/fastapi_limiter-0.1.6-py3-none-any.whl", hash = "sha256:2e53179a4208b8f2c8795e38bb001324d3dc37d2800ff49fd28ec5caabf7a240", size = 15829, upload-time = "2024-01-05T09:14:47.613Z" }, +] + [[package]] name = "filelock" version = "3.19.1" @@ -541,6 +554,7 @@ dependencies = [ { name = "bcrypt" }, { name = "cryptography" }, { name = "fastapi" }, + { name = "fastapi-limiter" }, { name = "httpx" }, { name = "loguru" }, { name = "maxminddb" }, @@ -581,6 +595,7 @@ requires-dist = [ { name = "bcrypt", specifier = ">=4.1.2" }, { name = "cryptography", specifier = ">=41.0.7" }, { name = "fastapi", specifier = ">=0.104.1" }, + { name = "fastapi-limiter", specifier = ">=0.1.6" }, { name = "httpx", specifier = ">=0.28.1" }, { name = "loguru", specifier = ">=0.7.3" }, { name = "maxminddb", specifier = ">=2.8.2" },