""" 中间件模块 ============================================================= 整合自 application/middleware/*.py: - check_login: 登录校验(JWT + 白名单) - check_demo: 演示模式拦截 - transaction: AOP 事务拦截器 - op_log: 操作日志自动记录(写入器由应用工厂注入) - error_handler: 全局异常处理 依赖约束:本模块仅依赖 core/common,不依赖 modules 层。 """ import logging import fnmatch from flask import g, request, current_app from config import LOGIN_WHITELIST_EXACT, LOGIN_WHITELIST_DEBUG, LOGIN_WHITELIST_PREFIX, LOGIN_WHITELIST_WILDCARD from src.core import response as R from src.core.jwt import parse_payload, is_token_blacklisted, get_access_token from functools import wraps from config import FLASK_DEMO from src.core.database import db import time import traceback from werkzeug.exceptions import HTTPException from src.core.exceptions import BizError logger = logging.getLogger(__name__) # ============================================================ # 登录校验中间件 # ============================================================ # 获取日志记录器 def is_whitelisted(path: str) -> bool: """ 检查请求路径是否在白名单中 支持三种匹配模式: 1. 精确匹配:路径完全相等 2. 前缀匹配:路径以指定前缀开头 3. 通配符匹配:支持 *(匹配任意字符)和 ?(匹配单个字符) Args: path: 请求路径(如:/job/sendSms) Returns: bool: True-在白名单中,False-不在白名单中 """ # ============================================================ # 1. 精确匹配检查 # ============================================================ # 示例:LOGIN_WHITELIST_EXACT=/login,/captcha # 当 path 为 /login 或 /captcha 时返回 True exact_urls = LOGIN_WHITELIST_EXACT if exact_urls: for url in exact_urls.split(","): if url.strip() and url.strip() == path: if LOGIN_WHITELIST_DEBUG: logger.debug(f"精确匹配白名单: {path}") return True # ============================================================ # 2. 前缀匹配检查 # ============================================================ # 示例:LOGIN_WHITELIST_PREFIX=/job/sendSms,/public # 当 path 以 /job/sendSms 或 /public 开头时返回 True prefix_urls = LOGIN_WHITELIST_PREFIX if prefix_urls: for prefix in prefix_urls.split(","): prefix = prefix.strip() if prefix and path.startswith(prefix): if LOGIN_WHITELIST_DEBUG: logger.debug(f"前缀匹配白名单: {path} 匹配 {prefix}") return True # ============================================================ # 3. 通配符匹配检查 # ============================================================ # 示例:LOGIN_WHITELIST_WILDCARD=/job/*/send*,/api/v1/*/public* # 当 path 匹配通配符模式时返回 True # - * 匹配任意多个字符 # - ? 匹配单个字符 wildcard_patterns = LOGIN_WHITELIST_WILDCARD if wildcard_patterns: for pattern in wildcard_patterns.split(","): pattern = pattern.strip() if pattern and fnmatch.fnmatch(path, pattern): if LOGIN_WHITELIST_DEBUG: logger.debug(f"通配符匹配白名单: {path} 匹配 {pattern}") return True return False def check_login(): """ 用户登录验证中间件 功能说明: 1. 检查请求路径是否在白名单中,白名单路径直接放行 2. OPTIONS请求(CORS预检)直接放行 3. 验证JWT Token的有效性 4. Token验证通过后将用户信息存储到g对象中 白名单配置(在.env文件中): - LOGIN_WHITELIST_EXACT: 精确匹配白名单 - LOGIN_WHITELIST_PREFIX: 前缀匹配白名单 - LOGIN_WHITELIST_WILDCARD: 通配符匹配白名单 Returns: None: 验证通过,继续处理请求 R.failed: 验证失败,返回错误响应(401未登录) 使用示例: # 在app.py中注册中间件 @app.before_request def before_request(): result = check_login() if result: return result """ current_path = request.path # ============================================================ # 步骤1:检查白名单 # ============================================================ # 白名单中的路径不需要验证Token(如:登录页、验证码、短信发送接口等) if is_whitelisted(current_path): if LOGIN_WHITELIST_DEBUG: logger.debug(f"白名单路径,跳过登录验证: {current_path}") return None # ============================================================ # 步骤2:OPTIONS请求放行 # ============================================================ # OPTIONS请求是CORS预检请求,不需要验证Token if request.method == "OPTIONS": if LOGIN_WHITELIST_DEBUG: logger.debug(f"OPTIONS请求,跳过登录验证: {current_path}") return None # ============================================================ # 步骤3:获取并验证Token # ============================================================ success, access_token, msg = get_access_token() # 如果没有Token if not success: logger.warning(f"未登录访问: {current_path}, IP: {request.remote_addr}") return R.failed(code=401, msg="请先登录") # JWT解密验证 result = parse_payload(access_token) # 验证失败处理 if result.get("code") != 0: logger.warning(f"Token验证失败: {result.get('msg')}, 路径: {current_path}") return R.failed(code=401, msg=result.get("msg", "Token无效,请重新登录")) # ============================================================ # 步骤3.5:检查Token黑名单 # ============================================================ # 退出登录时Token会被加入黑名单,黑名单中的Token即使未过期也应拒绝 if is_token_blacklisted(access_token): logger.warning(f"Token已被注销(黑名单): user_id={result.get('data', {}).get('userId')}, 路径: {current_path}") return R.failed(code=401, msg="Token已失效,请重新登录") # ============================================================ # 步骤4:存储用户信息 # ============================================================ # 将用户信息存储到g对象中,供后续的视图函数和权限验证使用 if result.get("data"): g.user_id = result["data"].get("userId") g.user_info = result["data"] if LOGIN_WHITELIST_DEBUG: logger.debug(f"用户登录验证通过: user_id={g.user_id}, 路径={current_path}") # 验证通过,返回None继续处理请求 return None # ============================================================ # 演示模式拦截 # ============================================================ """ 演示模式拦截装饰器 ============================================================= 在演示环境(FLASK_DEMO=True)下拦截所有写操作, 返回"演示环境,暂无操作权限"提示。 使用方式: 在 View 路由函数上添加 @check_demo 装饰器, 替代原有的 if FLASK_DEMO: return R.failed(...) 判断。 """ def check_demo(func): """ 演示模式拦截装饰器 当 FLASK_DEMO=True 时,拦截写操作并返回提示。 应放在 @check_permission 之后、路由函数之前。 Example: @position_blue.route('/add', methods=["POST"]) @check_permission("sys:position:add") @check_demo def add(): result = position.add_position() return result """ @wraps(func) def wrapper(*args, **kwargs): if FLASK_DEMO: return R.failed("演示环境,暂无操作权限") return func(*args, **kwargs) return wrapper # ============================================================ # AOP 事务拦截器 # ============================================================ """ AOP 事务拦截器(企业级实现) 设计理念: 1. 零代码侵入:业务代码无需任何事务管理代码 2. 自动读写分离:GET 不提交,POST/PUT/DELETE 自动提交 3. 统一异常处理:所有异常自动回滚 4. 性能监控:记录事务执行时间 使用方式: 在 Flask 应用初始化时调用 register_transaction(app) 即可。 业务代码无需任何改动。 Example: # app/__init__.py from src.core.middleware import register_transaction def create_app(): app = Flask(__name__) register_transaction(app) # 一行代码搞定所有事务 return app """ # ============================================================ # 核心:AOP 事务拦截器 # ============================================================ def register_transaction(app): """ 注册 AOP 事务拦截器到 Flask 应用 在请求生命周期中自动管理事务: - before_request: 开启事务标记 - after_request: 自动提交(POST/PUT/DELETE) - teardown_request: 异常回滚 + 资源清理 Args: app: Flask 应用实例 Example: register_transaction(app) """ @app.before_request def begin_transaction(): """请求开始:记录开始时间,开启事务标记""" g.request_start_time = time.time() g.transaction_active = True logger.debug(f"请求开始, method: {request.method}, path: {request.path}, " f"ip: {request.remote_addr}") @app.after_request def commit_transaction(response): """ 请求结束:自动提交事务 提交规则: 1. 只对写操作(POST/PUT/DELETE/PATCH)提交 2. 只在响应成功(2xx)且业务成功(code=0)时提交 3. GET/OPTIONS/HEAD 不提交(只读) 4. 业务失败(R.failed)不提交,自动回滚 """ # 只对写操作处理事务 if request.method in ("POST", "PUT", "DELETE", "PATCH"): should_commit = 200 <= response.status_code < 300 # 检查业务状态码:R.failed() 返回 HTTP 200 但 code=1/ok=False if should_commit: try: data = response.get_json(silent=True) if data and data.get("code", 0) != 0: should_commit = False except Exception: pass if should_commit: try: if db.session.is_active: db.session.commit() logger.debug( f"事务提交成功, method: {request.method}, " f"path: {request.path}, status: {response.status_code}" ) except Exception as e: db.session.rollback() logger.error( f"事务提交失败, method: {request.method}, " f"path: {request.path}, error: {str(e)}", exc_info=True, ) from src.core import response as R return R.failed(msg="数据处理失败"), 500 else: # 业务失败或非2xx,回滚未提交的变更 try: if db.session.is_active: db.session.rollback() except Exception: pass # 记录响应日志 elapsed = time.time() - g.get("request_start_time", time.time()) logger.debug( f"请求结束, method: {request.method}, path: {request.path}, " f"status: {response.status_code}, elapsed: {elapsed:.3f}s" ) return response @app.teardown_request def cleanup_transaction(exception=None): """ 请求结束:清理资源 发生异常时自动回滚事务,确保数据一致性。 """ if exception is not None: try: db.session.rollback() logger.error( f"请求异常,事务已回滚, method: {request.method}, " f"path: {request.path}, error: {str(exception)}", exc_info=True, ) except Exception as e: logger.error(f"事务回滚失败: {str(e)}", exc_info=True) # 清理请求标记 g.pop("transaction_active", None) g.pop("request_start_time", None) logger.info("AOP 事务拦截器注册成功") # ============================================================ # 备用:精细控制装饰器(特殊场景使用) # ============================================================ def transactional(func): """ 事务装饰器(备用) 适用于需要精细控制事务边界的特殊场景。 大多数情况下,使用 AOP 拦截器即可。 Example: @transactional def complex_business(): # 精细控制的事务逻辑 pass """ @wraps(func) def wrapper(*args, **kwargs): if g.get("disable_auto_commit", False): return func(*args, **kwargs) try: result = func(*args, **kwargs) if not g.get("transaction_active", False): db.session.commit() return result except Exception as e: db.session.rollback() logger.error(f"装饰器事务执行失败: {str(e)}", exc_info=True) raise return wrapper def transactional_response(func): """ 事务装饰器(带返回 R 对象) 用于需要直接返回 R 对象的接口。 Example: @transactional_response def add_position(): position.save() return R.ok(msg="添加成功") """ @wraps(func) def wrapper(*args, **kwargs): from src.core import response as R try: result = func(*args, **kwargs) if not g.get("transaction_active", False): db.session.commit() return result except Exception as e: db.session.rollback() logger.error(f"装饰器事务执行失败: {str(e)}", exc_info=True) return R.failed(msg=f"操作失败: {str(e)}") return wrapper # ============================================================ # 备用:事务上下文管理器(特殊场景使用) # ============================================================ class TransactionContext: """ 事务上下文管理器 用于需要精细控制事务的特殊场景。 Example: with TransactionContext(): position.save() user.save() # 自动提交或回滚 """ def __enter__(self): g.disable_auto_commit = True return self def __exit__(self, exc_type, exc_val, exc_tb): g.disable_auto_commit = False if exc_type is None: if not g.get("transaction_active", False): db.session.commit() else: db.session.rollback() logger.error(f"事务上下文回滚: {str(exc_val)}", exc_info=True) return False # ============================================================ # 操作日志自动记录(DI:写入器由应用工厂注入) # ============================================================ """ 操作日志自动记录中间件 ============================================================= 通过 after_request 钩子自动记录写操作(POST/PUT/DELETE/PATCH)日志。 无需在每个视图函数中手动调用 record_operation_log。 记录规则: - 只记录写操作(POST/PUT/DELETE/PATCH),GET/OPTIONS/HEAD 不记录 - 只记录成功操作(HTTP 2xx 且业务 code=0) - 内部调用(定时任务等)不记录 - 登录/退出/验证码等路径不记录(由 login_log 单独记录) - 日志记录失败不影响主流程 """ # 不记录操作日志的路径前缀(登录/退出/验证码等由 login_log 单独记录) _EXCLUDED_PREFIXES = ("/login", "/captcha", "/logout") # HTTP 方法 → 操作日志类型 映射 # 类型值参考 application/constants/dict.py 的 OPERATION_LOG_TYPE_LIST _METHOD_LOG_TYPE = { "POST": 3, # 新增 "PUT": 4, # 修改 "DELETE": 5, # 删除 "PATCH": 4, # 修改 } # 路径后缀 → 操作日志类型 覆盖映射(更精确) _PATH_ACTION_TYPE = { "/add": 3, # 新增 "/update": 4, # 修改 "/delete": 5, # 删除 "/status": 4, # 状态变更→修改 "/batchdelete": 5, # 批量删除 "/reset": 4, # 重置→修改 "/save": 4, # 保存→修改 "/sort": 4, # 排序→修改 } def register_operation_log(app): """ 注册操作日志自动记录中间件 在 after_request 中自动记录写操作日志。 Args: app: Flask 应用实例 """ @app.after_request def record_op_log(response): # 只记录写操作 if request.method not in ("POST", "PUT", "DELETE", "PATCH"): return response # 排除登录/退出/验证码等路径 path = request.path if any(path.startswith(prefix) for prefix in _EXCLUDED_PREFIXES): return response # 只记录成功操作(HTTP 2xx 且业务 code=0) if not (200 <= response.status_code < 300): return response try: data = response.get_json(silent=True) if data and data.get("code", 0) != 0: return response except Exception: pass # 内部调用不记录 if getattr(g, "_internal_call", False): return response # 异步记录,不阻塞响应 try: _write_log(response) except Exception as e: logger.warning(f"记录操作日志失败: {str(e)}") return response logger.info("操作日志中间件注册成功") def _write_log(response): """写入操作日志""" from src.core.security import get_username from src.common.utils.ip import get_client_ip from src.common.utils.ip2region import get_ip_location from src.common.utils.user_agent import parse_user_agent writer = current_app.extensions.get("operation_log_writer") if writer is None: logger.warning("operation_log_writer 未注入,跳过操作日志") return username = get_username() or "system" client_ip = get_client_ip() ua_info = parse_user_agent() # 确定操作类型 log_type = _infer_log_type(request.method, request.path) # 构建标题:从路径提取模块名,如 /user/add → "用户管理-新增" title = _build_title(request.path) # 请求参数(限制长度,避免存入过大的数据) param = _get_request_param() # 返回结果(限制长度) result = _get_response_result(response) # 请求耗时 start_time = getattr(g, "request_start_time", None) consume_time = int((time.time() - start_time) * 1000) if start_time else 0 writer( title=title, log_type=log_type, source=0, # OperationLog.Source.ADMIN status=0, # OperationLog.Status.NORMAL method=request.method, request_method=request.endpoint or "", url=request.path, param=param, result=result, ip=client_ip, location=get_ip_location(client_ip), os=ua_info.get("os", "未知"), browser=ua_info.get("browser", "未知"), consume_time=consume_time, error="", username=username, ) def _infer_log_type(method, path): """根据请求方法和路径推断操作日志类型""" # 优先根据路径后缀匹配 path_lower = path.lower() for suffix, log_type in _PATH_ACTION_TYPE.items(): if path_lower.endswith(suffix): return log_type # 回退到 HTTP 方法映射 return _METHOD_LOG_TYPE.get(method, 18) # 18=其他 def _get_request_param(): """获取请求参数""" try: if request.method in ("POST", "PUT", "PATCH", "DELETE"): # 优先解析 JSON body(batchDelete 等 DELETE 请求也带 body) data = request.get_json(silent=True) if data: import json return json.dumps(data, ensure_ascii=False) # 回退到 URL 查询参数 if request.args: import json from collections import OrderedDict return json.dumps(OrderedDict(request.args), ensure_ascii=False) except Exception: pass return "" def _get_response_result(response): """获取响应结果中的data字段""" try: data = response.get_json(silent=True) if data and isinstance(data, dict): result_data = data.get("data") if result_data is not None: import json return json.dumps(result_data, ensure_ascii=False) # data字段为空时取msg return data.get("msg", "") except Exception: pass return "" def _build_title(path): """从请求路径构建操作标题""" # 路径格式: /user/add, /role/update, /menu/delete 等 parts = path.strip("/").split("/") if len(parts) >= 2: module = parts[0] # 模块名中文化映射 module_names = { "user": "用户管理", "role": "角色管理", "menu": "菜单管理", "dept": "部门管理", "position": "岗位管理", "level": "职级管理", "param": "参数配置", "dict": "字典管理", "dictitem": "字典项管理", "config": "系统配置", "configitem": "配置项管理", "category": "分类管理", "article": "文章管理", "notice": "通知管理", "link": "友情链接", "job": "定时任务", "login": "登录管理", "loginlog": "登录日志", "operationlog": "操作日志", "upload": "文件上传", } module_name = module_names.get(module.lower(), module) action = parts[-1] if len(parts) > 1 else "" action_names = { "add": "新增", "update": "修改", "delete": "删除", "status": "状态变更", "batchdelete": "批量删除", "reset": "重置", "save": "保存", "sort": "排序", "page": "查询", "detail": "查询", "list": "查询", } action_name = action_names.get(action.lower(), action) return f"{module_name}-{action_name}" if action_name else module_name return path # ============================================================ # 全局异常处理 # ============================================================ """ 全局异常处理模块 ============================================================= 注册 Flask errorhandler,将所有 HTTP 错误和未捕获异常 统一转为 R.failed() JSON 响应,避免前端收到 HTML 错误页导致解析崩溃。 处理范围: - 400:请求参数错误 - 401:未授权 - 403:权限不足 - 404:资源不存在 - 405:请求方法不允许 - 500:服务器内部错误 - Exception:兜底,捕获所有未预期异常 """ def register_error_handlers(app): """ 注册全局异常处理器 确保所有 HTTP 错误和未捕获异常都返回标准 JSON 响应。 Args: app: Flask 应用实例 """ @app.errorhandler(400) def bad_request(e): return R.failed(msg="请求参数错误", code=400), 400 @app.errorhandler(401) def unauthorized(e): return R.failed(msg="未授权,请重新登录", code=401), 401 @app.errorhandler(403) def forbidden(e): return R.failed(msg="权限不足", code=403), 403 @app.errorhandler(404) def not_found(e): return R.failed(msg="请求的资源不存在", code=404), 404 @app.errorhandler(405) def method_not_allowed(e): return R.failed(msg="请求方法不允许", code=405), 405 @app.errorhandler(500) def internal_error(e): logger.error( f"服务器内部错误, method: {request.method}, path: {request.path}, " f"error: {str(e)}", exc_info=True ) msg = f"服务器内部错误: {str(e)}" if app.debug else "服务器内部错误" return R.failed(msg=msg, code=500), 500 @app.errorhandler(BizError) def handle_biz_error(e): """ 业务异常处理器 业务代码 raise BizError(...) 时统一转换为 R.failed 响应, 并回滚当前事务(teardown_request 处理)。 """ logger.info(f"业务异常: {e.msg} (code={e.code})") return R.failed(msg=e.msg, code=e.code, data=e.data, **e.kwargs) @app.errorhandler(Exception) def handle_exception(e): # HTTPException 子类(如 abort(403))已有专用 handler,不重复处理 if isinstance(e, HTTPException): return e.get_response() # 非HTTP异常:未知运行时异常 logger.error(f"未捕获异常, method: {request.method}, path: {request.path}, " f"error: {traceback.format_exc()}") msg = f"服务器内部错误: {str(e)}" if app.debug else "服务器内部错误" return R.failed(msg=msg, code=500), 500 logger.info("全局异常处理器注册成功") # ============================================================ # 中间件注册 # ============================================================ def register_middleware(app): """ 注册应用中间件(登录校验 before_request)。 Args: app: Flask 应用实例 """ @app.before_request def checklogin(): """登录验证中间件""" return check_login()