13 KiB
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,否则用户无法执行任何增删改操作。
:::
启用与验证
演示模式生效需同时满足两个条件,缺一不可:
- 配置接入:
config/app.php定义demo_mode键(读取DEMO_MODE环境变量); - 中间件挂载:
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: 演示模式和权限控制会冲突吗?
不会。两者是独立机制,按顺序执行:
- Authenticate 先校验权限(无权限返回 403 "无访问权限:{权限码}")
- DemoMiddleware 再判断演示模式(拦截写操作返回 403 "演示模式下禁止操作")
两者的 403 含义不同,前端可通过 msg 字段区分。
Q: 临时关闭演示模式需要重启服务吗?
修改 .env 后需清除配置缓存并重启:
php artisan config:clear
# 生产环境如执行过 config:cache,改完需重新缓存
php artisan config:cache
Q: #[DemoAllow] 支持类级标注吗?
不支持。DemoAllow 注解的 Attribute 目标仅为 TARGET_METHOD(方法级)。如果需要整个控制器放行,需在每个方法上单独标注。