签入版本
This commit is contained in:
@@ -0,0 +1,303 @@
|
||||
# 5.7 演示模式
|
||||
|
||||
:::tip 概述
|
||||
演示模式是系统内置的安全机制,用于公共演示环境的数据保护。开启后,DemoMiddleware 自动拦截所有写操作(POST/PUT/DELETE/PATCH),仅允许读操作(GET)通过,防止演示环境被随意修改。通过 `#[DemoAllow]` 注解可豁免特定接口。
|
||||
:::
|
||||
|
||||
## 应用场景
|
||||
|
||||
```text
|
||||
┌─────────────────────────────────────────────────────────────────┐
|
||||
│ 演示模式典型场景 │
|
||||
│ │
|
||||
│ ✅ 在线体验站:开放给潜在客户试用,但不允许修改数据 │
|
||||
│ ✅ 培训环境:学员可以浏览所有功能,但不能增删改 │
|
||||
│ ✅ 公开演示:展会/发布会现场演示,防止误操作 │
|
||||
│ ✅ 安全评审:安全团队审查时只允许只读访问 │
|
||||
│ │
|
||||
│ ⚠️ 部分"安全的写操作"仍需放行(如刷新缓存、代码预览) │
|
||||
│ → 通过 #[DemoAllow] 注解豁免 │
|
||||
└─────────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## 配置
|
||||
|
||||
```ini
|
||||
# .env
|
||||
[APP]
|
||||
# 开启演示模式(true=开启,false=关闭)
|
||||
DEMO = true
|
||||
```
|
||||
|
||||
| 变量 | 默认值 | 说明 |
|
||||
|------|--------|------|
|
||||
| `DEMO` | `false` | 演示模式开关,`true` 开启拦截,`false` 正常运行 |
|
||||
|
||||
:::warning 生产环境务必关闭
|
||||
演示模式会拦截所有写操作,生产环境必须设为 `false`,否则用户无法执行任何增删改操作。
|
||||
:::
|
||||
|
||||
## 拦截规则
|
||||
|
||||
```text
|
||||
请求进入
|
||||
│
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ ① DEMO = false ? │──── 是 ──→ 直接放行(非演示环境)
|
||||
└──────────┬───────────────┘
|
||||
│ 否
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ ② 请求方法 = GET ? │──── 是 ──→ 直接放行(读操作)
|
||||
└──────────┬───────────────┘
|
||||
│ 否(POST/PUT/DELETE/PATCH)
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ ③ 标注 #[DemoAllow] ? │──── 是 ──→ 直接放行(白名单豁免)
|
||||
└──────────┬───────────────┘
|
||||
│ 否
|
||||
▼
|
||||
┌──────────────────────────┐
|
||||
│ 返回 403 │
|
||||
│ "演示环境,禁止操作" │
|
||||
└──────────────────────────┘
|
||||
```
|
||||
|
||||
| 请求方法 | 行为 | 说明 |
|
||||
|---------|------|------|
|
||||
| GET | ✅ 放行 | 读操作始终允许 |
|
||||
| POST | ❌ 拦截 | 新增操作 |
|
||||
| PUT | ❌ 拦截 | 修改操作 |
|
||||
| DELETE | ❌ 拦截 | 删除操作 |
|
||||
| PATCH | ❌ 拦截 | 部分修改操作 |
|
||||
| 任意方法 + `#[DemoAllow]` | ✅ 放行 | 白名单豁免 |
|
||||
|
||||
## 中间件执行位置
|
||||
|
||||
DemoMiddleware 位于需认证路由组的中间件链中,执行顺序:
|
||||
|
||||
```text
|
||||
CorsMiddleware(全局)
|
||||
→ AuthMiddleware(认证 + 权限)
|
||||
→ TenantMiddleware(租户上下文)
|
||||
→ DemoMiddleware(演示模式拦截) ← 本中间件
|
||||
→ LogMiddleware(操作日志)
|
||||
→ 控制器
|
||||
```
|
||||
|
||||
:::tip 执行位置说明
|
||||
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;
|
||||
}
|
||||
```
|
||||
|
||||
:::details 注解解析流程
|
||||
1. 以 `类名@方法名` 为键查缓存,命中直接返回
|
||||
2. 通过 `ReflectionMethod` 获取方法上的所有 PHP 8 Attribute
|
||||
3. 遍历属性实例,匹配 `DemoAllow` 类型并存入结果
|
||||
4. 写入缓存,同一请求内不重复反射
|
||||
:::
|
||||
|
||||
## 白名单示例
|
||||
|
||||
系统中已标注 `#[DemoAllow]` 的方法:
|
||||
|
||||
| 控制器 | 方法 | 说明 | 为什么放行 |
|
||||
|--------|------|------|-----------|
|
||||
| `DictController` | `refreshCache` | 刷新字典缓存 | 仅清除缓存,不修改业务数据 |
|
||||
| `GeneratorController` | `preview` | 代码生成预览 | 仅预览不写盘,展示生成器能力 |
|
||||
|
||||
### 使用示例
|
||||
|
||||
```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, '缓存刷新成功');
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
:::warning 注意事项
|
||||
- `#[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`(方法级)。如果需要整个控制器放行,需在每个方法上单独标注。
|
||||
Reference in New Issue
Block a user