From b7f987f2c8adcc511bd8f8146bd90ce07945d48c Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 18 Jun 2026 22:25:21 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E4=BA=86=20IP=20=E9=BB=91?= =?UTF-8?q?=E7=99=BD=E5=90=8D=E5=8D=95=E5=8A=9F=E8=83=BD=EF=BC=88=E4=B8=AD?= =?UTF-8?q?=E9=97=B4=E4=BB=B6=EF=BC=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- a_long_dianjing/settings.py | 17 ++++ utils/ip_filter_middleware.py | 147 ++++++++++++++++++++++++++++++++++ 2 files changed, 164 insertions(+) create mode 100644 utils/ip_filter_middleware.py diff --git a/a_long_dianjing/settings.py b/a_long_dianjing/settings.py index ac30768..bdb73fa 100644 --- a/a_long_dianjing/settings.py +++ b/a_long_dianjing/settings.py @@ -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 diff --git a/utils/ip_filter_middleware.py b/utils/ip_filter_middleware.py new file mode 100644 index 0000000..ea1245f --- /dev/null +++ b/utils/ip_filter_middleware.py @@ -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, + )