增加了 IP 黑白名单功能(中间件)

This commit is contained in:
2026-06-18 22:25:21 +08:00
parent f169b6569a
commit b7f987f2c8
2 changed files with 164 additions and 0 deletions

View File

@@ -41,6 +41,7 @@ INSTALLED_APPS = [
]
MIDDLEWARE = [
'utils.ip_filter_middleware.IPFilterMiddleware',
'utils.debug_middleware.DebugExceptionMiddleware',
'corsheaders.middleware.CorsMiddleware',
'django.middleware.security.SecurityMiddleware',
@@ -209,6 +210,22 @@ CORS_ALLOW_HEADERS = [
"dnt", "origin", "user-agent", "x-csrftoken", "x-requested-with",
]
# ==================== IP 访问控制 ====================
# 访问模式:
# production — 生产模式(默认):放行所有 IP
# whitelist — 白名单模式:仅允许白名单内 IP 访问,其余返回 403
# blacklist — 黑名单模式:禁止黑名单内 IP 访问,其余放行
IP_ACCESS_MODE = 'whitelist'
# 白名单whitelist 模式下生效)— 支持单 IP 和 CIDR 网段
IP_WHITELIST = [
'111.32.64.171',
'43.142.0.208',
]
# 黑名单blacklist 模式下生效)— 支持单 IP 和 CIDR 网段
IP_BLACKLIST = []
# 免过滤路径(任何模式下都放行,如管理后台、健康检查)
IP_FILTER_EXEMPT_PATHS = []
# ==================== 腾讯云 COS ====================
COS_SECRET_ID = app_secrets.COS_SECRET_ID
COS_SECRET_KEY = app_secrets.COS_SECRET_KEY

View File

@@ -0,0 +1,147 @@
"""
IP 白黑名单访问控制中间件
支持三种访问模式(通过 settings.IP_ACCESS_MODE 切换):
- ``production`` 生产模式(默认):放行所有 IP
- ``whitelist`` 白名单模式:仅允许白名单内 IP 访问,其余返回 403
- ``blacklist`` 黑名单模式:禁止黑名单内 IP 访问,其余放行
IP 来源优先级X-Real-IP > X-Forwarded-For > REMOTE_ADDR
白名单/黑名单支持:
- 单 IP``'1.2.3.4'``
- CIDR 网段:``'10.0.0.0/8'``
- IPv6``'::1'`` / ``'2001:db8::/32'``
配置示例settings.py::
IP_ACCESS_MODE = 'whitelist' # production / whitelist / blacklist
IP_WHITELIST = ['127.0.0.1', '10.0.0.0/8']
IP_BLACKLIST = ['1.2.3.4']
IP_FILTER_EXEMPT_PATHS = ['/admin/', '/health/'] # 免过滤路径
"""
import ipaddress
import logging
from django.http import JsonResponse
from django.conf import settings
logger = logging.getLogger('django')
def get_client_ip(request) -> str:
"""获取客户端真实 IP。
优先级X-Real-IP > X-Forwarded-For取第一个非 unknown> REMOTE_ADDR
"""
# 1. X-Real-IPNginx 单机代理最可靠)
x_real_ip = request.META.get('HTTP_X_REAL_IP')
if x_real_ip:
ip = x_real_ip.strip()
if ip and ip.lower() != 'unknown':
return ip
# 2. X-Forwarded-For代理链第一个是客户端真实 IP
x_forwarded_for = request.META.get('HTTP_X_FORWARDED_FOR')
if x_forwarded_for:
for part in x_forwarded_for.split(','):
part = part.strip()
if part and part.lower() != 'unknown':
return part
# 3. REMOTE_ADDR 兜底
return request.META.get('REMOTE_ADDR', '0.0.0.0') or '0.0.0.0'
def _parse_ip_list(raw_list) -> list:
"""将配置中的 IP/CIDR 字符串列表解析为 ipaddress 网络对象列表。"""
networks = []
for entry in raw_list or []:
entry = str(entry).strip()
if not entry:
continue
try:
# 同时兼容单 IP 和 CIDR
networks.append(ipaddress.ip_network(entry, strict=False))
except ValueError:
logger.warning(f"[IP过滤] 无法解析 IP/CIDR: {entry},已忽略")
return networks
def _ip_in_networks(ip_str: str, networks: list) -> bool:
"""判断 IP 是否落在任一网段内。"""
try:
ip_obj = ipaddress.ip_address(ip_str)
except ValueError:
# 非法 IP 地址,按不匹配处理
return False
return any(ip_obj in net for net in networks)
class IPFilterMiddleware:
"""IP 白黑名单访问控制中间件。
根据 ``settings.IP_ACCESS_MODE`` 决定访问策略:
============ ====================================================
模式 行为
============ ====================================================
production 放行所有请求(默认)
whitelist 仅白名单内 IP 可访问;其余 403
blacklist 黑名单内 IP 禁止访问;其余放行
============ ====================================================
"""
def __init__(self, get_response):
self.get_response = get_response
self.mode = getattr(settings, 'IP_ACCESS_MODE', 'production').lower()
self.whitelist = _parse_ip_list(getattr(settings, 'IP_WHITELIST', []))
self.blacklist = _parse_ip_list(getattr(settings, 'IP_BLACKLIST', []))
self.exempt_paths = tuple(
p.rstrip('/') for p in getattr(settings, 'IP_FILTER_EXEMPT_PATHS', [])
)
logger.info(
f"[IP过滤] 中间件已启用,模式={self.mode}"
f"白名单={len(self.whitelist)}条,黑名单={len(self.blacklist)}条,"
f"免过滤路径={self.exempt_paths}"
)
def __call__(self, request):
# 生产模式直接放行
if self.mode == 'production':
return self.get_response(request)
# 免过滤路径直接放行(如 /admin/、/health/
path = request.path
if self.exempt_paths and path.startswith(self.exempt_paths):
return self.get_response(request)
client_ip = get_client_ip(request)
# 白名单模式:不在白名单内 → 403
if self.mode == 'whitelist':
if not _ip_in_networks(client_ip, self.whitelist):
logger.warning(
f"[IP过滤-白名单] 拒绝访问 path={path} ip={client_ip}"
)
return self._forbidden_response(client_ip)
# 黑名单模式:在黑名单内 → 403
elif self.mode == 'blacklist':
if _ip_in_networks(client_ip, self.blacklist):
logger.warning(
f"[IP过滤-黑名单] 拒绝访问 path={path} ip={client_ip}"
)
return self._forbidden_response(client_ip)
return self.get_response(request)
@staticmethod
def _forbidden_response(client_ip: str) -> JsonResponse:
"""返回 403 JSON 响应。"""
return JsonResponse(
{'code': 403, 'message': '访问被拒绝'},
status=403,
)