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