Become a sponsor

概述
演示模式是系统内置的安全机制,用于公共演示环境的数据保护。开启后,DemoMiddleware 自动拦截所有写操作(POST/PUT/DELETE/PATCH),仅允许读操作(GET)通过,防止演示环境被随意修改。通过 #[DemoAllow] 注解可豁免特定接口。
┌─────────────────────────────────────────────────────────────────┐
│ 演示模式典型场景 │
│ │
│ ✅ 在线体验站:开放给潜在客户试用,但不允许修改数据 │
│ ✅ 培训环境:学员可以浏览所有功能,但不能增删改 │
│ ✅ 公开演示:展会/发布会现场演示,防止误操作 │
│ ✅ 安全评审:安全团队审查时只允许只读访问 │
│ │
│ ⚠️ 部分"安全的写操作"仍需放行(如刷新缓存、代码预览) │
│ → 通过 #[DemoAllow] 注解豁免 │
└─────────────────────────────────────────────────────────────────┘# .env
[APP]
# 开启演示模式(true=开启,false=关闭)
DEMO = true| 变量 | 默认值 | 说明 |
|---|---|---|
DEMO | false | 演示模式开关,true 开启拦截,false 正常运行 |
生产环境务必关闭
演示模式会拦截所有写操作,生产环境必须设为 false,否则用户无法执行任何增删改操作。
请求进入
│
▼
┌──────────────────────────┐
│ ① DEMO = false ? │──── 是 ──→ 直接放行(非演示环境)
└──────────┬───────────────┘
│ 否
▼
┌──────────────────────────┐
│ ② 请求方法 = GET ? │──── 是 ──→ 直接放行(读操作)
└──────────┬───────────────┘
│ 否(POST/PUT/DELETE/PATCH)
▼
┌──────────────────────────┐
│ ③ 标注 #[DemoAllow] ? │──── 是 ──→ 直接放行(白名单豁免)
└──────────┬───────────────┘
│ 否
▼
┌──────────────────────────┐
│ 返回 403 │
│ "演示环境,禁止操作" │
└──────────────────────────┘| 请求方法 | 行为 | 说明 |
|---|---|---|
| GET | ✅ 放行 | 读操作始终允许 |
| POST | ❌ 拦截 | 新增操作 |
| PUT | ❌ 拦截 | 修改操作 |
| DELETE | ❌ 拦截 | 删除操作 |
| PATCH | ❌ 拦截 | 部分修改操作 |
任意方法 + #[DemoAllow] | ✅ 放行 | 白名单豁免 |
DemoMiddleware 位于需认证路由组的中间件链中,执行顺序:
CorsMiddleware(全局)
→ AuthMiddleware(认证 + 权限)
→ TenantMiddleware(租户上下文)
→ DemoMiddleware(演示模式拦截) ← 本中间件
→ LogMiddleware(操作日志)
→ 控制器执行位置说明
DemoMiddleware 在 AuthMiddleware 和 TenantMiddleware 之后执行,意味着:
// 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);
}
}// app/attribute/DemoAllow.php
#[Attribute(Attribute::TARGET_METHOD)] // 仅支持方法级,不支持类级
class DemoAllow
{
// 无参数的标记注解,仅用于标识"允许在演示环境执行"
}// app/service/AttributeService.php
// 通过反射判断方法是否标注了 #[DemoAllow]
public static function hasDemoAllow(string $controllerClass, string $method): bool
{
$attributes = self::getMethodAttributes($controllerClass, $method);
return $attributes['demoAllow'] !== null;
}类名@方法名 为键查缓存,命中直接返回ReflectionMethod 获取方法上的所有 PHP 8 AttributeDemoAllow 类型并存入结果系统中已标注 #[DemoAllow] 的方法:
| 控制器 | 方法 | 说明 | 为什么放行 |
|---|---|---|---|
DictController | refreshCache | 刷新字典缓存 | 仅清除缓存,不修改业务数据 |
GeneratorController | preview | 代码生成预览 | 仅预览不写盘,展示生成器能力 |
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 响应时,需识别演示模式拦截并给出友好提示:
// 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>
import { useAppStore } from '@/store/modules/projectSetting';
const isDemoMode = computed(() => import.meta.env.VITE_DEMO_MODE === 'true');
</script>当演示模式拦截写操作时,返回:
{
"code": 403,
"msg": "演示环境,禁止操作",
"data": null
}HTTP 状态码为 403。
能。登录接口 /api/login 使用 POST 方法,但它位于公开接口组(不经过 DemoMiddleware),因此不受演示模式影响。
在控制器方法上标注 #[DemoAllow] 注解即可:
#[DemoAllow]
public function mySafeAction(): Json
{
// 演示模式下也可执行
}不会。两者是独立机制,按顺序执行:
两者的 403 含义不同,前端可通过 msg 字段区分。
取决于 .env 的缓存配置:
.env 后需清缓存或重启(php think clear)APP_DEBUG=true):每次请求重新读取 .env,无需重启#[DemoAllow] 支持类级标注吗? 不支持。DemoAllow 注解的 Attribute 目标仅为 TARGET_METHOD(方法级)。如果需要整个控制器放行,需在每个方法上单独标注。