Files
fastapi_best_architecture/backend/utils/request_parse.py
Wu Clan 966779bcd7 Refactor the interface rate limiter implementation (#1072)
* Refactor the interface rate limiter implementation

* Update deps
2026-02-11 17:27:46 +08:00

124 lines
3.7 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import httpx
from fastapi import Request
from ip2loc import XdbSearcher
from user_agents import parse
from backend.common.dataclasses import IpInfo, UserAgentInfo
from backend.common.log import log
from backend.core.conf import settings
from backend.core.path_conf import STATIC_DIR
from backend.database.redis import redis_client
def get_request_ip(request: Request) -> str:
"""
获取请求的 IP 地址
:param request: FastAPI 请求对象
:return:
"""
real = request.headers.get('X-Real-IP')
if real:
return real
forwarded = request.headers.get('X-Forwarded-For')
if forwarded:
return forwarded.split(',')[0]
# 忽略 pytest
if request.client.host == 'testclient':
return '127.0.0.1'
return request.client.host
async def get_location_online(ip: str) -> dict | None:
"""
在线获取 IP 地址属地,无法保证可用性,准确率较高
:param ip: IP 地址
:return:
"""
async with httpx.AsyncClient(timeout=3) as client:
try:
response = await client.get(f'http://ip-api.com/json/{ip}?lang=zh-CN')
if response.status_code == 200:
return response.json()
except Exception as e:
log.error(f'在线获取 IP 地址属地失败,错误信息:{e}')
return None
# 离线 IP 搜索器单例(数据将缓存到内存,缓存大小取决于 IP 数据文件大小)
__xdb_searcher = XdbSearcher(contentBuff=XdbSearcher.loadContentFromFile(dbfile=STATIC_DIR / 'ip2region_v4.xdb'))
def get_location_offline(ip: str) -> dict | None:
"""
离线获取 IP 地址属地无法保证准确率100% 可用
:param ip: IP 地址
:return:
"""
try:
data = __xdb_searcher.search(ip)
data = data.split('|')
return {
'country': data[0] if data[0] != '0' else None,
'regionName': data[1] if data[1] != '0' else None,
'city': data[2] if data[2] != '0' else None,
}
except Exception as e:
log.error(f'离线获取 IP 地址属地失败,错误信息:{e}')
return None
async def parse_ip_info(request: Request) -> IpInfo:
"""
解析请求的 IP 信息
:param request: FastAPI 请求对象
:return:
"""
country, region, city = None, None, None
ip = get_request_ip(request)
location = await redis_client.get(f'{settings.IP_LOCATION_REDIS_PREFIX}:{ip}')
if location:
country, region, city = location.split('|')
return IpInfo(ip=ip, country=country, region=region, city=city)
location_info = None
if settings.IP_LOCATION_PARSE == 'online':
location_info = await get_location_online(ip)
elif settings.IP_LOCATION_PARSE == 'offline':
location_info = get_location_offline(ip)
if location_info:
country = location_info.get('country')
region = location_info.get('regionName')
city = location_info.get('city')
await redis_client.set(
f'{settings.IP_LOCATION_REDIS_PREFIX}:{ip}',
f'{country}|{region}|{city}',
ex=settings.IP_LOCATION_EXPIRE_SECONDS,
)
return IpInfo(ip=ip, country=country, region=region, city=city)
def parse_user_agent_info(request: Request) -> UserAgentInfo:
"""
解析请求的用户代理信息
:param request: FastAPI 请求对象
:return:
"""
os, browser, device = None, None, None
user_agent = request.headers.get('User-Agent')
if user_agent:
user_agent_ = parse(user_agent)
os = user_agent_.get_os()
browser = user_agent_.get_browser()
device = user_agent_.get_device()
return UserAgentInfo(user_agent=user_agent, device=device, os=os, browser=browser)