增加了 IP 黑白名单功能(中间件)
This commit is contained in:
@@ -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
|
||||
|
||||
147
utils/ip_filter_middleware.py
Normal file
147
utils/ip_filter_middleware.py
Normal 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-IP(Nginx 单机代理最可靠)
|
||||
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,
|
||||
)
|
||||
Reference in New Issue
Block a user