优化更新
This commit is contained in:
@@ -136,9 +136,6 @@ CAPTCHA_NOISE_DOTS=30
|
||||
# CORS允许的源 - 逗号分隔
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:8001
|
||||
|
||||
# 可信代理层数 - 0=不信任X-Forwarded-For,>=1 表示在反向代理后
|
||||
TRUSTED_PROXY_COUNT=0
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 邮件配置
|
||||
|
||||
@@ -129,10 +129,6 @@ CAPTCHA_NOISE_DOTS=30
|
||||
|
||||
# CORS允许的源 - 逗号分隔,如 http://localhost:8001,https://admin.example.com
|
||||
CORS_ALLOWED_ORIGINS=http://localhost:8001
|
||||
# 可信代理层数 - 0=不信任X-Forwarded-For;仅部署在Nginx等反代后才设>=1
|
||||
TRUSTED_PROXY_COUNT=0
|
||||
# 可信代理IP白名单(逗号分隔)
|
||||
TRUSTED_PROXY_IPS=
|
||||
|
||||
# ============================================================
|
||||
# 邮件配置
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
# DjangoAdmin Nginx 反向代理配置
|
||||
# 部署时修改 server_name、root、proxy_pass 为实际值
|
||||
|
||||
upstream fastapi_backend {
|
||||
server 127.0.0.1:8031;
|
||||
keepalive 16;
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name your-domain.com;
|
||||
|
||||
charset utf-8;
|
||||
client_max_body_size 50m;
|
||||
|
||||
# 安全头
|
||||
add_header X-Frame-Options "SAMEORIGIN" always;
|
||||
add_header X-Content-Type-Options "nosniff" always;
|
||||
|
||||
# Gzip
|
||||
gzip on;
|
||||
gzip_min_length 1k;
|
||||
gzip_comp_level 6;
|
||||
gzip_types text/plain text/css application/json application/javascript text/xml image/svg+xml;
|
||||
gzip_vary on;
|
||||
|
||||
# 前端静态资源
|
||||
root /data/apps/djangoadmin/frontend/dist;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ /index.html;
|
||||
}
|
||||
|
||||
location /assets/ {
|
||||
expires 30d;
|
||||
add_header Cache-Control "public, immutable";
|
||||
access_log off;
|
||||
}
|
||||
|
||||
# API 反向代理
|
||||
location /api/ {
|
||||
proxy_pass http://fastapi_backend/api/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Host $host;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
# 透传上游X-Forwarded-For(不追加本机IP);无上游头时回退到直连IP
|
||||
proxy_set_header X-Forwarded-For $http_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
proxy_set_header Connection "";
|
||||
proxy_connect_timeout 60s;
|
||||
proxy_read_timeout 120s;
|
||||
proxy_send_timeout 60s;
|
||||
}
|
||||
|
||||
# 上传文件访问
|
||||
location /api/file/ {
|
||||
alias /data/apps/djangoadmin/uploads/;
|
||||
expires 7d;
|
||||
access_log off;
|
||||
limit_except GET { deny all; }
|
||||
}
|
||||
|
||||
# 禁止访问隐藏文件
|
||||
location ~ /\. { deny all; }
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
; Supervisor 进程管理示例配置
|
||||
; 将 command / directory 替换为实际路径后复制到 /etc/supervisor/conf.d/
|
||||
|
||||
[program:djangoadmin-fastapi]
|
||||
; 启动命令:使用项目虚拟环境中的 uvicorn
|
||||
command=/path/to/venv/bin/uvicorn src.main:app --host 127.0.0.1 --port 8031
|
||||
; 项目根目录
|
||||
directory=/path/to/DjangoAdmin_FastAPI_EleVue
|
||||
autostart=true
|
||||
autorestart=true
|
||||
stopasgroup=true
|
||||
killasgroup=true
|
||||
redirect_stderr=true
|
||||
stdout_logfile=/path/to/logs/supervisor.out.log
|
||||
stdout_logfile_maxbytes=50MB
|
||||
stdout_logfile_backups=10
|
||||
environment=PYTHONUNBUFFERED=1
|
||||
@@ -76,7 +76,7 @@ from core.config.app import (
|
||||
FASTAPI_APP, FASTAPI_ENV, FASTAPI_DEBUG, FASTAPI_DEMO,
|
||||
FASTAPI_ROOT_PATH, FASTAPI_TEMPLATE_FOLDER, FASTAPI_STATIC_FOLDER,
|
||||
FASTAPI_UPLOAD_DIR, FASTAPI_TEMP_PATH, FASTAPI_FILE_URL,
|
||||
TRUSTED_PROXY_COUNT, TRUSTED_PROXY_IPS, UPLOAD_ALLOWED_EXTS, UPLOAD_MAX_SIZE_MB,
|
||||
UPLOAD_ALLOWED_EXTS, UPLOAD_MAX_SIZE_MB,
|
||||
)
|
||||
|
||||
# 数据库配置
|
||||
@@ -133,7 +133,7 @@ __all__ = [
|
||||
'FASTAPI_APP', 'FASTAPI_ENV', 'FASTAPI_DEBUG', 'FASTAPI_DEMO',
|
||||
'FASTAPI_ROOT_PATH', 'FASTAPI_TEMPLATE_FOLDER', 'FASTAPI_STATIC_FOLDER',
|
||||
'FASTAPI_UPLOAD_DIR', 'FASTAPI_TEMP_PATH', 'FASTAPI_FILE_URL',
|
||||
'TRUSTED_PROXY_COUNT', 'TRUSTED_PROXY_IPS', 'UPLOAD_ALLOWED_EXTS', 'UPLOAD_MAX_SIZE_MB',
|
||||
'UPLOAD_ALLOWED_EXTS', 'UPLOAD_MAX_SIZE_MB',
|
||||
|
||||
# 数据库配置
|
||||
'DB_DRIVER', 'DB_HOST', 'DB_PORT', 'DB_DATABASE', 'DB_USERNAME', 'DB_PASSWORD',
|
||||
|
||||
+1
-11
@@ -57,7 +57,7 @@ _logger = logging.getLogger(__name__)
|
||||
# 应用基本信息
|
||||
# ============================================================
|
||||
# 应用名称
|
||||
FASTAPI_NAME = os.getenv('FASTAPI_NAME', 'FastAPI+AntdVue旗舰版')
|
||||
FASTAPI_NAME = os.getenv('FASTAPI_NAME', 'FastAPI+EleVue旗舰版')
|
||||
# 应用版本
|
||||
FASTAPI_VERSION = os.getenv('FASTAPI_VERSION', 'v3.0.0')
|
||||
# 应用运行地址
|
||||
@@ -96,16 +96,6 @@ FASTAPI_FILE_URL = os.getenv('FASTAPI_FILE_URL', '')
|
||||
# CORS允许的源(逗号分隔),未配置则允许所有源但不携带凭证
|
||||
CORS_ALLOWED_ORIGINS = os.getenv('CORS_ALLOWED_ORIGINS', '')
|
||||
|
||||
# 可信代理层数(0=不信任X-Forwarded-For,1=一层反向代理,2=两层…)
|
||||
# 仅当应用部署在Nginx等反向代理后才应设置为>=1
|
||||
TRUSTED_PROXY_COUNT = int(os.getenv('TRUSTED_PROXY_COUNT', '0'))
|
||||
# 可信代理IP白名单(逗号分隔,可含CIDR前的字面IP)。仅当直连对端命中此白名单且
|
||||
# TRUSTED_PROXY_COUNT>0 时才信任 X-Forwarded-For;否则 fail-closed 回退 request.client.host。
|
||||
# 防止应用被直接暴露(如 Docker bridge 映射端口)时客户端伪造 XFF 绕过限流/登录锁定
|
||||
TRUSTED_PROXY_IPS = frozenset(
|
||||
ip.strip() for ip in os.getenv('TRUSTED_PROXY_IPS', '').split(',') if ip.strip()
|
||||
)
|
||||
|
||||
# ============================================================
|
||||
# 上传配置
|
||||
# ============================================================
|
||||
|
||||
+6
-36
@@ -54,9 +54,6 @@ IP地址工具模块
|
||||
(IP地理位置查询已迁移至 utils/ip2region 离线库)
|
||||
"""
|
||||
|
||||
from core.config import TRUSTED_PROXY_COUNT, TRUSTED_PROXY_IPS
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 获取客户端IP
|
||||
# ============================================================
|
||||
@@ -64,11 +61,8 @@ def get_client_ip(request) -> str:
|
||||
"""
|
||||
获取客户端真实IP地址
|
||||
|
||||
仅当直连对端命中 TRUSTED_PROXY_IPS 白名单且 TRUSTED_PROXY_COUNT>0 时才信任
|
||||
X-Forwarded-For 头:
|
||||
- TRUSTED_PROXY_COUNT==0 或 TRUSTED_PROXY_IPS 未配置 / 对端不命中:不信任代理头,
|
||||
直接使用 request.client.host(fail-closed,防止被直接暴露时伪造 XFF)
|
||||
- 命中白名单:从 X-Forwarded-For 右侧取倒数第 N 个 IP 作为真实客户端IP
|
||||
优先从 X-Forwarded-For 头取最左侧IP(即真实客户端IP),
|
||||
无代理头时回退到 request.client.host。
|
||||
|
||||
Args:
|
||||
request: FastAPI Request 对象
|
||||
@@ -76,34 +70,10 @@ def get_client_ip(request) -> str:
|
||||
Returns:
|
||||
客户端IP地址
|
||||
"""
|
||||
if TRUSTED_PROXY_COUNT > 0 and _peer_is_trusted_proxy(request):
|
||||
forwarded = request.headers.get('X-Forwarded-For', '')
|
||||
if forwarded:
|
||||
ips = [ip.strip() for ip in forwarded.split(',')]
|
||||
# XFF 最右侧由受信代理追加,客户端可控的伪造条目只可能出现在左侧;
|
||||
# 跳过最右侧 TRUSTED_PROXY_COUNT 个代理条目取真实客户端IP
|
||||
index = len(ips) - TRUSTED_PROXY_COUNT
|
||||
# 代理条目少于预期(如部分代理未追加 XFF)时回退到最右侧,
|
||||
# 避免退回 request.client.host 拿到代理IP、导致反代部署下所有请求共享同一IP
|
||||
if index < 0:
|
||||
index = len(ips) - 1
|
||||
return ips[index]
|
||||
forwarded = request.headers.get('X-Forwarded-For', '')
|
||||
if forwarded:
|
||||
# 取最左侧IP(由CDN/反代追加的真实客户端IP,右侧为代理节点IP)
|
||||
return forwarded.split(',')[0].strip()
|
||||
|
||||
client = request.client
|
||||
return client.host if client else '127.0.0.1'
|
||||
|
||||
|
||||
# ============================================================
|
||||
# 私有辅助函数
|
||||
# ============================================================
|
||||
def _peer_is_trusted_proxy(request) -> bool:
|
||||
"""直连对端(TCP 层)必须是配置的可信代理IP,才允许信任 X-Forwarded-For
|
||||
|
||||
若 TRUSTED_PROXY_IPS 为空或对端不命中 → 不信任代理头。否则应用被直接暴露
|
||||
(如 Docker bridge 映射端口、未走 Nginx)时,客户端可随意伪造 XFF 首部,
|
||||
绕过基于 IP 的登录锁定/请求限流。
|
||||
"""
|
||||
if not TRUSTED_PROXY_IPS:
|
||||
return False
|
||||
client = request.client
|
||||
return client is not None and client.host in TRUSTED_PROXY_IPS
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
"""utils/ip.py 客户端IP获取测试。"""
|
||||
import types
|
||||
|
||||
from utils.ip import get_client_ip
|
||||
|
||||
|
||||
def _make_request(client_host, xff=None):
|
||||
"""构造模拟 Request 对象。"""
|
||||
req = types.SimpleNamespace()
|
||||
req.client = types.SimpleNamespace(host=client_host)
|
||||
req.headers = {'X-Forwarded-For': xff} if xff else {}
|
||||
return req
|
||||
|
||||
|
||||
def test_xff_single_ip():
|
||||
"""XFF 只有一个 IP → 直接返回"""
|
||||
assert get_client_ip(_make_request('10.0.0.1', xff='1.2.3.4')) == '1.2.3.4'
|
||||
|
||||
|
||||
def test_xff_multiple_ips_returns_leftmost():
|
||||
"""XFF 多个 IP → 取最左侧(真实客户端 IP)"""
|
||||
assert get_client_ip(_make_request('10.0.0.1', xff='1.2.3.4, 10.0.0.1')) == '1.2.3.4'
|
||||
|
||||
|
||||
def test_no_xff_falls_back_to_client_host():
|
||||
"""无 XFF 头 → 回退到 request.client.host"""
|
||||
assert get_client_ip(_make_request('203.0.113.9')) == '203.0.113.9'
|
||||
|
||||
|
||||
def test_no_client_falls_back_to_localhost():
|
||||
"""client 为 None → 回退到 127.0.0.1"""
|
||||
req = types.SimpleNamespace(client=None, headers={})
|
||||
assert get_client_ip(req) == '127.0.0.1'
|
||||
Reference in New Issue
Block a user