feat(api): add rate limiting for API requests
This commit is contained in:
@@ -28,6 +28,7 @@ DEBUG=false
|
|||||||
CORS_URLS='[]'
|
CORS_URLS='[]'
|
||||||
SERVER_URL="http://localhost:8000"
|
SERVER_URL="http://localhost:8000"
|
||||||
FRONTEND_URL=
|
FRONTEND_URL=
|
||||||
|
ENABLE_RATE_LIMIT=true
|
||||||
|
|
||||||
# SignalR Settings
|
# SignalR Settings
|
||||||
SIGNALR_NEGOTIATE_TIMEOUT=30
|
SIGNALR_NEGOTIATE_TIMEOUT=30
|
||||||
|
|||||||
@@ -93,6 +93,7 @@ class Settings(BaseSettings):
|
|||||||
cors_urls: list[HttpUrl] = []
|
cors_urls: list[HttpUrl] = []
|
||||||
server_url: HttpUrl = HttpUrl("http://localhost:8000")
|
server_url: HttpUrl = HttpUrl("http://localhost:8000")
|
||||||
frontend_url: HttpUrl | None = None
|
frontend_url: HttpUrl | None = None
|
||||||
|
enable_rate_limit: bool = True
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def web_url(self):
|
def web_url(self):
|
||||||
|
|||||||
30
main.py
30
main.py
@@ -4,7 +4,7 @@ from contextlib import asynccontextmanager
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from app.config import settings
|
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.fetcher import get_fetcher
|
||||||
from app.dependencies.scheduler import start_scheduler, stop_scheduler
|
from app.dependencies.scheduler import start_scheduler, stop_scheduler
|
||||||
from app.log import logger
|
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.service.redis_message_system import redis_message_system
|
||||||
from app.utils import bg_tasks, utcnow
|
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.exceptions import RequestValidationError
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
from fastapi.middleware.cors import CORSMiddleware
|
||||||
from fastapi.responses import JSONResponse
|
from fastapi.responses import JSONResponse
|
||||||
|
from fastapi_limiter import FastAPILimiter
|
||||||
|
from fastapi_limiter.depends import RateLimiter
|
||||||
import sentry_sdk
|
import sentry_sdk
|
||||||
|
|
||||||
|
|
||||||
@asynccontextmanager
|
@asynccontextmanager
|
||||||
async def lifespan(app: FastAPI):
|
async def lifespan(app: FastAPI):
|
||||||
# on startup
|
# on startup
|
||||||
|
await FastAPILimiter.init(get_redis())
|
||||||
await get_fetcher() # 初始化 fetcher
|
await get_fetcher() # 初始化 fetcher
|
||||||
await init_geoip() # 初始化 GeoIP 数据库
|
await init_geoip() # 初始化 GeoIP 数据库
|
||||||
await create_rx_statistics()
|
await create_rx_statistics()
|
||||||
@@ -80,7 +83,7 @@ async def lifespan(app: FastAPI):
|
|||||||
await redis_client.aclose()
|
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` 中。
|
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)
|
- v2 API 文档:[osu-web 文档](https://osu.ppy.sh/docs/index.html)
|
||||||
@@ -136,6 +152,14 @@ app = FastAPI(
|
|||||||
lifespan=lifespan,
|
lifespan=lifespan,
|
||||||
description=desc,
|
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_v2_router)
|
||||||
app.include_router(api_v1_router)
|
app.include_router(api_v1_router)
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ dependencies = [
|
|||||||
"bcrypt>=4.1.2",
|
"bcrypt>=4.1.2",
|
||||||
"cryptography>=41.0.7",
|
"cryptography>=41.0.7",
|
||||||
"fastapi>=0.104.1",
|
"fastapi>=0.104.1",
|
||||||
|
"fastapi-limiter>=0.1.6",
|
||||||
"httpx>=0.28.1",
|
"httpx>=0.28.1",
|
||||||
"loguru>=0.7.3",
|
"loguru>=0.7.3",
|
||||||
"maxminddb>=2.8.2",
|
"maxminddb>=2.8.2",
|
||||||
|
|||||||
15
uv.lock
generated
15
uv.lock
generated
@@ -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" },
|
{ 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]]
|
[[package]]
|
||||||
name = "filelock"
|
name = "filelock"
|
||||||
version = "3.19.1"
|
version = "3.19.1"
|
||||||
@@ -541,6 +554,7 @@ dependencies = [
|
|||||||
{ name = "bcrypt" },
|
{ name = "bcrypt" },
|
||||||
{ name = "cryptography" },
|
{ name = "cryptography" },
|
||||||
{ name = "fastapi" },
|
{ name = "fastapi" },
|
||||||
|
{ name = "fastapi-limiter" },
|
||||||
{ name = "httpx" },
|
{ name = "httpx" },
|
||||||
{ name = "loguru" },
|
{ name = "loguru" },
|
||||||
{ name = "maxminddb" },
|
{ name = "maxminddb" },
|
||||||
@@ -581,6 +595,7 @@ requires-dist = [
|
|||||||
{ name = "bcrypt", specifier = ">=4.1.2" },
|
{ name = "bcrypt", specifier = ">=4.1.2" },
|
||||||
{ name = "cryptography", specifier = ">=41.0.7" },
|
{ name = "cryptography", specifier = ">=41.0.7" },
|
||||||
{ name = "fastapi", specifier = ">=0.104.1" },
|
{ name = "fastapi", specifier = ">=0.104.1" },
|
||||||
|
{ name = "fastapi-limiter", specifier = ">=0.1.6" },
|
||||||
{ name = "httpx", specifier = ">=0.28.1" },
|
{ name = "httpx", specifier = ">=0.28.1" },
|
||||||
{ name = "loguru", specifier = ">=0.7.3" },
|
{ name = "loguru", specifier = ">=0.7.3" },
|
||||||
{ name = "maxminddb", specifier = ">=2.8.2" },
|
{ name = "maxminddb", specifier = ">=2.8.2" },
|
||||||
|
|||||||
Reference in New Issue
Block a user