feat(auth): 完成 AUTH-01~04 后端实现 - IP白名单中间件+mfa.py删除+agents.py重构+conftest修复

This commit is contained in:
Simon
2026-07-07 22:54:17 +08:00
parent fab75760e0
commit d56a9a6079
6 changed files with 153 additions and 587 deletions
@@ -0,0 +1,137 @@
# =============================================================================
# 企微IT智能服务台 — 管理端 IP 白名单中间件(三端认证重构 AUTH-02)
# =============================================================================
# 说明:管理后台(/api/admin/*)的 IP 白名单校验中间件。
#
# 工作原理:
# 1. 仅在 production 环境启用(非生产环境直接放行)
# 2. 仅对管理端 API 路径生效(/api/admin/* 和 /api/auth/otp-admin-*
# 3. 校验客户端 IP 是否在 admin_allowed_ips 白名单内
# 4. 不在白名单内则返回 403 + 统一错误格式 {code: 4004, message: "无访问权限"}
#
# 配置来源:
# - APP_ENV=production 时启用(从环境变量读取)
# - admin_allowed_ips 配置在 app.config(格式:"117.147.35.138,218.75.34.87,10.240.0.0/16"
#
# 使用方式:
# 在 main.py 中注册:
# from app.middleware.admin_ip_whitelist import AdminIPWhitelistMiddleware
# app.add_middleware(AdminIPWhitelistMiddleware)
# =============================================================================
import logging
import re
from typing import Callable
from fastapi import Request, Response
from starlette.middleware.base import BaseHTTPMiddleware
from starlette.responses import JSONResponse
from app.utils.env_gating import is_production, ip_in_whitelist
from app.utils.response import error_response
logger = logging.getLogger(__name__)
# 需要校验 IP 白名单的管理端路径前缀(正则表达式)
ADMIN_PATH_PATTERNS = [
r"^/api/admin/", # 管理后台 API
r"^/api/auth/otp-admin", # 管理端 OTP 操作
]
class AdminIPWhitelistMiddleware(BaseHTTPMiddleware):
"""管理端 IP 白名单校验中间件。
仅在 production 环境对管理端 API 路径生效。
非生产环境(dev/test)直接放行,方便本地开发。
"""
def __init__(self, app, *args, **kwargs):
super().__init__(app, *args, **kwargs)
# 预编译正则表达式,提升匹配性能
self._compiled_patterns = [re.compile(p) for p in ADMIN_PATH_PATTERNS]
def _is_admin_path(self, path: str) -> bool:
"""判断路径是否属于管理端 API。
Args:
path: 请求路径(如 "/api/admin/users"
Returns:
bool: True=管理端路径,False=非管理端路径
"""
for pattern in self._compiled_patterns:
if pattern.match(path):
return True
return False
async def dispatch(self, request: Request, call_next: Callable) -> Response:
"""中间件主逻辑。
1. 非 production 环境 → 直接放行
2. 非管理端路径 → 直接放行
3. 获取客户端 IP → 白名单校验
- 在白名单 → 放行
- 不在白名单 → 返回 403
"""
# 1. 非 production 环境 → 直接放行(环境门控)
if not is_production():
return await call_next(request)
# 2. 非管理端路径 → 直接放行
path = request.url.path
if not self._is_admin_path(path):
return await call_next(request)
# 3. 获取客户端 IP
# 优先从 X-Forwarded-For 获取(反向代理场景)
# 否则使用 request.client.host
client_ip = self._get_client_ip(request)
logger.debug(f"管理端 IP 校验: path={path}, client_ip={client_ip}")
# 4. 白名单校验
if not ip_in_whitelist(client_ip):
logger.warning(
f"管理端 IP 未授权: path={path}, client_ip={client_ip}, "
f"拒绝访问"
)
# 返回统一错误格式(与 AppException 一致)
return JSONResponse(
status_code=200, # HTTP 状态码 200,业务错误码在 body 中
content=error_response(
code=4004,
message="无访问权限:您的 IP 不在允许范围内,请联系管理员"
),
)
# 5. 在白名单内 → 放行
return await call_next(request)
def _get_client_ip(self, request: Request) -> str:
"""获取客户端真实 IP。
优先从 X-Forwarded-For 请求头获取(反向代理场景),
这是标准的获取客户端真实 IP 的方式。
Args:
request: FastAPI 请求对象
Returns:
str: 客户端 IP 地址
"""
# X-Forwarded-For 可能包含多个 IP,第一个是原始客户端
forwarded_for = request.headers.get("X-Forwarded-For")
if forwarded_for:
# 格式:"client_ip, proxy1, proxy2"
# 取第一个(原始客户端)
ips = forwarded_for.split(",")
if ips:
return ips[0].strip()
# 直接连接场景
if request.client:
return request.client.host
# 兜底
return ""