Files
rxthinkcmf-wiki/RXThinkCMF_Laravel13_EleVue/zh/core/demo-mode.md
T
2026-10-09 10:50:38 +08:00

13 KiB
Raw Blame History

5.7 演示模式

:::tip 概述 演示模式是系统内置的安全机制,用于公共演示环境的数据保护。开启后,DemoMiddleware 自动拦截所有写操作(POST/PUT/DELETE/PATCH),仅允许读操作(GET)通过,防止演示环境被随意修改。通过 #[DemoAllow] 注解可豁免特定接口。 :::

应用场景

┌─────────────────────────────────────────────────────────────────┐
│                     演示模式典型场景                              │
│                                                                 │
│  ✅ 在线体验站:开放给潜在客户试用,但不允许修改数据              │
│  ✅ 培训环境:学员可以浏览所有功能,但不能增删改                  │
│  ✅ 公开演示:展会/发布会现场演示,防止误操作                     │
│  ✅ 安全评审:安全团队审查时只允许只读访问                        │
│                                                                 │
│  ⚠️ 部分"安全的写操作"仍需放行(如刷新缓存、代码预览)            │
│     → 通过 #[DemoAllow] 注解豁免                                │
└─────────────────────────────────────────────────────────────────┘

配置

# .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 中间件已挂载到目标路由组(见中间件挂载与执行顺序)。

启用步骤:

# ① .env 开启
DEMO_MODE=true

# ② 清除配置缓存(如执行过 config:cache,改完需重新缓存)
php artisan config:clear

逐项验证:

# ① 配置键已接入(期望输出 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。 :::

拦截规则

请求进入
  │
  ▼
┌──────────────────────────────┐
│  ① 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):

// 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 () { /* ... */ });
挂载后的执行顺序(认证路由组内):

ProxyMiddleware(全局)
  → Cors(全局)
    → ForceJsonResponse(全局)
      → Authenticate(认证 + 权限)
        → ResolveTenant(租户上下文)
          → DemoMiddleware(演示模式拦截)  ← 本中间件
            → LogOperation(操作日志)
              → 控制器

:::tip 执行位置说明 DemoMiddleware 设计在 Authenticate 和 ResolveTenant 之后执行,意味着:

  • 未登录用户已被 Authenticate 拦截(401),不会到达演示模式判断;
  • 租户状态已由 ResolveTenant 校验,确保用户合法;
  • 位于 LogOperation 之前:被演示模式拦截(403)的请求不再写入操作日志;
  • 演示模式仅拦截"合法用户的写操作",不影响认证流程。

登录、验证码、令牌刷新等公开接口定义在路由组外,不经过 demo 中间件。 :::

核心代码

DemoMiddleware

// 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 注解

// app/Attributes/DemoAllow.php

#[Attribute(Attribute::TARGET_METHOD)]  // 仅支持方法级,不支持类级
class DemoAllow
{
    // 无参数的标记注解,仅用于标识"允许在演示环境执行"
}

注解读取(中间件内反射)

// 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,演示模式下仍可正常访问。 :::

使用示例

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 响应时,需识别演示模式拦截并给出友好提示:

// HTTP 响应拦截器
axios.interceptors.response.use(response => {
  const { code, msg } = response.data;

  // 演示模式拦截
  if (code === 403 && msg === '演示模式下禁止操作') {
    ElMessage.warning('当前为演示环境,不允许执行此操作');
    return Promise.reject(response.data);
  }

  return response;
});

前端可根据需要,在演示模式下隐藏或禁用写操作按钮:

<template>
  <!-- 演示模式下禁用删除按钮 -->
  <el-button
    type="danger"
    :disabled="isDemoMode"
    @click="handleDelete"
  >
    删除
  </el-button>
</template>

<script setup>
const isDemoMode = computed(() => import.meta.env.VITE_DEMO_MODE === 'true');
</script>

响应示例

当演示模式拦截写操作时,返回(HTTP 状态码 403):

{
    "code": 403,
    "ok": false,
    "msg": "演示模式下禁止操作",
    "data": null
}

常见问题

Q: 演示模式下登录还能用吗?

能。登录接口 /api/login 位于公开接口组(中间件组外),不经过 demo 中间件,因此不受演示模式影响。

Q: 设置 DEMO_MODE=true 后仍能正常增删改?

演示模式生效需同时满足配置接入与中间件挂载两个条件,逐项排查(详见启用与验证):

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] 注解即可:

#[DemoAllow]
public function mySafeAction(): JsonResponse
{
    // 演示模式下也可执行
}

Q: 演示模式和权限控制会冲突吗?

不会。两者是独立机制,按顺序执行:

  1. Authenticate 先校验权限(无权限返回 403 "无访问权限:{权限码}")
  2. DemoMiddleware 再判断演示模式(拦截写操作返回 403 "演示模式下禁止操作")

两者的 403 含义不同,前端可通过 msg 字段区分。

Q: 临时关闭演示模式需要重启服务吗?

修改 .env 后需清除配置缓存并重启:

php artisan config:clear
# 生产环境如执行过 config:cache,改完需重新缓存
php artisan config:cache

Q: #[DemoAllow] 支持类级标注吗?

不支持。DemoAllow 注解的 Attribute 目标仅为 TARGET_METHOD(方法级)。如果需要整个控制器放行,需在每个方法上单独标注。