安全(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(市场 / 开发者中心 / 审核 / 分润)
13 KiB
设备注册(MQTT system/device/register/+)
设备通过 MQTT 主题向服务端注册入网,领取正式连接凭证(uuid/secret)与订阅主题清单;用户随后用设备识别码绑定设备获取设备信息与面板类型。
支持两种协议:
- MAC 即插即用注册(推荐):设备上电自动上报
mac/type/model/ver,服务端自动入网,无需后台预登记。 - 华为式预登记注册(旧协议,兼容):设备出厂预置 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)
{
"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(成功)
{
"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(②③)
{
"token": "b5d3...32位hex激活令牌"
}
也兼容裸 token 字符串形式(直接以 token 作为 payload 发布)。
回执 payload(④)
成功(code=0):
{
"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": "客厅灯" }
绑定成功返回(含订阅主题与设备信息):
{
"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(服务器与端口已写死在固件里,见下节):
- 上电先用已保存凭据连网(
WIFI_CONNECT_S= 15s),连上即正常跑,不会打扰用户; - 连不上(首次上电 / 换路由器 / 改密码)→ 自动开配网热点
TempMonit-Esp32<MAC后4位>(默认密码12345678,见WIFI_AP_PASS); - 手机连热点 → 浏览器打开
http://192.168.4.1(Android 会自动弹配网页) → 表单只填 WiFi → 保存 → 设备自动重启生效; - 热点 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 问题),换台毫无意义 |
默认表(按现场改这几行即可):
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 原样发下去即可):
{"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):
{"action": "clear_identity"}
{"action": "factory_reset"}
⚠️ 重新注册后拿到的是全新 uuid,后台会新增一条设备(member_id=0 未绑定),
需在 App / 后台重新绑定;旧 iot_device_log / iot_alert 中按旧 uuid 存的历史不会自动迁移。