# F. 中间件执行顺序 :::tip 概述 本页展示请求在框架中的完整中间件执行流程,包括全局中间件和路由级中间件的执行顺序,帮助开发者理解请求处理链路。注册位置:`bootstrap/app.php`(别名 + API 组)与 `routes/api.php`(路由组)。 ::: ## 执行流程图 ```text 请求进入 │ ▼ ┌────────────────┐ │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)│ 写操作始终记录 └──────┬───────┘ │ ▼ ┌──────────────┐ │ 控制器方法 │ └──────────────┘ ``` :::info 两种挂载方式 - **API 组前置**(`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/middleware.php` → `auth_except` | | ResolveTenant | 路由组(别名 `tenant`) | 租户上下文注入与校验 | 超管(uid=1)跳过校验 | | LogOperation | 路由组(别名 `log.operation`) | 操作日志记录(terminable,响应后落库) | `config/middleware.php` → `log_except` | | DemoMiddleware | 路由组(别名 `demo`,tenant 与 log.operation 之间) | 演示模式拦截写操作(无 `#[DemoAllow]` 的写操作返回 403) | `config('app.demo_mode')` | ## 排除配置 ```php // config/middleware.php return [ // Authenticate::handle() 命中时跳过 JWT 解析与权限校验(公开接口定义在路由组外,此为兜底) 'auth_except' => [ 'login', 'captcha', 'refresh', ], // LogOperation::handle() 命中时不写入 OperationLog // 登录/登出由 LoginLog 单独记录 'log_except' => [ 'login', 'logout', 'captcha', ], ]; ``` ## 注入内容 | 中间件 | 注入字段 | 读取方式 | 说明 | |--------|---------|---------|------| | 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')` | 非平台用户时注入租户模型实例 | ## ProxyMiddleware 详解 当项目部署在 CDN / 负载均衡 / Nginx 反向代理后面时,PHP 的 `REMOTE_ADDR` 是代理服务器的 IP(通常是 `127.0.0.1`),真实客户端 IP 由代理通过 `X-Forwarded-For` / `X-Real-IP` 请求头传递。 ### 工作原理 ```text 客户端(真实IP) → CDN/代理 → Nginx → PHP-FPM ↓ 设置头 X-Forwarded-For: 真实IP, 代理IP X-Real-IP: 真实IP ``` 1. 检查 `REMOTE_ADDR` 是否在受信代理列表(`.env` → `TRUSTED_PROXIES`,支持 CIDR 网段) 2. 如果是,从 `X-Real-IP` 或 `X-Forwarded-For` 头提取真实 IP 3. 覆盖 `$_SERVER['REMOTE_ADDR']`,后续 `request()->ip()` 与 `get_client_ip()` 返回真实 IP ### 配置 ```ini # .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 ```