# 5.7 演示模式 :::tip 概述 演示模式是系统内置的安全机制,用于公共演示环境的数据保护。开启后,`DemoMiddleware` 自动拦截所有写操作(POST/PUT/DELETE/PATCH),仅允许读操作(GET)通过,防止演示环境被随意修改。通过 `#[DemoAllow]` 注解可豁免特定接口。 ::: ## 应用场景 ```text ┌─────────────────────────────────────────────────────────────────┐ │ 演示模式典型场景 │ │ │ │ ✅ 在线体验站:开放给潜在客户试用,但不允许修改数据 │ │ ✅ 培训环境:学员可以浏览所有功能,但不能增删改 │ │ ✅ 公开演示:展会/发布会现场演示,防止误操作 │ │ ✅ 安全评审:安全团队审查时只允许只读访问 │ │ │ │ ⚠️ 部分"安全的写操作"仍需放行(如刷新缓存、代码预览) │ │ → 通过 #[DemoAllow] 注解豁免 │ └─────────────────────────────────────────────────────────────────┘ ``` ## 配置 ```ini # .env # 开启演示模式(true=开启,false=关闭) DEMO_MODE=true ``` | 变量 | 默认值 | 读取位置 | 说明 | |------|--------|---------|------| | `DEMO_MODE` | `false` | `config('app.demo_mode')` → DemoMiddleware | 演示模式开关,`true` 开启拦截,`false` 正常运行 | :::warning 生产环境务必关闭 演示模式会拦截所有写操作,生产环境必须保持 `DEMO_MODE=false`,否则用户无法执行任何增删改操作。 ::: ## 启用与验证 演示模式生效需**同时满足两个条件**,缺一不可: 1. **配置接入**:`config/app.php` 定义 `demo_mode` 键(读取 `DEMO_MODE` 环境变量); 2. **中间件挂载**:`demo` 中间件已挂载到目标路由组(见[中间件挂载与执行顺序](#中间件挂载与执行顺序))。 启用步骤: ```bash # ① .env 开启 DEMO_MODE=true # ② 清除配置缓存(如执行过 config:cache,改完需重新缓存) php artisan config:clear ``` 逐项验证: ```bash # ① 配置键已接入(期望输出 true) php artisan tinker --execute="dump(config('app.demo_mode'))" # ② 中间件已挂载到认证路由组(demo 位于 tenant 与 log.operation 之间) php artisan route:list --path=user/add --json # → "middleware": ["api", "auth.jwt", "tenant", "demo", "log.operation"] # ③ 拦截生效(携带有效 token 发起写操作,期望 HTTP 403) curl -X POST http://localhost:8000/api/user/add \ -H "Authorization: Bearer {access_token}" \ -H "Accept: application/json" \ -d '{"username":"demo"}' # → {"code":403,"ok":false,"msg":"演示模式下禁止操作","data":null} ``` :::warning 两层缺一不可 仅设置 `DEMO_MODE=true` 不代表生效: - 若 `config/app.php` 缺少 `demo_mode` 键,`config('app.demo_mode')` 永远返回默认值 `false`,中间件直接放行; - 若 `demo` 中间件未挂载到路由组,`DemoMiddleware::handle()` 根本不会执行。 启用后请按上述清单逐项验证,不要只改 `.env`。 ::: ## 拦截规则 ```text 请求进入 │ ▼ ┌──────────────────────────────┐ │ ① demo_mode = false ? │──── 是 ──→ 直接放行(非演示环境) └──────────┬───────────────────┘ │ 否 ▼ ┌──────────────────────────────┐ │ ② 请求方法 = GET ? │──── 是 ──→ 直接放行(读操作) └──────────┬───────────────────┘ │ 否(POST/PUT/DELETE/PATCH) ▼ ┌──────────────────────────────┐ │ ③ 标注 #[DemoAllow] ? │──── 是 ──→ 直接放行(白名单豁免) │ (ReflectionMethod 反射读取, │ │ 反射异常静默视为未标注) │ └──────────┬───────────────────┘ │ 否 ▼ ┌──────────────────────────────┐ │ HTTP 403 │ │ {"code":403, │ │ "msg":"演示模式下禁止操作"} │ └──────────────────────────────┘ ``` | 请求方法 | 行为 | 说明 | |---------|------|------| | GET | ✅ 放行 | 读操作始终允许 | | POST | ❌ 拦截 | 新增操作 | | PUT | ❌ 拦截 | 修改操作 | | DELETE | ❌ 拦截 | 删除操作 | | PATCH | ❌ 拦截 | 部分修改操作 | | 任意方法 + `#[DemoAllow]` | ✅ 放行 | 白名单豁免 | ## 中间件挂载与执行顺序 `DemoMiddleware` 以别名 `demo` 注册于 `bootstrap/app.php`,并挂载在认证路由组上(`routes/api.php`): ```php // bootstrap/app.php —— 别名注册 $middleware->alias([ // ... 'demo' => DemoMiddleware::class, ]); // routes/api.php —— 认证路由组 // demo 位于 tenant 之后、log.operation 之前 Route::middleware(['auth.jwt', 'tenant', 'demo', 'log.operation']) ->group(function () { /* ... */ }); ``` ```text 挂载后的执行顺序(认证路由组内): ProxyMiddleware(全局) → Cors(全局) → ForceJsonResponse(全局) → Authenticate(认证 + 权限) → ResolveTenant(租户上下文) → DemoMiddleware(演示模式拦截) ← 本中间件 → LogOperation(操作日志) → 控制器 ``` :::tip 执行位置说明 DemoMiddleware 设计在 `Authenticate` 和 `ResolveTenant` 之后执行,意味着: - 未登录用户已被 `Authenticate` 拦截(401),不会到达演示模式判断; - 租户状态已由 `ResolveTenant` 校验,确保用户合法; - **位于 `LogOperation` 之前**:被演示模式拦截(403)的请求不再写入操作日志; - 演示模式仅拦截"合法用户的写操作",不影响认证流程。 登录、验证码、令牌刷新等公开接口定义在路由组外,不经过 `demo` 中间件。 ::: ## 核心代码 ### DemoMiddleware ```php // app/Http/Middleware/DemoMiddleware.php(节选) class DemoMiddleware { public function handle(Request $request, Closure $next): Response { // ① 非演示模式直接放行 if (! config('app.demo_mode', false)) { return $next($request); } // (后续:GET 放行 → 反射 #[DemoAllow] 放行 → 否则 403) // 注解读取通过反射完成,反射异常时静默处理,视为未标注; // 未放行的写操作统一返回 403,响应结构与 Result::fail() 一致。 } } ``` ### DemoAllow 注解 ```php // app/Attributes/DemoAllow.php #[Attribute(Attribute::TARGET_METHOD)] // 仅支持方法级,不支持类级 class DemoAllow { // 无参数的标记注解,仅用于标识"允许在演示环境执行" } ``` ### 注解读取(中间件内反射) ```php // DemoMiddleware 通过 ReflectionMethod 读取方法上的 #[DemoAllow] $controllerClass = $route->getControllerClass(); $method = $route->getActionMethod(); $reflection = new \ReflectionMethod($controllerClass, $method); $attrs = $reflection->getAttributes(DemoAllow::class); if (! empty($attrs)) { return $next($request); // 白名单放行 } ``` :::tip 反射性能 注解读取开销为微秒级(与 `Authenticate`、`LogOperation` 一致),不做缓存——避免开发阶段改注解后需等缓存过期。 ::: ## 白名单示例 当前代码中标注 `#[DemoAllow]` 的方法: | 控制器 | 方法 | 路由 | 说明 | 放行理由 | |--------|------|------|------|---------| | `GeneratorController` | `preview` | `GET generator/preview` | 代码生成预览 | 仅预览不写盘,不修改业务数据 | | `AuthController` | `logout` | `POST logout` | 用户登出 | 吊销当前令牌并写登录日志(`login_log`),不修改业务数据;演示环境必须允许退出 | :::tip GET 路由天然放行 `dict/refreshCache` 等维护接口均为 GET 方法,本就不受拦截、无需注解。`#[DemoAllow]` 的价值在于**防御性兜底**——即使后续将路由方法改为 POST,演示模式下仍可正常访问。 ::: ### 使用示例 ```php use App\Attributes\DemoAllow; use App\Attributes\OperationLog; use App\Attributes\Permission; class GeneratorController extends Controller { /** * 预览生成代码 * * 标注 #[DemoAllow],演示环境下允许访问。 * 根据请求参数预览待生成的代码内容,不写入文件。 */ #[OperationLog('代码生成-预览代码', OperationLog::TYPE_QUERY)] #[Permission('sys:generator:generate', '代码生成')] #[DemoAllow] // ← 演示模式白名单 public function preview(Request $request): JsonResponse { $result = $this->logic->preview($request->all()); return $this->success($result); } } ``` :::warning 注意事项 - `#[DemoAllow]` 仅支持方法级标注,不支持类级(与 `#[Permission]` 相同为方法级) - 仅对"安全的写操作"使用,如刷新缓存、预览、导出等 - 不要对增删改数据的接口使用,否则失去演示模式的保护意义 ::: ## 前端适配 前端在收到 403 响应时,需识别演示模式拦截并给出友好提示: ```typescript // HTTP 响应拦截器 axios.interceptors.response.use(response => { const { code, msg } = response.data; // 演示模式拦截 if (code === 403 && msg === '演示模式下禁止操作') { ElMessage.warning('当前为演示环境,不允许执行此操作'); return Promise.reject(response.data); } return response; }); ``` 前端可根据需要,在演示模式下隐藏或禁用写操作按钮: ```vue ``` ## 响应示例 当演示模式拦截写操作时,返回(HTTP 状态码 403): ```json { "code": 403, "ok": false, "msg": "演示模式下禁止操作", "data": null } ``` ## 常见问题 ### Q: 演示模式下登录还能用吗? 能。登录接口 `/api/login` 位于**公开接口组**(中间件组外),不经过 `demo` 中间件,因此不受演示模式影响。 ### Q: 设置 DEMO_MODE=true 后仍能正常增删改? 演示模式生效需同时满足**配置接入**与**中间件挂载**两个条件,逐项排查(详见[启用与验证](#启用与验证)): ```bash php artisan tinker --execute="dump(config('app.demo_mode'))" # 期望 true php artisan route:list --path=user/add --json # middleware 应含 demo ``` 若配置键缺失或中间件未挂载,改 `.env` 不会有任何效果。另外注意:执行过 `config:cache` 时,改完 `.env` 需重新缓存。 ### Q: 如何让某个 POST 接口在演示模式下也能用? 在控制器方法上标注 `#[DemoAllow]` 注解即可: ```php #[DemoAllow] public function mySafeAction(): JsonResponse { // 演示模式下也可执行 } ``` ### Q: 演示模式和权限控制会冲突吗? 不会。两者是独立机制,按顺序执行: 1. **Authenticate** 先校验权限(无权限返回 403 "无访问权限:{权限码}") 2. **DemoMiddleware** 再判断演示模式(拦截写操作返回 403 "演示模式下禁止操作") 两者的 403 含义不同,前端可通过 `msg` 字段区分。 ### Q: 临时关闭演示模式需要重启服务吗? 修改 `.env` 后需清除配置缓存并重启: ```bash php artisan config:clear # 生产环境如执行过 config:cache,改完需重新缓存 php artisan config:cache ``` ### Q: `#[DemoAllow]` 支持类级标注吗? 不支持。`DemoAllow` 注解的 `Attribute` 目标仅为 `TARGET_METHOD`(方法级)。如果需要整个控制器放行,需在每个方法上单独标注。