import{_ as s,o as n,c as e,a2 as t}from"./chunks/framework.B-QBKwc1.js";const u=JSON.parse('{"title":"中间件执行顺序","description":"本页展示请求在框架中的完整中间件执行流程,包括全局中间件和路由级中间件的执行顺序,帮助开发者理解请求处理链路。","frontmatter":{},"headers":[],"relativePath":"zh/appendix/middleware.md","filePath":"zh/appendix/middleware.md"}'),d={name:"zh/appendix/middleware.md"};function p(o,a,l,i,c,r){return n(),e("div",null,[...a[0]||(a[0]=[t(`
概述
本页展示请求在框架中的完整中间件执行流程,包括全局中间件和路由级中间件的执行顺序,帮助开发者理解请求处理链路。注册位置:bootstrap/app.php(别名 + API 组)与 routes/api.php(路由组)。
请求进入
│
▼
┌────────────────┐
│ProxyMiddleware │ API 组前置(最先执行)
│ │ 从代理头提取真实客户端 IP
└──────┬─────────┘
│
▼
┌──────────────┐
│ Cors │ API 组前置
│ │ 处理 OPTIONS 预检 + 跨域响应头
└──────┬───────┘
│
▼
┌──────────────┐
│ForceJson │ API 组前置
│Response │ 强制返回 JSON 响应头
└──────┬───────┘
│
▼
┌──────────────┐ 401
│Authenticate │──────────► 未认证(Token 缺失/无效/过期)
│ (auth.jwt) │ 403
│ JWT认证 │──────────► 无权限
│ 权限校验 │
└──────┬───────┘
│ 注入 userInfo(attributes)
▼
┌──────────────┐ 403
│ResolveTenant │──────────► 租户异常(禁用/过期)
│ (tenant) │
│ 租户上下文 │
└──────┬───────┘
│ 注入 tenantId / isSuperAdmin / tenantInfo
▼
┌──────────────┐ 403
│ demo │──────────► 演示模式下写操作被拦截
│ (demo 别名) │
│ 演示模式拦截 │
└──────┬───────┘
│ 放行:非演示模式 / GET / 有 #[DemoAllow]
▼
┌──────────────┐
│ LogOperation │ 记录操作日志(terminable,响应后写入)
│(log.operation)│ 写操作始终记录(#[WithoutOperationLog] 跳过)
└──────┬───────┘
│
▼
┌──────────────┐
│ 控制器方法 │
└──────────────┘两种挂载方式
bootstrap/app.php → $middleware->api(prepend:)):Proxy → Cors → ForceJson,对全部 /api 请求生效routes/api.php → Route::middleware([...])->group(...)):auth.jwt → tenant → demo → log.operation,仅认证接口组生效;登录、验证码、令牌刷新等公开接口定义在组外,天然免认证| 中间件 | 类型 | 职责 | 配置 |
|---|---|---|---|
| ProxyMiddleware | API 组前置 | 代理头解析,提取真实客户端 IP | .env → TRUSTED_PROXIES |
| Cors | API 组前置 | 跨域处理(OPTIONS 预检 + 响应头) | config/cors.php → CORS_ALLOW_ORIGIN |
| ForceJsonResponse | API 组前置 | 强制 Content-Type: application/json | — |
| Authenticate | 路由组(别名 auth.jwt) | JWT 认证 + 权限校验(#[Permission] 注解) | config/jwt.php → header_prefix |
| ResolveTenant | 路由组(别名 tenant) | 租户上下文注入与校验 | 超管(uid=1)跳过校验 |
| LogOperation | 路由组(别名 log.operation) | 操作日志记录(terminable,响应后落库,#[WithoutOperationLog] 跳过) | — |
| DemoMiddleware | 路由组(别名 demo,tenant 与 log.operation 之间) | 演示模式拦截写操作(无 #[DemoAllow] 的写操作返回 403) | config('app.demo_mode') |
| 目标 | 机制 | 说明 |
|---|---|---|
认证(auth.jwt) | 路由分组 | 公开接口(login、captcha、refresh、oauth2)定义在认证组外,天然免认证 |
操作日志(log.operation) | #[WithoutOperationLog] 注解 | 方法标注即不写入 OperationLog,优先级高于"写操作一律记录";适用于已有专门日志载体的接口,如登出记录写入登录日志(login_log,log_type=2) |
操作日志排除机制详见 4.2 请求处理链路。
| 中间件 | 注入字段 | 读取方式 | 说明 |
|---|---|---|---|
| ProxyMiddleware | $_SERVER['REMOTE_ADDR'] | request()->ip() | 覆盖为真实客户端 IP |
| Authenticate | userInfo | $request->attributes->get('userInfo') | 解码后的 JWT 载荷(uid、username 等) |
| ResolveTenant | tenantId | $request->attributes->get('tenantId') | 租户 ID(0=平台用户) |
| ResolveTenant | isSuperAdmin | $request->attributes->get('isSuperAdmin') | 是否超级管理员(uid=1) |
| ResolveTenant | tenantInfo | $request->attributes->get('tenantInfo') | 非平台用户时注入租户模型实例 |
当项目部署在 CDN / 负载均衡 / Nginx 反向代理后面时,PHP 的 REMOTE_ADDR 是代理服务器的 IP(通常是 127.0.0.1),真实客户端 IP 由代理通过 X-Forwarded-For / X-Real-IP 请求头传递。
客户端(真实IP) → CDN/代理 → Nginx → PHP-FPM
↓ 设置头
X-Forwarded-For: 真实IP, 代理IP
X-Real-IP: 真实IPREMOTE_ADDR 是否在受信代理列表(.env → TRUSTED_PROXIES,支持 CIDR 网段)X-Real-IP 或 X-Forwarded-For 头提取真实 IP$_SERVER['REMOTE_ADDR'],后续 request()->ip() 与 get_client_ip() 返回真实 IP# .env
# 受信代理 IP(逗号分隔,支持 CIDR 网段)
# 未配置时默认信任 127.0.0.1 和 ::1
TRUSTED_PROXIES=127.0.0.1,::1,101.226.0.0/15,180.101.0.0/16