feat: 上线前安全加固与发布工程化;appmall 由插件迁移为独立应用
安全(P0) - 发布包签名:新增 scripts/package_keygen.php、scripts/package_verify.php、 ywxapp\service\PackageVerifier;FrameworkService::apply() 解压前强制验签 (ywxapp.framework_package_verify,默认开启) - JWT 收紧:config/jwt.php 移除公开默认密钥,JwtService::assertStrongSecret() 校验非空 / 长度≥32 / 已知弱值;.env.production.example 键名由 JWT_SECRET_KEY 修正为 JWT_SECRET - /api/v1/Article:免登录白名单收窄为只读接口,写操作字段白名单 + assertOwner() 越权校验 - 开发者令牌:查找接口不再回显明文、统一失败文案 + 限流 - TemplateInstaller:文件名白名单 + realpath 归一化 + 目录内断言,修复 Zip Slip;补 use ZipArchive - 后台登录:接入验证码、失败锁定与登录限流(config/ratelimit.php) - 缓存 / Session 驱动可由 .env 覆盖(CACHE_DRIVER / SESSION_TYPE),集群部署改 redis - paycenter 支付日志开关改为 PAY_LOG,不再跟随 APP_DEBUG 工程化 / 交付 - 新增 CHANGELOG.md 与 .env.production.example;composer.lock 入库锁定依赖版本 - 插件离线安装 CLI 增加 --uid,后台上传安装透传 uid,付费插件授权校验可通过 - 授权拒绝文案区分「未携带 uid」与「未找到有效授权」,便于排障 - .gitignore 放行 .env.production.example / .env.addon.example / .gitattributes / public/.htaccess 结构 - appmall 由 addon/appmall 迁移为独立应用 app/appmall(市场 / 开发者中心 / 审核 / 分润)
This commit is contained in:
1 parent
1d49e6f5ee
commit
18101cf11a
636 files changed
+58517
-9519
No files matched your search
@@ -0,0 +1,274 @@
|
||||
# 设备注册(MQTT `system/device/register/+`)
|
||||
|
||||
设备通过 MQTT 主题向服务端注册入网,领取正式连接凭证(uuid/secret)与订阅主题清单;用户随后用**设备识别码**绑定设备获取设备信息与面板类型。
|
||||
|
||||
支持两种协议:
|
||||
|
||||
1. **MAC 即插即用注册(推荐)**:设备上电自动上报 `mac/type/model/ver`,服务端自动入网,无需后台预登记。
|
||||
2. **华为式预登记注册(旧协议,兼容)**:设备出厂预置 **SN + activate_token**,后台登记后激活。
|
||||
|
||||
## 主题约定
|
||||
|
||||
| 主题 | 方向 | 说明 |
|
||||
|------|------|------|
|
||||
| `system/device/register/{sn}` | 设备 → 服务端 | 设备发布注册/激活请求 |
|
||||
| `system/device/register/{sn}/resp` | 服务端 → 设备 | 注册回执(凭证 + 订阅主题 / 错误原因) |
|
||||
|
||||
ACL:上述主题**内置放行**(`Auth::checkAcl` 对 `system/device/register/{sn}` 发布与 `.../resp` 订阅始终放行,不依赖 ACL 开关),设备可在激活前匿名/以 SN 连接完成注册。
|
||||
|
||||
---
|
||||
|
||||
## MAC 即插即用注册(推荐,ESP32 等设备直连入网)
|
||||
|
||||
设备无需后台预登记:上电后把自身信息发布到注册主题,服务端**自动创建设备**并发放凭证。
|
||||
|
||||
### 主题
|
||||
|
||||
| 主题 | 方向 | 说明 |
|
||||
|------|------|------|
|
||||
| `system/device/register/{mac}` | 设备 → 服务端 | 注册请求(`{mac}` = 设备 MAC 去冒号小写,12 位 hex,如 `a0b1c2d3e4f5`) |
|
||||
| `system/device/register/{mac}/resp` | 服务端 → 设备 | 注册回执(凭证 + 订阅主题 / 错误原因) |
|
||||
|
||||
ACL:上述主题**内置放行**,设备可在未激活前以任意 clientId(建议直接用 mac)连接并完成注册。
|
||||
|
||||
### 注册消息(设备上电 publish,建议 retained)
|
||||
|
||||
```json
|
||||
{
|
||||
"mac": "a0b1c2d3e4f5",
|
||||
"type": "esp32c3_temp",
|
||||
"model": "KF-C3-3KANG",
|
||||
"ver": "1.0"
|
||||
}
|
||||
```
|
||||
|
||||
| 字段 | 值 | 作用 |
|
||||
|------|------|------|
|
||||
| `mac` | 设备 MAC 去冒号小写 | 唯一身份证,服务端靠它区分设备、做绑定(须与主题中的 mac 一致,否则拒绝) |
|
||||
| `type` | `esp32c3_temp` | 设备类型,服务端落库为 `dev_type`,App 据此渲染面板(炕温控器/插座/灯…) |
|
||||
| `model` | `KF-C3-3KANG` | 产品型号,后台管理区分硬件版本 |
|
||||
| `ver` | `1.0` | 固件版本,服务端记录,用于版本检测/OTA 提示 |
|
||||
|
||||
> **retained 说明**:服务端收到 publish 即处理(不依赖 retained 标志)。retained 的作用是让**之后订阅**该主题的客户端也能看到设备注册信息。回执 `.../resp` **不会 retained**(防止 secret 泄露给后来的订阅者),设备若未收到回执应延时重发注册消息(例如 5s/30s/5min 退避)。
|
||||
|
||||
### 处理流程
|
||||
|
||||
```
|
||||
① 设备联网:订阅 system/device/register/{mac}/resp
|
||||
② 设备发布(retained):system/device/register/{mac},payload 含 mac/type/model/ver
|
||||
③ 服务端:mac 首次出现 → 自动创建设备(sn=mac)并生成 uuid/secret,写入 iot_device
|
||||
④ 服务端回执:{uuid, secret(仅首次), dev_type, model, ver, topics}
|
||||
⑤ 设备保存凭证,改用 clientId=uuid / username=uuid / password=secret 连接
|
||||
⑥ 设备按 topics 订阅 /devices/{uuid}/#,收发状态与命令
|
||||
```
|
||||
|
||||
再次上报(设备重启 / 固件升级后):
|
||||
- mac 已存在 → 幂等更新 `type/model/ver`,**不重置凭证**,回执不带 secret;
|
||||
- 固件 `ver` 变化自动同步到设备记录,后台/OTA 据此判断是否需要推送新固件。
|
||||
|
||||
### 回执 payload(成功)
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"id": 1, "sn": "a0b1c2d3e4f5", "mac": "a0b1c2d3e4f5",
|
||||
"uuid": "dev_8f3a...",
|
||||
"dev_type": "esp32c3_temp", "model": "KF-C3-3KANG", "ver": "1.0",
|
||||
"secret": "明文凭证(仅首次注册返回)",
|
||||
"activated": true, "bound": false,
|
||||
"topics": {
|
||||
"state": "/devices/dev_8f3a.../state", "cmd": "/devices/dev_8f3a.../cmd",
|
||||
"ota": "/devices/dev_8f3a.../ota", "ota_report": "/devices/dev_8f3a.../ota/report",
|
||||
"event": "/devices/dev_8f3a.../event"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
失败(`code=1`)示例:`{"code":1,"message":"payload mac 与主题不一致","data":null}`、`{"code":1,"message":"mac 格式非法(应为12位十六进制,去冒号小写)","data":null}`。
|
||||
|
||||
### 用户绑定(App 扫码/输 mac)
|
||||
|
||||
```
|
||||
POST /iot/api/client/bindDevice (需登录)
|
||||
body: { "sn": "a0b1c2d3e4f5", "title": "炕温控器" }
|
||||
```
|
||||
|
||||
MAC 自动注册的设备**无配对码**(扫码即绑,先到先得);如需防抢绑,可在后台设备详情为该设备补设 `pair_code`,此后绑定须携带该码。
|
||||
|
||||
绑定成功返回 `mac / dev_type / ver` 与 `topics`:App 用 `dev_type` 查「设备类型 → 面板」映射表渲染对应控制面板,用 `ver` 与后台最新固件比对触发 OTA 提示。
|
||||
|
||||
---
|
||||
|
||||
## 华为式预登记注册(旧协议,兼容)
|
||||
|
||||
```
|
||||
① 后台预登记设备:生成 sn + pair_code(贴纸)+ activate_token(出厂预置)
|
||||
② 设备联网:订阅 system/device/register/{sn}/resp
|
||||
③ 设备发布:system/device/register/{sn},payload 见下
|
||||
④ 收到回执:uuid / secret / topics → 保存凭证
|
||||
⑤ 使用凭证连接 MQTT:clientId=uuid,username=uuid,password=secret
|
||||
⑥ 按 topics 订阅 /devices/{uuid}/# 收发状态与命令
|
||||
```
|
||||
|
||||
### 请求 payload(②③)
|
||||
|
||||
```json
|
||||
{
|
||||
"token": "b5d3...32位hex激活令牌"
|
||||
}
|
||||
```
|
||||
|
||||
也兼容裸 token 字符串形式(直接以 token 作为 payload 发布)。
|
||||
|
||||
### 回执 payload(④)
|
||||
|
||||
成功(`code=0`):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "ok",
|
||||
"data": {
|
||||
"id": 1,
|
||||
"sn": "SN20260826",
|
||||
"uuid": "dev_8f3a...",
|
||||
"title": "智能灯泡",
|
||||
"model": "LAMP-01",
|
||||
"secret": "明文凭证(仅激活成功这一次返回)",
|
||||
"activated": true,
|
||||
"bound": false,
|
||||
"topics": {
|
||||
"state": "/devices/dev_8f3a.../state",
|
||||
"cmd": "/devices/dev_8f3a.../cmd",
|
||||
"ota": "/devices/dev_8f3a.../ota",
|
||||
"ota_report": "/devices/dev_8f3a.../ota/report",
|
||||
"event": "/devices/dev_8f3a.../event"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
失败(`code=1`):`{ "code": 1, "message": "激活令牌无效或已失效", "data": null }`
|
||||
|
||||
幂等:已激活设备重复注册不报错,回执返回 `activated:true` 但 **不再下发 secret**(如需换凭证走 `resetSecret`)。
|
||||
|
||||
## 用户绑定设备识别码(App 侧)
|
||||
|
||||
设备激活后,用户通过 App 绑定:
|
||||
|
||||
```
|
||||
POST /iot/api/client/bindDevice (需登录)
|
||||
body: { "sn": "SN20260826", "pair_code": "123456", "title": "客厅灯" }
|
||||
```
|
||||
|
||||
绑定成功返回(含订阅主题与设备信息):
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 1,
|
||||
"data": {
|
||||
"id": 1, "sn": "SN20260826", "uuid": "dev_8f3a...",
|
||||
"title": "客厅灯", "model": "LAMP-01", "room": "",
|
||||
"bound": true, "activated": true, "secret": "",
|
||||
"topics": { "state": "/devices/dev_8f3a.../state", "cmd": "/devices/dev_8f3a.../cmd", "ota": "/devices/dev_8f3a.../ota", "ota_report": "/devices/dev_8f3a.../ota/report", "event": "/devices/dev_8f3a.../event" }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
> 注:插件 API 的 `Result::success` 用 `code=1` 表示成功(与框架 `code=0` 相反),解析时以 `message` 键为准。
|
||||
|
||||
绑定后 App 即可使用 `uuid` 连接 MQTT 推送(WebSocket `ws://{host}:8083`)或通过 REST 接口下发命令。
|
||||
|
||||
## 其他相关接口
|
||||
|
||||
| 接口 | 说明 |
|
||||
|------|------|
|
||||
| `POST /iot/api/deviceActivate` | HTTP 方式激活(`sn` + `token`),与 MQTT 注册等效,适合无 MQTT 客户端的设备 |
|
||||
| `POST /iot/api/resetSecret` | 重置设备密钥(返回新明文,旧密钥作废) |
|
||||
| `GET /iot/api/myDevices` | 我的设备列表(主人 + 共享,含在线状态/影子) |
|
||||
| `POST /iot/api/sendCommand` | 向设备下发命令(写入 `/devices/{uuid}/cmd`) |
|
||||
| `POST /iot/api/deviceLogs` | 设备消息流水 |
|
||||
|
||||
## 设备侧配网(ESP32 固件 `water_temp.ino`)
|
||||
|
||||
现场部署**不再需要**为了改 WiFi 重烧固件(OTA 又必须先联网,过去是死锁)。固件用 `WiFiManager` 自带配网页,**v1.6.2 起表单里只有 WiFi**(服务器与端口已写死在固件里,见下节):
|
||||
|
||||
1. 上电先用**已保存凭据**连网(`WIFI_CONNECT_S` = 15s),连上即正常跑,不会打扰用户;
|
||||
2. 连不上(首次上电 / 换路由器 / 改密码)→ 自动开配网热点
|
||||
`TempMonit-Esp32<MAC后4位>`(默认密码 `12345678`,见 `WIFI_AP_PASS`);
|
||||
3. 手机连热点 → 浏览器打开 `http://192.168.4.1`(Android 会自动弹配网页)
|
||||
→ 表单**只填 WiFi** → 保存 → 设备自动重启生效;
|
||||
4. 热点 180s(`WIFI_PORTAL_S`)无人操作自动关闭,回后台继续重试 —— 要重新配网请断电重上电。
|
||||
|
||||
### 服务器地址:编译期写死 + 备用机自动切换(v1.6.2)
|
||||
|
||||
| 项 | 位置 | 说明 |
|
||||
|---|---|---|
|
||||
| 服务器表 | 固件常量区 `SRV_HOSTS[]` | 按 **主 → 备** 顺序填,只填域名或 IP(表尾空串/`nullptr` 即结束) |
|
||||
| 端口 | `DEF_MQTT_PORT` = `1883` | **写死**,所有服务器共用(配网页已无端口输入框) |
|
||||
| 切换阈值 | `SRV_FAILOVER_N` = `3` | 当前这台连续失败 3 次(5s/次 ⇒ 约 15s)切下一台,走到表尾回到第 0 台,永不卡死 |
|
||||
| 切换条件 | 仅「网络层」失败 | `-2/-3/-4/3`(TCP 连不上/超时/被断/服务端不可用)才切;**CONNACK 1~5 是「服务器已应答、只是拒绝」(凭据/ACL 问题),换台毫无意义** |
|
||||
|
||||
默认表(按现场改这几行即可):
|
||||
|
||||
```c
|
||||
const char* SRV_HOSTS[SRV_MAX] = {
|
||||
"mqtt.ywxapp.cn", // 0 主:域名(正常走这条)
|
||||
"211.149.215.200", // 1 备:同机 IP 直连 —— 免 DNS(解析失败/被劫持时仍能连上)
|
||||
// "mqtt2.ywxapp.cn", // 2 备:真·第二台服务器(有独立机器时填它的域名或 IP)
|
||||
};
|
||||
```
|
||||
|
||||
- 串口:开机打印整张表(`[SRV] ... <- 当前`),心跳行带 `srv=`,切换时打
|
||||
`[SRV] 连接连续失败:A → B(第 2/2 台)` —— 排障一眼看清在用哪台、切过没有。
|
||||
- NVS 里的 `mqtt_host` / `mqtt_port`(≤v1.6.1 写过的)**一律不再读取**;遗留键不擦除(省一次 flash 写)。
|
||||
- ⚠️ 换服务器 / 加备用机 = 改表后**重新编译烧录**;现场设备不需要任何配置,也就没有填错的可能。
|
||||
- 注册流程同样参与切换(主服务器挂掉时,新设备也必须能注册上)。
|
||||
|
||||
**远程清空重配**(设备还连得上时最方便,App 用 `sendCommand` 把 payload 原样发下去即可):
|
||||
|
||||
```json
|
||||
{"action": "wifi_reset"}
|
||||
```
|
||||
|
||||
设备先回 `ack`,随后抹掉 WiFi 凭据并重启 → 开机连不上 → 自动进配网热点。
|
||||
⚠️ **只清 WiFi 凭据**:`uuid` / `secret` / 传感器配置全部保留(服务器写死在固件里,不受影响),不需要重新注册。
|
||||
|
||||
### 清除身份 / 恢复出厂(固件 v1.3.3+,BOOT 键)
|
||||
|
||||
⚠️ **背景(重要)**:后台「删除设备」是**硬删** —— `DeviceService::remove()` 会清掉
|
||||
`iot_device`、`iot_auth`、`iot_acl`、`iot_retain`、`iot_topic`、`iot_offline`、
|
||||
分组关联 / 共享关系 / 传感器配置。**但设备端 NVS 里的 `uuid`/`secret` 不会被清**,于是:
|
||||
|
||||
> 设备拿旧凭据去连 → `iot_auth` 里已无此用户 → CONNACK 4/5 被拒;
|
||||
> 而注册流程只在「`uuid` 为空」时才启动 → **设备永久爬不起来**。
|
||||
> 现场表现:串口一直 `[MQTT] fail rc=4`,后台也永远不会再出现该设备。
|
||||
|
||||
固件 v1.3.3 起提供设备端「清除身份」入口,**现场无需拆机重烧**:
|
||||
|
||||
| 操作 | 清除内容 | 结果 |
|
||||
|---|---|---|
|
||||
| **按住 BOOT 键 3 秒后松手** | `uuid` / `secret` / 传感器配置 | 重启 → 自动重新注册,拿全新凭证;**WiFi 与服务器地址保留,不用重新配网** |
|
||||
| **按住 BOOT 键 8 秒不松** | 上述 + WiFi 凭据(彻底恢复出厂) | 重启 → 进配网热点 `TempMonit-Esp32<MAC后4位>` |
|
||||
|
||||
- **键位**:ESP32-C3 = `GPIO9`,经典 ESP32 = `GPIO0`(改固件宏 `BTN_PIN`);固件里的
|
||||
`ONEWIRE_PINS`(当前 `{3,4,10}`)不要与它撞脚。
|
||||
- **上电时已按住** 也生效(恢复出厂的标准姿势),无 3s 内的时机要求。
|
||||
- 串口日志以 `[BTN]` / `[RST]` 开头,会在按满 3s 时提示"现在松手=清身份 / 继续按住=全清"。
|
||||
- **护栏**:开机按住超 15s(`BTN_ABNORMAL_MS`)判定为引脚异常并放弃清除;运行期必须先
|
||||
观察到一次「松开」才允许触发(`armed` 标志),引脚恒低的板子不会被反复恢复出厂。
|
||||
|
||||
**远程等价命令**(设备仍在线时最方便,发到 `/devices/{uuid}/cmd`):
|
||||
|
||||
```json
|
||||
{"action": "clear_identity"}
|
||||
```
|
||||
```json
|
||||
{"action": "factory_reset"}
|
||||
```
|
||||
|
||||
⚠️ 重新注册后拿到的是**全新 uuid**,后台会新增一条设备(`member_id=0` 未绑定),
|
||||
需在 App / 后台**重新绑定**;旧 `iot_device_log` / `iot_alert` 中按旧 uuid 存的历史不会自动迁移。
|
||||
Reference in new issue
Block a user