Skip to content

5.7 演示模式 ​

概述

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

应用场景 ​

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

配置 ​

ini
# .env
[APP]
# 开启演示模式(true=开启,false=关闭)
DEMO = true
变量默认值说明
DEMOfalse演示模式开关,true 开启拦截,false 正常运行

生产环境务必关闭

演示模式会拦截所有写操作,生产环境必须设为 false,否则用户无法执行任何增删改操作。

拦截规则 ​

text
请求进入
  │
  ▼
┌──────────────────────────┐
│  ① DEMO = false ?        │──── 是 ──→ 直接放行(非演示环境)
└──────────┬───────────────┘
           │ 否
           ▼
┌──────────────────────────┐
│  ② 请求方法 = GET ?      │──── 是 ──→ 直接放行(读操作)
└──────────┬───────────────┘
           │ 否(POST/PUT/DELETE/PATCH)
           ▼
┌──────────────────────────┐
│  ③ 标注 #[DemoAllow] ?   │──── 是 ──→ 直接放行(白名单豁免)
└──────────┬───────────────┘
           │ 否
           ▼
┌──────────────────────────┐
│  返回 403                 │
│  "演示环境,禁止操作"      │
└──────────────────────────┘
请求方法行为说明
GET✅ 放行读操作始终允许
POST❌ 拦截新增操作
PUT❌ 拦截修改操作
DELETE❌ 拦截删除操作
PATCH❌ 拦截部分修改操作
任意方法 + #[DemoAllow]✅ 放行白名单豁免

中间件执行位置 ​

DemoMiddleware 位于需认证路由组的中间件链中,执行顺序:

text
CorsMiddleware(全局)
  → AuthMiddleware(认证 + 权限)
    → TenantMiddleware(租户上下文)
      → DemoMiddleware(演示模式拦截)  ← 本中间件
        → LogMiddleware(操作日志)
          → 控制器

执行位置说明

DemoMiddleware 在 AuthMiddleware 和 TenantMiddleware 之后执行,意味着:

  • 未登录用户已被 AuthMiddleware 拦截(401),不会到达演示模式判断
  • 租户状态已由 TenantMiddleware 校验,确保用户合法
  • 演示模式仅拦截"合法用户的写操作",不影响认证流程

核心代码 ​

DemoMiddleware ​

php
// app/middleware/DemoMiddleware.php

class DemoMiddleware
{
    public function handle(Request $request, \Closure $next): Response
    {
        // ① 非演示环境,直接放行
        if (!env('app_demo', false)) {
            return $next($request);
        }

        // ② GET 请求放行(读操作)
        $method = strtoupper($request->method());
        if ($method === 'GET') {
            return $next($request);
        }

        // ③ 检查是否有 #[DemoAllow] 注解
        $controllerClass = $request->controller();
        $action = $request->action();

        // 补全命名空间(ThinkPHP 控制器解析不带命名空间前缀)
        if (strpos($controllerClass, '\\') === false) {
            $controllerClass = 'app\\controller\\' . $controllerClass;
        }

        if (AttributeService::hasDemoAllow($controllerClass, $action)) {
            return $next($request);
        }

        // ④ 演示环境,拦截写操作
        return json([
            'code' => 403,
            'msg'  => '演示环境,禁止操作',
            'data' => null,
        ], 403);
    }
}

DemoAllow 注解 ​

php
// app/attribute/DemoAllow.php

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

注解读取(AttributeService) ​

php
// app/service/AttributeService.php

// 通过反射判断方法是否标注了 #[DemoAllow]
public static function hasDemoAllow(string $controllerClass, string $method): bool
{
    $attributes = self::getMethodAttributes($controllerClass, $method);
    return $attributes['demoAllow'] !== null;
}
注解解析流程
  1. 以 类名@方法名 为键查缓存,命中直接返回
  2. 通过 ReflectionMethod 获取方法上的所有 PHP 8 Attribute
  3. 遍历属性实例,匹配 DemoAllow 类型并存入结果
  4. 写入缓存,同一请求内不重复反射

白名单示例 ​

系统中已标注 #[DemoAllow] 的方法:

控制器方法说明为什么放行
DictControllerrefreshCache刷新字典缓存仅清除缓存,不修改业务数据
GeneratorControllerpreview代码生成预览仅预览不写盘,展示生成器能力

使用示例 ​

php
use app\attribute\DemoAllow;
use app\attribute\Permission;
use app\attribute\Log;

class DictController extends BaseController
{
    /**
     * 刷新字典缓存
     *
     * 演示环境下也可执行:清除缓存不影响业务数据,
     * 且是展示字典功能的必要操作。
     */
    #[Log('字典管理-刷新缓存', Log::TYPE_CLEAR)]
    #[Permission('sys:dict:update', '修改字典')]
    #[DemoAllow]  // ← 演示模式白名单
    public function refreshCache(): Json
    {
        DictService::clearAllCache();
        return $this->success(null, '缓存刷新成功');
    }
}

注意事项

  • #[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
<template>
  <!-- 演示模式下禁用删除按钮 -->
  <el-button
    type="danger"
    :disabled="isDemoMode"
    @click="handleDelete"
  >
    删除
  </el-button>
</template>

<script setup>
import { useAppStore } from '@/store/modules/projectSetting';

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

响应示例 ​

当演示模式拦截写操作时,返回:

json
{
    "code": 403,
    "msg": "演示环境,禁止操作",
    "data": null
}

HTTP 状态码为 403。

常见问题 ​

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

能。登录接口 /api/login 使用 POST 方法,但它位于公开接口组(不经过 DemoMiddleware),因此不受演示模式影响。

Q: 如何让某个 POST 接口在演示模式下也能用? ​

在控制器方法上标注 #[DemoAllow] 注解即可:

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

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

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

  1. AuthMiddleware 先校验权限(无权限返回 403 "无访问权限")
  2. DemoMiddleware 再判断演示模式(拦截写操作返回 403 "演示环境,禁止操作")

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

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

取决于 .env 的缓存配置:

  • 文件缓存:修改 .env 后需清缓存或重启(php think clear)
  • Redis 缓存:同上
  • 开发环境(APP_DEBUG=true):每次请求重新读取 .env,无需重启

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

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

小蚂蚁云团队 · 提供技术支持