4.3 中间件与请求链路 本节摘要:中间件是横亘在每个请求与响应之间的处理层,安全头、会话、CSRF、认证状态都由内置中间件在视图之前就准备妥当。本节讲洋葱模型的执行顺序、内置中间件各自承担什么、如何编写自定义中间件,以及顺序调整的影响。理解中间件,才算真正理解第 1 章那张请求流转图的第一段。 洋葱模型 中间件常被形容为洋葱:请求从外向里穿过每一层,到达视图后再从里向外穿出来。每层有两个时机出手——进的时候处理请求,出的时候处理响应: 洋葱模型 执行顺序有两条铁律:请求阶段按配置列表自上而下,响应阶段自下而上;某一层不调用后续环节(直接返回响应或抛异常),内层就被短路。Django 内置的异常处理会把特定异常翻译成对应状态码,比如权限不足抛出后被转成 403 响应。
本节摘要:中间件是横亘在每个请求与响应之间的处理层,安全头、会话、CSRF、认证状态都由内置中间件在视图之前就准备妥当。本节讲洋葱模型的执行顺序、内置中间件各自承担什么、如何编写自定义中间件,以及顺序调整的影响。理解中间件,才算真正理解第 1 章那张请求流转图的第一段。
中间件常被形容为洋葱:请求从外向里穿过每一层,到达视图后再从里向外穿出来。每层有两个时机出手——进的时候处理请求,出的时候处理响应:

执行顺序有两条铁律:请求阶段按配置列表自上而下,响应阶段自下而上;某一层不调用后续环节(直接返回响应或抛异常),内层就被短路。Django 内置的异常处理会把特定异常翻译成对应状态码,比如权限不足抛出后被转成 403 响应。
项目配置里的默认清单,每行都是一段历史教训:
MIDDLEWARE = [ "django.middleware.security.SecurityMiddleware", # 安全头与跳转 "django.contrib.sessions.middleware.SessionMiddleware", # 会话读写 "django.middleware.common.CommonMiddleware", # 尾斜杠统一 "django.middleware.csrf.CsrfViewMiddleware", # CSRF 校验 "django.contrib.auth.middleware.AuthenticationMiddleware", # request.user "django.contrib.messages.middleware.MessageMiddleware", # 一次性消息 "django.middleware.clickjacking.XFrameOptionsMiddleware", # 防点击劫持 ]
最值得体会的是 AuthenticationMiddleware:它给每个请求注入 user 属性。正因为它排在会话之后、视图之前,视图里那句 request.user 才能在任何地方直接用——你从没写过"查会话、验登录、挂用户"这三步,中间件替所有视图统一做了。这就是横切关注与业务逻辑的分工:每个请求都要做的事,一处统一做。
顺序敏感是真实的。把 CSRF 中间件挪到会话前面,CSRF 校验依赖会话的方案直接失效;把认证中间件挪到 CSRF 前面,登录表单的提交自己就被拦下。原则:安全防护层尽量靠外,依赖会话的层必须排在会话之后。
墨迹博客要记录每个请求的耗时,用来定位慢页面。新式写法基于闭包函数:
# 应用内的中间件模块 import time import logging logger = logging.getLogger(__name__) def request_timing(get_response): def middleware(request): start = time.perf_counter() response = get_response(request) # 调用后续所有层与视图 cost = (time.perf_counter() - start) * 1000 if cost > 300: logger.warning( "慢请求 %s %.0fms", request.path, cost) response["X-Request-Time"] = f"{cost:.0f}ms" return response return middleware
注册进清单(位置按需选择,度量类放最外层才能覆盖全程):
MIDDLEWARE = [ "blog.middleware.request_timing", # 放最前:度量整条链路 # ... 原有清单 ... ]
get_response 是"后半段世界"的入口:调用它,意味着把请求交给后面所有层与视图;不调用而直接返回响应,就是短路。自定义响应头、按路径白名单放行、异常统一转 JSON,全在这一个模式里变体。
需要同时处理请求与响应两阶段时,老式类写法更直观:
class SimpleLoggingMiddleware: def __init__(self, get_response): self.get_response = get_response def __call__(self, request): # 请求阶段:视图之前 logger.info("进入 %s", request.path) response = self.get_response(request) # 响应阶段:视图之后 logger.info("离开 %s 状态 %s", request.path, response.status_code) return response
💡 中间件与装饰器的边界:只影响个别视图的逻辑用装饰器;影响所有请求(或一大类)的逻辑用中间件。登录校验放中间件、记录文章浏览量放视图,各安其位。
写过一两个中间件后,会有一种什么都能往里放的诱惑——毕竟它在每个请求的必经之路上,挂一个逻辑全局生效,太方便了。这份方便正是危险所在。中间件里放重逻辑(复杂查询、外部调用),等于给全站每个请求加了一份固定开销;放业务逻辑,等于把特定功能混进了所有请求的公共路径,与功能无关的请求也在为它买单;放隐式行为(悄悄改写请求参数),排查问题时没人会想到祸根在中间件里。克制原则可以概括成三条:执行要快——毫秒级的轻操作才配上链;职责要横——只处理全站共性关注(日志、计时、异常兜底),垂直业务去视图层;行为要显——中间件做过什么,要在文档里写得明明白白。墨迹博客最终的中间件清单只有六项,每一项都经得起这三问。链路上的位置越关键,越要放得少而精。
至此请求链路全部打通。下一章进入模板层,把视图递来的数据变成用户眼前的页面。