Files
MrleeAdmin/doc/api-encryption-design.md
T
2026-08-31 11:33:59 +08:00

1063 lines
21 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 接口加密方案设计(适配 MrleeAdmin / Madong 项目)
## 1. 文档目标
本文档用于为当前项目设计一套**符合现有架构、可渐进落地、支持前后台统一接入**的接口加密方案。
设计目标:
- 在**不破坏现有接口返回规范**的前提下,为 API 增加可配置的请求/响应加密能力。
- 同时适配:
- `backend`(Webman / PHP)
- `frontend/admin`(Vue 3 + Axios)
- `frontend/web`(Nuxt 4 + fetch 封装)
- 支持:
- **关闭加密**
- **部分加密**
- **全局加密**
- 支持后续扩展:
- 密钥轮换
- 按模块排除
- 注解/Attribute 标记接口是否加密
- 管理端可视化配置页面
本文档是**总体方案设计文档**,不是直接实现文档;但所有设计都以当前项目真实结构为前提,可直接指导后续开发。
---
## 2. 当前项目现状结论
### 2.1 当前接口未做统一加密
经核实,当前项目主链路没有统一接口加密能力。
现状如下:
- 后端统一返回封装 `backend/core/tool/Json.php`
- 直接输出标准 JSON:`code / msg / data`
- 没有 AES / RSA / 自定义密文包装逻辑
- 前台 Web 请求封装 `frontend/web/app/utils/request.ts`
- 直接 `response.json()`
- 按 `code / msg / data` 处理
- 没有解密逻辑
- Admin 请求封装 `frontend/admin/src/utils/http/index.ts`
- `transformResponse` 直接 `JSON.parse`
- 响应拦截器直接读取 `response.data.code / msg / data`
- 没有解密逻辑
### 2.2 当前项目没有代码层强制 HTTPS
经核实,当前仓库代码没有发现全局强制 HTTPS 的逻辑:
- 没有发现统一 `forceScheme('https')`
- 没有发现 HTTP -> HTTPS 跳转中间件
- 前端 `.env` 使用相对 API 路径:
- `frontend/admin/.env.production` 中 `VITE_API_URL=/adminapi`
- `frontend/web/.env` 中 `VITE_APP_BASE_URL=/api`
- `backend/config/session.php` 中 `secure => false`
结论:
- 当前生产环境是否使用 HTTPS,取决于 **Nginx / 反向代理 / 宝塔站点 / CDN** 等部署层配置。
- 接口加密方案**不能替代 HTTPS**,建议生产环境必须启用 HTTPS。
---
## 3. 设计原则
### 3.1 不使用“前端持有私钥”方案
必须明确:
- **RSA 私钥不能下发到前端**
- 前端只能持有 **RSA 公钥**
- 后端持有 **RSA 私钥**
如果将私钥配置到浏览器侧,则任何接口加密都失去意义。
### 3.2 采用 RSA + AES 混合加密
不直接使用 RSA 加密整个请求/响应包体,原因:
- RSA 性能较差
- RSA 明文长度有限制
- JSON 包体、分页数据、列表数据通常不适合直接用 RSA 全量加密
采用混合加密:
- **请求体 / 响应体**:使用 AES 加密
- **AES 会话密钥**:使用 RSA 公钥加密传输
- **AES 解密密钥**:后端使用 RSA 私钥解出
这是兼顾安全性、可用性、性能和扩展性的主流方案。
### 3.3 保持现有响应协议语义不变
当前项目大量前后端逻辑依赖如下结构:
```json
{
"code": 0,
"msg": "获取成功",
"data": {}
}
```
加密改造后,必须尽量保持**业务语义兼容**,避免影响错误处理、Token 刷新、UI 消息提示、分页通用逻辑。
### 3.4 先支持 JSON 接口,再扩展其他流类型
第一阶段仅支持:
- `application/json`
- 普通 GET / POST / PUT / DELETE 业务接口
第一阶段不直接纳入:
- 文件上传 `multipart/form-data`
- 文件下载 `blob / stream`
- SSE / 长连接
- 第三方回调接口
- Swagger / OpenAPI 文档接口
这些接口应通过**排除策略**先保持明文。
### 3.5 渐进落地,先可切换后全量接入
建议分阶段:
- 第一阶段:支持开关和部分加密
- 第二阶段:支持全局加密
- 第三阶段:补齐密钥轮换、排除规则、监控、告警
---
## 4. 目标能力范围
方案最终应支持以下能力:
### 4.1 加密开关
- 关闭
- 部分加密
- 全局加密
### 4.2 加密方向
- 请求体加密
- 响应体加密
说明:
- 只做响应加密意义有限
- 推荐请求与响应一起做
### 4.3 前后端统一接入
- Admin 端统一请求封装接入
- Web 端统一请求封装接入
- 后端统一请求解密中间件 + 响应加密中间件接入
### 4.4 接口排除与例外能力
需要支持排除:
- 登录前置公钥获取接口
- Token 刷新接口
- 文件上传/下载接口
- SSE 接口
- 第三方回调接口
- OpenAPI / Swagger 文档接口
### 4.5 密钥管理能力
- RSA 密钥对生成
- RSA 公钥发布
- RSA 私钥仅服务端存储
- 支持重新生成密钥
- 后续支持密钥版本号和轮换
---
## 5. 总体架构设计
### 5.1 核心流程
#### 请求流程
1. 前端生成一次性 AES Key + IV
2. 前端使用 AES 加密业务请求数据
3. 前端使用 RSA 公钥加密 AES Key(必要时也包含 IV)
4. 前端将密文提交给后端
5. 后端使用 RSA 私钥解出 AES Key
6. 后端使用 AES 解密业务数据
7. 后端将解密结果重新写回请求上下文,供控制器正常读取
#### 响应流程
1. 控制器照常返回 `Json::success()` / `Json::fail()`
2. 响应加密中间件判断当前接口是否需要加密
3. 将原始 JSON 响应体用 AES 加密
4. 按统一密文结构返回前端
5. 前端响应拦截器识别密文结构并解密
6. 还原为原始 `code / msg / data` 结构,再继续走现有业务流程
### 5.2 推荐密文协议结构
#### 请求体结构
```json
{
"encrypted": true,
"algorithm": "rsa-aes",
"key_version": "v1",
"key": "<RSA加密后的AES密钥>",
"iv": "<Base64 IV 或并入 key 元数据>",
"data": "<AES加密后的业务密文>",
"timestamp": 1720000000,
"nonce": "random-string"
}
```
#### 响应体结构
```json
{
"encrypted": true,
"algorithm": "rsa-aes",
"key_version": "v1",
"data": "<AES加密后的响应密文>"
}
```
说明:
- 如果请求与响应共用同一个 AES Key,则响应体中可以不再返回 `key`
- 如果设计为响应重新生成一次性响应 AES Key,则响应体还要额外返回新的加密 key
- 对本项目第一阶段,建议:
- **响应复用请求里的 AES Key**
- 可降低协议复杂度
---
## 6. 加密算法建议
### 6.1 RSA 方案
建议:
- `RSA 2048` 起步
- 填充模式建议:`OAEP`
- 密钥格式:PEM
用途:
- 仅用于加密 AES 会话密钥
- 不直接加密大包体 JSON
### 6.2 AES 方案
建议:
- `AES-256-CBC` 或 `AES-256-GCM`
建议优先级:
- 如果当前 PHP / 前端依赖兼容良好,优先 `AES-256-GCM`
- 如果需要兼顾实现稳定性与现有库接入复杂度,可先落地 `AES-256-CBC`
说明:
- `GCM` 更现代,带认证标签
- `CBC` 更容易快速落地,但需要额外做好完整性校验
### 6.3 编码格式
建议统一:
- 二进制密文转 `Base64`
- JSON 结构继续使用标准 JSON
---
## 7. 本项目推荐的实现分层
### 7.1 后端分层设计
建议新增以下模块:
#### 1. 配置层
建议新增:
- `backend/config/api_encrypt.php`
用途:
- 全局开关
- 加密范围
- 排除规则
- 密钥读取配置
- 算法配置
#### 2. 密钥服务层
建议新增:
- `backend/core/security/ApiEncrypt/KeyManager.php`
- `backend/core/security/ApiEncrypt/CryptoService.php`
职责:
- 读取公钥/私钥
- 生成密钥对
- RSA 加解密
- AES 加解密
#### 3. 请求解密中间件
建议新增:
- `backend/app/middleware/DecryptApiRequestMiddleware.php`
职责:
- 判断当前请求是否需要解密
- 解密请求体
- 将解密后的 JSON 数据重新注入 Request
- 解密失败时统一返回错误响应
#### 4. 响应加密中间件
建议新增:
- `backend/app/middleware/EncryptApiResponseMiddleware.php`
职责:
- 判断当前响应是否需要加密
- 对 `Json::success / Json::fail` 的响应体进行加密包装
- 排除文件流、SSE、下载等响应类型
#### 5. 注解 / Attribute
建议新增:
- `backend/core/attribute/EncryptResponse.php`
- `backend/core/attribute/SkipApiEncrypt.php`
职责:
- 部分加密模式下,用于标记哪些接口需要加密
- 某些接口在全局加密时仍需排除
### 7.2 前端分层设计
#### Admin 端
接入点:
- `frontend/admin/src/utils/http/index.ts`
建议新增:
- `frontend/admin/src/utils/security/api-encrypt.ts`
职责:
- 获取公钥
- 生成 AES Key
- 请求加密
- 响应解密
- 根据配置判断是否对当前请求启用加密
#### Web 端
接入点:
- `frontend/web/app/utils/request.ts`
建议新增:
- `frontend/web/app/utils/security/api-encrypt.ts`
职责同上。
#### 共通原则
- 不直接把加密逻辑散落在业务页面
- 统一在请求封装层处理
- 页面和业务 API 文件无感知
---
## 8. 配置设计
### 8.1 后端全局配置文件
建议新增:
`backend/config/api_encrypt.php`
建议结构:
```php
return [
'enabled' => env('API_ENCRYPT_ENABLED', false),
'scope' => env('API_ENCRYPT_SCOPE', 'off'), // off | partial | all
'algorithm' => env('API_ENCRYPT_ALGORITHM', 'rsa-aes'),
'rsa_public_key_path' => env('API_ENCRYPT_RSA_PUBLIC_KEY_PATH', base_path('runtime/keys/api_encrypt_public.pem')),
'rsa_private_key_path' => env('API_ENCRYPT_RSA_PRIVATE_KEY_PATH', base_path('runtime/keys/api_encrypt_private.pem')),
'aes_mode' => env('API_ENCRYPT_AES_MODE', 'AES-256-CBC'),
'exclude_paths' => [
'/auth/public-key',
'/auth/refresh',
'/upload',
'/swagger',
],
];
```
### 8.2 环境变量设计
建议新增:
```env
API_ENCRYPT_ENABLED=false
API_ENCRYPT_SCOPE=off
API_ENCRYPT_ALGORITHM=rsa-aes
API_ENCRYPT_AES_MODE=AES-256-CBC
API_ENCRYPT_RSA_PUBLIC_KEY_PATH=runtime/keys/api_encrypt_public.pem
API_ENCRYPT_RSA_PRIVATE_KEY_PATH=runtime/keys/api_encrypt_private.pem
```
### 8.3 动态配置设计
如果后续要做成你截图里的后台配置页,建议将以下配置支持数据库存储:
- 是否启用接口加密
- 加密范围:`partial / all`
- 当前密钥版本号
- RSA 公钥展示
- RSA 私钥只允许后端存储,不在前端展示原文
说明:
- 如果需要“重新生成密钥”,由后端管理命令或管理接口生成
- 页面只触发生成动作,不直接在浏览器生成私钥
---
## 9. 加密范围设计
### 9.1 模式一:关闭
- 所有接口明文传输
- 保持当前项目行为
### 9.2 模式二:部分加密
只加密满足以下条件的接口:
- 控制器方法标记了 `#[EncryptResponse]`
- 或者请求经过明确的加密白名单匹配
适用场景:
- 登录
- 用户资料
- 支付前置接口
- 涉及敏感个人信息的查询与更新
### 9.3 模式三:全局加密
默认所有 JSON API 都加密,以下接口默认排除:
- 公钥获取接口
- 登录前预检接口
- Token 刷新
- 文件上传
- 文件下载
- SSE
- Swagger/OpenAPI 文档接口
- 第三方回调
---
## 10. 必须排除的接口类型
### 10.1 公钥获取接口
需要提供一个**明文可访问**的公钥接口,例如:
- `GET /system/security/api-encrypt/public-key`
返回:
- `key_version`
- `public_key`
- `enabled`
- `scope`
该接口必须明文,否则前端无法开始第一次加密通信。
### 10.2 Token 刷新接口
建议第一阶段排除:
- `/auth/refresh`
原因:
- Token 过期后的刷新链路要尽量简单可靠
- 避免与解密失败 / Token 失效形成耦合死循环
### 10.3 文件上传接口
如:
- `multipart/form-data`
原因:
- 上传流不适合直接套 JSON 密文结构
- 第一阶段先排除
### 10.4 文件下载 / Blob / 导出接口
原因:
- Admin 和 Web 当前请求封装对 `blob` 有专门处理
- 这类返回体不是标准 JSON,不应进入统一加密中间件
### 10.5 SSE / 流式接口
原因:
- SSE 是持续输出,不适合标准 JSON 包装加密
### 10.6 Swagger / OpenAPI 接口
原因:
- 文档接口要便于调试、对接和生成 SDK
### 10.7 第三方回调接口
原因:
- 第三方平台无法适配你本项目自定义加密协议
---
## 11. 前端接入设计
### 11.1 Admin 端接入点
目标文件:
- `frontend/admin/src/utils/http/index.ts`
建议改造点:
#### 请求前
- 判断当前接口是否启用加密
- 若启用:
- 获取缓存公钥
- 生成 AES Key / IV
- 加密请求体
- 用 RSA 公钥加密 AES Key
- 将请求体替换为密文协议
- 标记请求头,例如:
- `X-Api-Encrypt: 1`
- `X-Api-Encrypt-Version: v1`
#### 响应后
- 判断响应体是否为加密协议
- 若是:
- 使用本次请求缓存的 AES Key 解密响应
- 还原为 `code / msg / data`
- 再进入现有业务处理链
### 11.2 Web 端接入点
目标文件:
- `frontend/web/app/utils/request.ts`
改造逻辑与 Admin 一致。
### 11.3 公钥缓存策略
建议:
- 启动后首次调用公钥接口并缓存
- 缓存内容包含:
- `key_version`
- `public_key`
- `expire_at`(可选)
当出现以下情况时刷新公钥:
- 服务端返回“密钥版本不匹配”
- 公钥缓存过期
- 管理员手动重新生成密钥
---
## 12. 后端接入设计
### 12.1 请求解密中间件
职责:
- 识别密文协议
- 验证请求头与请求体格式
- RSA 解出 AES Key
- AES 解出业务 JSON
- 写回 `$request` 可读取结构
- 校验时间戳 / nonce(防重放,第二阶段可增强)
失败返回建议:
```json
{
"code": 400,
"msg": "请求解密失败",
"data": null
}
```
### 12.2 响应加密中间件
职责:
- 仅拦截 JSON 响应
- 判断当前接口是否需加密
- 读取原始响应体
- 使用当前请求上下文里的 AES Key 对响应整体 JSON 加密
- 替换成密文结构返回
### 12.3 中间件挂载建议
当前项目全局中间件在:
- `backend/config/middleware.php`
建议:
- 先作为**主项目全局中间件**挂载
- 中间件内部再根据:
- 配置开关
- 路由前缀
- 排除名单
- Attribute
做是否启用的判断
推荐顺序:
1. 跨域 / 语言中间件
2. 请求解密中间件
3. 业务鉴权 / Token 验证中间件
4. 控制器处理
5. 响应加密中间件
原因:
- 鉴权前应拿到解密后的真实请求参数
- 响应加密应放在业务执行后
### 12.4 Attribute 风格保持一致
当前项目已大量使用 PHP Attribute,例如:
- `#[AllowAnonymous]`
- OpenAPI `#[OA\Get] / #[OA\Post]`
因此接口加密标记也建议采用 Attribute 风格,而不是单独再引入一套注释体系。
建议:
- `#[EncryptResponse]`
- `#[SkipApiEncrypt]`
---
## 13. 与现有返回结构的兼容策略
### 13.1 明文模式
仍返回:
```json
{
"code": 0,
"msg": "成功",
"data": {}
}
```
### 13.2 密文模式
传输层返回:
```json
{
"encrypted": true,
"algorithm": "rsa-aes",
"key_version": "v1",
"data": "..."
}
```
前端解密后,还原为:
```json
{
"code": 0,
"msg": "成功",
"data": {}
}
```
这样可以最大程度复用当前:
- Admin 响应拦截器
- Web 请求封装错误处理
- 页面业务逻辑
---
## 14. 密钥管理设计
### 14.1 密钥存储原则
- **公钥**:可向前端公开
- **私钥**:仅允许服务端读取
- **私钥绝不进入前端页面、接口返回、构建产物**
### 14.2 密钥存储位置建议
建议文件存储:
- `runtime/keys/api_encrypt_public.pem`
- `runtime/keys/api_encrypt_private.pem`
也可后续支持:
- 数据库存储公钥元信息
- 私钥文件仅存磁盘
- 或接入云 KMS(长期方案)
### 14.3 密钥生成方式
建议新增后端命令,例如:
- `php webman api-encrypt:generate-keypair`
职责:
- 生成 RSA 密钥对
- 写入目标路径
- 记录 `key_version`
### 14.4 密钥轮换设计
建议第二阶段支持:
- 当前生效版本 `v1`
- 待切换版本 `v2`
- 前端获取公钥时拿到当前版本
- 服务端解密时可兼容多个版本一段时间
---
## 15. 安全增强建议
### 15.1 时间戳校验
请求带:
- `timestamp`
后端校验:
- 请求时间与服务器时间差不得超过 5 分钟
防止离线重放攻击。
### 15.2 nonce 去重
请求带:
- `nonce`
后端使用 Redis 记录一段时间内已用 nonce:
- 防止短时间重复提交同一密文包
### 15.3 签名校验(可选二阶段)
如果后续安全要求更高,可加:
- 对密文包进行签名
- 服务端验签
但第一阶段不建议同时引入过多复杂度。
### 15.4 敏感日志脱敏
启用接口加密后,后端日志中不能直接打印:
- 原始请求密钥
- AES 明文
- 私钥内容
日志只能打印:
- 路径
- key_version
- nonce
- 时间戳
- 错误码
---
## 16. 性能与运维影响
### 16.1 性能影响
加密会增加:
- 前端加密耗时
- 后端解密 / 加密耗时
- 大列表响应的 CPU 消耗
建议:
- 第一阶段仅在敏感接口上验证性能
- 对大列表、报表、导出接口默认排除
### 16.2 调试复杂度
启用后:
- 浏览器 Network 面板看到的是密文
- 抓包看到的也是密文
- 后端日志不能打印明文
需要增加:
- 开发环境关闭开关
- 测试环境单独验证
- 日志打印加密链路状态而不是明文内容
### 16.3 故障排查
建议新增错误码:
- `API_ENCRYPT_INVALID_PAYLOAD`
- `API_ENCRYPT_DECRYPT_FAILED`
- `API_ENCRYPT_KEY_VERSION_INVALID`
- `API_ENCRYPT_TIMESTAMP_EXPIRED`
- `API_ENCRYPT_NONCE_REPLAY`
---
## 17. 与本项目现有模块的兼容策略
### 17.1 Admin 端
兼容点:
- 当前 Axios 响应拦截器依赖 `code / msg / data`
- 解密必须发生在这套业务判断之前
### 17.2 Web 端
兼容点:
- 当前 `request.ts` 里有 401 刷新 Token 逻辑
- 解密必须在业务 code 判断之前完成
- `showErrorMessage: false` 等语义必须保持不变
### 17.3 插件系统
兼容点:
- 插件接口默认也应继承主项目加密能力
- 插件中如有上传/下载/SSE,需要支持排除
### 17.4 Swagger / OpenAPI
建议保持明文访问,不进入统一加密链路。
---
## 18. 管理端配置页面建议
如果后续做成你截图里的配置页面,建议字段如下:
### 18.1 基础开关
- 接口加密:开 / 关
- 加密范围:
- 部分加密
- 全局加密
### 18.2 密钥信息
- 当前密钥版本
- RSA 公钥(可复制)
- RSA 私钥:
- 不直接展示完整明文
- 仅支持“已配置 / 未配置”状态展示
- 仅服务端生成和存储
### 18.3 操作项
- 重新生成密钥
- 导出公钥
- 同步前端缓存失效标记
### 18.4 排除规则
- 路径排除列表
- 控制器/模块排除列表
- 文件上传/下载排除开关
### 18.5 安全参数
- 请求时间窗口
- nonce 有效期
- 是否启用重放校验
---
## 19. 分阶段实施计划
### 第一阶段:最小可用方案
目标:先跑通主链路。
内容:
- 新增全局配置 `api_encrypt.php`
- 新增 RSA 密钥生成命令
- 新增公钥获取接口
- 后端新增:
- 请求解密中间件
- 响应加密中间件
- Web 前端接入请求/响应加解密
- 仅支持:
- JSON 接口
- 部分加密模式
- 排除:
- 登录刷新
- 上传下载
- SSE
- Swagger
### 第二阶段:全局加密方案
内容:
- 支持 `scope=all`
- 支持路径排除
- 支持 `#[SkipApiEncrypt]`
- 支持 Admin 端统一接入
### 第三阶段:增强安全与运维能力
内容:
- 支持 nonce 防重放
- 支持时间戳过期校验
- 支持密钥轮换 / 多版本兼容
- 支持后台可视化配置
- 支持链路日志和告警
---
## 20. 推荐实施顺序
建议按以下顺序推进:
1. **先强制生产 HTTPS**
- 不先做 HTTPS,接口加密的实际收益会打折
2. **实现后端密钥管理与公钥接口**
3. **先接 Web 端请求加密**
- 范围更可控
4. **再接 Admin 端统一请求封装**
5. **最后接全局开关与管理端配置页面**
---
## 21. 风险清单
### 21.1 风险:前后端版本不一致
可能出现:
- 前端已发密文,后端还未解密
- 后端已返回密文,前端还未解密
规避:
- 必须通过配置开关灰度上线
- 先测试环境联调,再小范围启用
### 21.2 风险:Token 刷新链路被加密耦合破坏
规避:
- 第一阶段将 `/auth/refresh` 排除加密
### 21.3 风险:文件下载/上传被误加密
规避:
- 根据 `Content-Type` / `responseType` 明确排除
### 21.4 风险:前端调试困难
规避:
- 开发环境默认关闭
- 提供调试日志开关,仅打印协议状态,不打印明文
---
## 22. 最终建议结论
对于当前项目,推荐的接口加密方案是:
- **以 HTTPS 为基础**
- **采用 RSA + AES 混合加密**
- **后端私钥、前端公钥**
- **请求与响应统一加密**
- **支持关闭 / 部分加密 / 全局加密**
- **第一阶段只覆盖 JSON 业务接口**
- **排除上传、下载、SSE、刷新 Token、Swagger、第三方回调**
- **前后端统一在请求封装层与中间件层接入**
- **保持现有 `code / msg / data` 业务语义不变**
这是最符合本项目现状、改造成本、后续可维护性和安全收益平衡的一套方案。
---
## 23. 后续开发文档建议
本方案文档确认后,建议继续拆成 4 份执行文档:
1. **后端实现设计**
- 中间件、配置、命令、公钥接口、Attribute
2. **Web 前端接入设计**
- `frontend/web/app/utils/request.ts` 改造方案
3. **Admin 前端接入设计**
- `frontend/admin/src/utils/http/index.ts` 改造方案
4. **联调与灰度上线清单**
- 测试用例、排除清单、回滚方案