feat(controlled): 实现设备激活与安全认证流程
- 添加API客户端、加密存储和provision/token激活逻辑 - WebSocket改用Bearer令牌认证,移除REGISTER请求 - 设备ID改为服务端下发,支持令牌刷新和强制下线处理 - 新增deviceSecret加密存储和accessToken自动刷新 - 更新设备ID获取方式为出厂SN,添加安全存储依赖
This commit is contained in:
700
WebRTCSignalServer/docs/CLIENT_API.md
Normal file
700
WebRTCSignalServer/docs/CLIENT_API.md
Normal file
@@ -0,0 +1,700 @@
|
||||
# 客户端对接接口规范 v1
|
||||
|
||||
面向 5 个客户端:`WebRTCControlled`(被控端)、`WebRTCController`(主控端 Android)、
|
||||
`WebRTCControllerWeb`、`webrtc_controller_flutter`、`webrtc_controller_ios`。
|
||||
|
||||
> **破坏性变更**:`/ws/signal` 不再接受匿名连接。未携带有效令牌的握手将被 **HTTP 401** 拒绝。
|
||||
> 各端必须完成本文档的改造才能连接。
|
||||
|
||||
---
|
||||
|
||||
## 0. 基础约定
|
||||
|
||||
### 0.1 地址
|
||||
|
||||
现有各端硬编码的信令地址为 `wss://www.ttstd.com/signal`(经反向代理)。
|
||||
新增的 HTTP 接口位于同域下的 `/api/**`:
|
||||
|
||||
| 用途 | 地址 |
|
||||
|---|---|
|
||||
| HTTP API 基址 | `https://www.ttstd.com` |
|
||||
| WebSocket 信令 | `wss://www.ttstd.com/signal` |
|
||||
|
||||
> **运维需确认**:反向代理需将 `/api/**` 转发到信令服务的 `/api/**`,
|
||||
> 且 `/signal` 转发到 `/ws/signal`。若代理仅配置了 `/signal`,需补充 `/api` 规则。
|
||||
|
||||
建议各端把「HTTP 基址」和「WS 地址」拆成两个可配置项,而非从 WS 地址推导。
|
||||
|
||||
### 0.2 通用响应
|
||||
|
||||
成功:`200 OK`,JSON body。
|
||||
|
||||
失败:
|
||||
```json
|
||||
{ "code": 401, "error": "UNAUTHORIZED", "message": "认证失败" }
|
||||
```
|
||||
|
||||
> 服务端对外只返回**模糊提示**(不区分"账号不存在/密码错误"、"设备离线/被拉黑"),
|
||||
> 详细原因仅记录在服务端日志。客户端不要试图解析 message 做业务分支,**请以 `error` 字段为准**。
|
||||
|
||||
### 0.3 通用错误码
|
||||
|
||||
| HTTP | error | 客户端应对 |
|
||||
|---|---|---|
|
||||
| 400 | `BAD_REQUEST` | 参数问题,提示用户修正 |
|
||||
| 401 | `UNAUTHORIZED` | 令牌无效/过期 → 尝试刷新,失败则跳登录 |
|
||||
| 403 | `FORBIDDEN` | 账号被封禁/设备被禁用 → 提示并登出 |
|
||||
| 429 | `TOO_MANY_REQUESTS` | 触发限流 → 退避重试,勿立即重连 |
|
||||
|
||||
---
|
||||
|
||||
## 1. 主控端接口(Controller / Web / Flutter / iOS)
|
||||
|
||||
### 1.1 注册
|
||||
|
||||
```
|
||||
POST /api/auth/register
|
||||
Content-Type: application/json
|
||||
|
||||
{ "username": "alice", "password": "Passw0rd!" }
|
||||
```
|
||||
|
||||
响应 `200`:
|
||||
```json
|
||||
{ "userId": "usr_xxxxxxxxxxxxxxxxxxxx", "username": "alice" }
|
||||
```
|
||||
|
||||
约束:
|
||||
- 用户名:3-32 位,`[a-zA-Z0-9_.-]`
|
||||
- 密码:8-128 位,且至少包含「大写 / 小写 / 数字 / 符号」中的两类
|
||||
- 限流:单 IP 每小时 5 次
|
||||
- 可由服务端配置关闭(`ACCOUNT_REGISTRATION_ENABLED=false`),关闭时返回 403
|
||||
|
||||
---
|
||||
|
||||
### 1.2 登录
|
||||
|
||||
```
|
||||
POST /api/auth/login
|
||||
|
||||
{ "username": "alice", "password": "Passw0rd!" }
|
||||
```
|
||||
|
||||
响应 `200`:
|
||||
```json
|
||||
{
|
||||
"accessToken": "eyJhbGc...",
|
||||
"refreshToken": "ses_xxxx.AbCdEf...",
|
||||
"expiresIn": 900,
|
||||
"sessionId": "ses_xxxxxxxxxxxxxxxxxxxx",
|
||||
"principalId": "usr_xxxxxxxxxxxxxxxxxxxx",
|
||||
"displayName": "alice"
|
||||
}
|
||||
```
|
||||
|
||||
要点:
|
||||
- `accessToken` 有效期 **900 秒**,用于 HTTP 与 WebSocket 握手
|
||||
- `refreshToken` 有效期 **7 天**,格式为 `<sessionId>.<secret>`,**必须整体保存**
|
||||
- 限流:单 IP 5 分钟内 10 次;连续密码错误 5 次锁定账号 15 分钟(返回 429)
|
||||
|
||||
> **开启 TOTP 的账号**:若响应为 `401` 且 `error` 为 `TOTP_REQUIRED`,
|
||||
> 需引导用户输入 6 位动态码,并带 `totpCode` 字段重新请求登录。详见 §1.8。
|
||||
|
||||
**存储要求**:
|
||||
|
||||
| 端 | accessToken | refreshToken |
|
||||
|---|---|---|
|
||||
| Android | 内存 | EncryptedSharedPreferences |
|
||||
| iOS | 内存 | **Keychain**(勿用 UserDefaults) |
|
||||
| Flutter | 内存 | `flutter_secure_storage` |
|
||||
| Web | 内存变量 | `sessionStorage`(勿用 localStorage) |
|
||||
|
||||
---
|
||||
|
||||
### 1.3 刷新令牌
|
||||
|
||||
```
|
||||
POST /api/auth/refresh
|
||||
|
||||
{ "refreshToken": "ses_xxxx.AbCdEf..." }
|
||||
```
|
||||
|
||||
响应结构同登录。
|
||||
|
||||
**关键规则(务必遵守)**:
|
||||
- 刷新令牌**一次性**,每次刷新都会返回新的,**必须立即覆盖保存旧值**
|
||||
- 服务端有**复用检测**:若用已被使用过的旧 refreshToken 再次请求,
|
||||
会判定为令牌泄露并**吊销整个会话**(该账号在此设备上需重新登录)
|
||||
- 因此**严禁并发刷新**。请加互斥锁/单飞(single-flight),
|
||||
多个请求同时遇到 401 时只允许一个发起刷新,其余等待其结果
|
||||
|
||||
**刷新时机**:建议在 `accessToken` 剩余有效期不足 1/3(即约 300 秒)时主动刷新,
|
||||
不要等到 401 才被动刷新(WebSocket 握手失败重连成本更高)。
|
||||
|
||||
---
|
||||
|
||||
### 1.4 登出
|
||||
|
||||
```
|
||||
POST /api/auth/logout
|
||||
Authorization: Bearer <accessToken>
|
||||
```
|
||||
|
||||
```
|
||||
POST /api/auth/logout-all # 全端登出
|
||||
Authorization: Bearer <accessToken>
|
||||
```
|
||||
|
||||
响应:`{ "success": true }`
|
||||
|
||||
登出后应清空本地令牌并断开 WebSocket。
|
||||
|
||||
---
|
||||
|
||||
### 1.5 修改密码
|
||||
|
||||
```
|
||||
POST /api/auth/change-password
|
||||
Authorization: Bearer <accessToken>
|
||||
|
||||
{ "oldPassword": "...", "newPassword": "..." }
|
||||
```
|
||||
|
||||
响应:`{ "success": true, "message": "密码已修改,请重新登录" }`
|
||||
|
||||
> 成功后服务端会**吊销全部会话**,客户端必须清空令牌并跳转登录页。
|
||||
|
||||
---
|
||||
|
||||
### 1.6 查询当前身份
|
||||
|
||||
```
|
||||
GET /api/auth/me
|
||||
Authorization: Bearer <accessToken>
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"principalId": "usr_xxx",
|
||||
"principalType": "USER",
|
||||
"displayName": "alice",
|
||||
"sessionId": "ses_xxx",
|
||||
"signalDeviceId": "ctl_ses_xxx",
|
||||
"admin": false
|
||||
}
|
||||
```
|
||||
|
||||
`signalDeviceId` 即该连接在信令网络中的 ID,**由服务端派生,客户端不可自定义**。
|
||||
|
||||
---
|
||||
|
||||
### 1.7 会话管理
|
||||
|
||||
```
|
||||
GET /api/auth/sessions
|
||||
Authorization: Bearer <accessToken>
|
||||
```
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"sessionId": "ses_xxx",
|
||||
"ip": "1.2.3.4",
|
||||
"userAgent": "okhttp/4.12.0",
|
||||
"createdAt": 1730000000000,
|
||||
"lastSeenAt": 1730000600000,
|
||||
"current": true
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
可用于「登录设备管理」界面,配合 §1.4 的 logout-all 实现异地下线。
|
||||
|
||||
---
|
||||
|
||||
### 1.8 TOTP 双因子(可选启用)
|
||||
|
||||
```
|
||||
POST /api/auth/totp/setup # 生成密钥,返回 otpauth:// URI 供扫码
|
||||
Authorization: Bearer <accessToken>
|
||||
```
|
||||
```json
|
||||
{ "secret": "JBSWY3DP...", "otpauthUri": "otpauth://totp/...", "notice": "..." }
|
||||
```
|
||||
|
||||
```
|
||||
POST /api/auth/totp/enable # 输入一次动态码完成绑定
|
||||
{ "code": "123456" }
|
||||
```
|
||||
|
||||
```
|
||||
POST /api/auth/totp/disable # 需当前密码 + 动态码
|
||||
{ "password": "...", "code": "123456" }
|
||||
```
|
||||
|
||||
启用后,`/api/auth/login` 需附带 `totpCode`:
|
||||
```json
|
||||
{ "username": "alice", "password": "Passw0rd!", "totpCode": "123456" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.9 令牌校验(建立长连前的预检)
|
||||
|
||||
客户端在发起 WebSocket 握手前,可先调用此接口确认令牌未过期 / 未失效,
|
||||
避免握手阶段被服务端直接断开(关闭码 4001)。用户令牌与设备令牌均可使用。
|
||||
|
||||
```
|
||||
GET /api/client/verify
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
有效时响应 `200`:
|
||||
```json
|
||||
{
|
||||
"valid": true,
|
||||
"principalType": "DEVICE", // USER 或 DEVICE
|
||||
"principalId": "dev_xxx",
|
||||
"displayName": "Pixel 3",
|
||||
"expiresAt": 1730000900, // 令牌过期时间(秒)
|
||||
"remainingSeconds": 812, // 剩余有效秒数
|
||||
"serverTime": 1730000088
|
||||
}
|
||||
```
|
||||
|
||||
无效时响应 `200`(HTTP 仍为 200,业务字段辨状态,避免泄露令牌是否存在):
|
||||
```json
|
||||
{ "valid": false, "error": "UNAUTHORIZED", "message": "认证失败" }
|
||||
```
|
||||
|
||||
> 客户端策略:进入前台或准备重连时调用;若 `remainingSeconds` 低于 `accessTokenTtlSeconds * 0.33`,
|
||||
> 用户端走 `/api/auth/refresh`,设备端重新执行 `/api/device/token`。
|
||||
|
||||
---
|
||||
|
||||
### 1.10 设备自助信息(被控端)
|
||||
|
||||
被控端用自身设备令牌查询当前状态与在线情况(需 `DEVICE` 令牌)。
|
||||
|
||||
```
|
||||
GET /api/client/device/me
|
||||
Authorization: Bearer <deviceToken>
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"deviceUid": "dev_xxx",
|
||||
"model": "Pixel 3",
|
||||
"status": "ACTIVE", // ACTIVE / SUSPENDED / BANNED
|
||||
"usable": true, // 是否被禁用(false 时无法建立信令连接)
|
||||
"provisionedAt": 1729000000,
|
||||
"lastOnlineAt": 1730000088,
|
||||
"statusReason": null // 被封禁/暂停原因(如有)
|
||||
}
|
||||
```
|
||||
|
||||
> 若 `usable == false`,客户端应停止尝试连接并提示用户设备已被禁用。
|
||||
|
||||
---
|
||||
|
||||
### 1.11 握手指引(自配置)
|
||||
|
||||
返回 WebSocket 握手所需的地址、子协议、关闭码与刷新阈值,便于各端在无硬编码的前提下自配置。
|
||||
|
||||
```
|
||||
GET /api/client/ws-info
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"wsPath": "/ws/signal",
|
||||
"subprotocol": "signal.v1",
|
||||
"tokenMethods": [
|
||||
"header:Authorization Bearer <token>",
|
||||
"subprotocol:Sec-WebSocket-Protocol: signal.v1, auth.<token>",
|
||||
"query:?token=<token>"
|
||||
],
|
||||
"closeCodes": { "NORMAL": 1000, "UNAUTHORIZED": 4001, "FORCE_LOGOUT": 4003 },
|
||||
"idleTimeoutSeconds": 90,
|
||||
"recommendedRefreshRatio": 0.33,
|
||||
"accessTokenTtlSeconds": 900,
|
||||
"deviceTokenTtlSeconds": 900
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.12 绑定关系与黑名单(防骚扰核心)
|
||||
|
||||
被控端(设备)与主控端(用户账号)之间**必须先建立绑定**,主控端才能向其发起连接(OFFER)。
|
||||
即便已绑定,被控端仍可将某主控端加入**黑名单**,黑名单优先级高于绑定,OFFER 会被服务端直接拒绝。
|
||||
|
||||
**默认行为(服务端强制)**:
|
||||
- 未绑定 → OFFER 立即被拒,回送 `REQUEST_ERROR`:`"未与该设备建立绑定关系,无法发起连接"`。
|
||||
- 已拉黑 → OFFER 立即被拒,回送 `REQUEST_ERROR`:`"该设备已拒绝来自你的连接"`。
|
||||
- `DEVICE_LIST` 不再返回全局设备清单,主控端仅能看到自己已绑定设备的在线状态(消除被控端枚举)。
|
||||
|
||||
#### 被控端自助管理(设备令牌,`Authorization: Bearer <deviceToken>`)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| POST | `/api/client/device/bind` | 按用户名绑定某主控端:`{"username":"alice","alias":"客厅电视"}` |
|
||||
| POST | `/api/client/device/unbind` | 解绑:`{"username":"alice"}` |
|
||||
| POST | `/api/client/device/blacklist` | 拉黑:`{"username":"alice","reason":"骚扰"}` |
|
||||
| POST | `/api/client/device/unblacklist` | 解除拉黑:`{"username":"alice"}` |
|
||||
| GET | `/api/client/device/relations` | 查看本设备的绑定与黑名单列表 |
|
||||
|
||||
#### 主控端自助查询(用户令牌)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| GET | `/api/client/bindings` | 查看自己已绑定的设备及其在线状态 |
|
||||
|
||||
#### 管理员接口(`/api/admin`,需管理员令牌)
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| GET | `/api/admin/devices/{deviceUid}/bindings` | 设备全部绑定关系 |
|
||||
| GET | `/api/admin/devices/{deviceUid}/blacklist` | 设备黑名单 |
|
||||
| POST | `/api/admin/devices/{deviceUid}/bind` | 建立绑定:`{"username","role":"MEMBER|OWNER","alias"}` |
|
||||
| POST | `/api/admin/devices/{deviceUid}/unbind` | 解绑:`{"username"}` |
|
||||
| POST | `/api/admin/devices/{deviceUid}/blacklist` | 拉黑:`{"username","reason"}` |
|
||||
| POST | `/api/admin/devices/{deviceUid}/unblacklist` | 解除拉黑:`{"username"}` |
|
||||
|
||||
> 绑定由「被控端自助」或「管理员」创建;解绑为软删除(置 `REVOKED`),重新绑定可恢复。
|
||||
|
||||
---
|
||||
|
||||
### 1.13 配对码、TURN 凭证与骚扰举报
|
||||
|
||||
#### 配对码(建立绑定的用户友好入口)
|
||||
被控端生成一次性配对码(8 位、去除易混淆字符、10 分钟有效、单次使用、错误 5 次失效),主控端输入码即建立绑定(无需管理员介入)。
|
||||
|
||||
| 方法 | 路径 | 令牌 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| POST | `/api/client/device/pairing-code` | 设备 | 生成配对码,返回 `{"code":"A2B9K7M4","expiresInSeconds":600}` |
|
||||
| POST | `/api/client/pairing/redeem` | 用户 | 兑换:`{"code":"A2B9K7M4"}`,成功返回绑定信息 |
|
||||
|
||||
```bash
|
||||
# 被控端生成
|
||||
curl -X POST https://www.ttstd.com/api/client/device/pairing-code \
|
||||
-H "Authorization: Bearer $DEVICE_TOKEN"
|
||||
|
||||
# 主控端兑换
|
||||
curl -X POST https://www.ttstd.com/api/client/pairing/redeem \
|
||||
-H "Authorization: Bearer $USER_TOKEN" \
|
||||
-d '{"code":"A2B9K7M4"}'
|
||||
```
|
||||
|
||||
#### TURN 短期凭证(中继,RFC 7635 风格)
|
||||
客户端在 `new RTCPeerConnection` 前调用,获取时限内有效的 TURN 用户名/口令(HMAC-SHA1 由服务端密钥签名,到期需重新获取)。是否启用由服务端 `security.turn.enabled` 决定;`/api/client/ws-info` 的 `turnEnabled` 字段告知客户端。
|
||||
|
||||
| 方法 | 路径 | 令牌 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| GET | `/api/client/turn-credentials` | 用户/设备 | 返回 `iceServers`(含 urls/username/credential)、`expiresAt`、`ttlSeconds` |
|
||||
|
||||
```json
|
||||
{
|
||||
"iceServers": [
|
||||
{ "urls": "turn:turn.ttstd.com:3478?transport=udp",
|
||||
"username": "1730000000:ab12cd34ef56",
|
||||
"credential": "Base64(HMAC-SHA1(sharedSecret, username))" }
|
||||
],
|
||||
"expiresAt": 1730003600,
|
||||
"ttlSeconds": 3600
|
||||
}
|
||||
```
|
||||
|
||||
> 客户端应将 `iceServers` 直接传入 `RTCPeerConnection` 配置;凭证过期后重新 GET 本接口刷新。
|
||||
|
||||
#### 骚扰举报(P2 风控)
|
||||
被控端遭遇骚扰时可举报某主控端账号,管理员在 `/api/admin/abuse-reports` 查看并处理。
|
||||
|
||||
| 方法 | 路径 | 令牌 | 说明 |
|
||||
| --- | --- | --- | --- |
|
||||
| POST | `/api/client/device/report` | 设备 | 举报:`{"username":"alice","reason":"持续骚扰"}` |
|
||||
| GET | `/api/admin/abuse-reports` | 管理员 | 列表,支持 `status=PENDING` / `reportedUserId=` 过滤 |
|
||||
| POST | `/api/admin/abuse-reports/{id}/handle` | 管理员 | 处理:`{"status":"HANDLED"}` 或 `DISMISSED` |
|
||||
|
||||
---
|
||||
|
||||
## 2. 被控端接口(WebRTCControlled)
|
||||
|
||||
被控端为系统签名应用(`android:sharedUserId="android.uid.system"`),
|
||||
可通过 `DeviceUtils.getSerial()` 稳定获取 SN,但无法登录账号。
|
||||
因此采用「SN + 内置密钥 HMAC」激活。
|
||||
|
||||
### 2.1 首次激活
|
||||
|
||||
```
|
||||
POST /api/device/provision
|
||||
|
||||
{
|
||||
"sn": "ABC123456789",
|
||||
"model": "Pixel 3",
|
||||
"nonce": "550e8400-e29b-41d4-a716-446655440000",
|
||||
"timestamp": 1730000000,
|
||||
"hmac": "3f2a...(64 位小写 hex)"
|
||||
}
|
||||
```
|
||||
|
||||
**HMAC 计算**:
|
||||
```
|
||||
hmac = HexLowercase( HMAC-SHA256( DEVICE_PROVISION_SECRET, sn + "|" + nonce + "|" + timestamp ) )
|
||||
```
|
||||
- `timestamp`:Unix **秒**(非毫秒),与服务端偏差超过 300 秒即拒绝
|
||||
- `nonce`:每次请求唯一(建议 UUID),服务端做重放检测
|
||||
- `DEVICE_PROVISION_SECRET`:内置于被控端 APK,建议放 NDK 层并配合 R8 混淆
|
||||
|
||||
响应 `200`:
|
||||
```json
|
||||
{
|
||||
"deviceUid": "dev_xxxxxxxxxxxxxxxxxxxxxx",
|
||||
"deviceSecret": "AbCdEf...",
|
||||
"notice": "deviceSecret 仅返回一次,请立即安全存储"
|
||||
}
|
||||
```
|
||||
|
||||
> **`deviceSecret` 仅此一次明文返回**,必须立即写入 Android Keystore
|
||||
> (或 EncryptedSharedPreferences)。丢失后只能重新激活。
|
||||
|
||||
**幂等性**:同一 SN 重复激活会复用同一 `deviceUid`,但会**轮换 deviceSecret 并使旧令牌立即失效**。
|
||||
因此客户端应先检查本地是否已有 `deviceSecret`,**有则跳过激活**,避免自己把自己踢下线。
|
||||
|
||||
限流:单 IP 每小时 10 次。
|
||||
|
||||
### 2.2 换取访问令牌
|
||||
|
||||
```
|
||||
POST /api/device/token
|
||||
|
||||
{ "deviceUid": "dev_xxx", "deviceSecret": "AbCdEf..." }
|
||||
```
|
||||
|
||||
```json
|
||||
{
|
||||
"accessToken": "eyJhbGc...",
|
||||
"refreshToken": null,
|
||||
"expiresIn": 900,
|
||||
"principalId": "dev_xxx",
|
||||
"displayName": "Pixel 3"
|
||||
}
|
||||
```
|
||||
|
||||
设备令牌**没有 refreshToken**,过期后直接用 `deviceSecret` 重新换取即可。
|
||||
建议在剩余有效期不足 1/3 时提前换取。
|
||||
|
||||
限流:单 IP 每小时 60 次。
|
||||
|
||||
### 2.3 被控端启动流程
|
||||
|
||||
```
|
||||
读取本地 deviceSecret
|
||||
├─ 不存在 → getSerial() → POST /api/device/provision → 存储 deviceUid + deviceSecret
|
||||
└─ 已存在 → 跳过
|
||||
↓
|
||||
POST /api/device/token → accessToken
|
||||
↓
|
||||
WebSocket 握手(携带 accessToken)
|
||||
↓
|
||||
收到 REGISTER_SUCCESS,其中 deviceId 即服务端分配的 deviceUid
|
||||
```
|
||||
|
||||
> **重要**:被控端界面上原本让用户手输的「设备ID」输入框应当**移除或改为只读展示**。
|
||||
> 设备身份现在完全由 SN 激活决定,不再由用户输入。
|
||||
|
||||
---
|
||||
|
||||
## 3. WebSocket 握手改造(所有端)
|
||||
|
||||
### 3.1 令牌传递方式
|
||||
|
||||
**方式 A —— 推荐(Web 端必用)**:子协议
|
||||
```
|
||||
Sec-WebSocket-Protocol: signal.v1, auth.<token>
|
||||
```
|
||||
服务端会回显 `signal.v1` 完成协商,**不会回显含令牌的那一项**。
|
||||
优点:令牌不进入 URL,不会被网关/代理写入访问日志。
|
||||
|
||||
**方式 B —— 原生客户端可用**:请求头
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
**方式 C —— 兼容,不推荐**:查询参数
|
||||
```
|
||||
wss://www.ttstd.com/signal?token=<token>
|
||||
```
|
||||
服务端会打印告警。仅在前两种都无法实现时使用。
|
||||
|
||||
### 3.2 各端实现要点
|
||||
|
||||
**Android(OkHttp)** — 两端通用:
|
||||
```java
|
||||
Request request = new Request.Builder()
|
||||
.url(serverUrl)
|
||||
.addHeader("Authorization", "Bearer " + accessToken)
|
||||
.build();
|
||||
client.newWebSocket(request, listener);
|
||||
```
|
||||
|
||||
**Web(浏览器 WebSocket)** — 浏览器无法设置请求头,**必须用子协议**:
|
||||
```js
|
||||
const ws = new WebSocket(serverUrl, ['signal.v1', 'auth.' + accessToken]);
|
||||
```
|
||||
|
||||
**Flutter(web_socket_channel)**:
|
||||
```dart
|
||||
// IOWebSocketChannel 支持 headers(移动端)
|
||||
final channel = IOWebSocketChannel.connect(
|
||||
Uri.parse(serverUrl),
|
||||
headers: {'Authorization': 'Bearer $accessToken'},
|
||||
);
|
||||
// 若需兼容 Flutter Web,改用子协议:
|
||||
// WebSocketChannel.connect(uri, protocols: ['signal.v1', 'auth.$accessToken']);
|
||||
```
|
||||
|
||||
**iOS(URLSessionWebSocketTask)**:
|
||||
```swift
|
||||
var request = URLRequest(url: url)
|
||||
request.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization")
|
||||
let task = session.webSocketTask(with: request)
|
||||
```
|
||||
|
||||
### 3.3 连接建立后
|
||||
|
||||
**不再需要发送 `REGISTER`**。认证通过后服务端自动完成注册并主动下发:
|
||||
|
||||
```json
|
||||
{
|
||||
"type": "REGISTER_SUCCESS",
|
||||
"deviceId": "ctl_ses_xxx",
|
||||
"deviceType": "CONTROLLER",
|
||||
"displayName": "alice"
|
||||
}
|
||||
```
|
||||
|
||||
客户端应以此消息中的 `deviceId` 作为自己的信令 ID。
|
||||
|
||||
> 旧客户端继续发送 `REGISTER` 不会报错(服务端仅回显同样的响应),但已无实际意义。
|
||||
|
||||
### 3.4 身份字段不再可信
|
||||
|
||||
消息中的 `fromDeviceId` 和 `deviceType` **会被服务端用鉴权结果强制覆盖**。
|
||||
客户端填写任意值都无效(不一致时服务端记录 WARN 日志)。发送消息时可继续填写,也可省略。
|
||||
|
||||
### 3.5 关闭码处理
|
||||
|
||||
| 关闭码 | 含义 | 客户端应对 |
|
||||
|---|---|---|
|
||||
| 4001 | 未认证 / 令牌无效 | 刷新令牌后重连;刷新失败则跳登录 |
|
||||
| 4003 | 被强制下线(封禁/踢出/设备禁用) | **停止自动重连**,提示用户,清空令牌跳登录 |
|
||||
| 1000/1001 | 正常关闭 | 按既有逻辑 |
|
||||
| 其他 | 网络异常 | 指数退避重连 |
|
||||
|
||||
**特别注意**:被控端 `WebRTCControlled` 现有的自动重连是指数退避(2s 起,上限 30s)。
|
||||
收到 **4003** 时必须**关闭自动重连**,否则会造成无效重连风暴。
|
||||
|
||||
### 3.6 FORCE_LOGOUT 消息
|
||||
|
||||
服务端在强制断开前会先下发:
|
||||
```json
|
||||
{ "type": "FORCE_LOGOUT", "payload": "账号已被封禁:违规操作" }
|
||||
```
|
||||
客户端应展示 `payload` 给用户,然后清理本地状态。
|
||||
|
||||
### 3.7 握手限流
|
||||
|
||||
单 IP **60 秒内最多 30 次**握手,超限返回 **429**。
|
||||
各端重连必须使用指数退避,避免触发。
|
||||
|
||||
---
|
||||
|
||||
## 4. 各端改造清单
|
||||
|
||||
### 4.1 WebRTCControlled(被控端)
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 新增 | HTTP 客户端(复用已有 OkHttp)调用 provision / token |
|
||||
| 新增 | `DeviceUtils.getSerial()` 接入激活流程(代码已存在,当前被注释) |
|
||||
| 新增 | Keystore / EncryptedSharedPreferences 存储 `deviceUid` + `deviceSecret` |
|
||||
| 新增 | 令牌过期前自动换取 |
|
||||
| 修改 | `WebSocketClient` 构造增加 token,握手加 `Authorization` 头 |
|
||||
| 修改 | 移除/只读化「设备ID」输入框(`activity_main.xml` 中硬编码的 `981964879` 应删除) |
|
||||
| 修改 | 收到 4003 时停止自动重连 |
|
||||
| 修改 | 服务器地址持久化(当前每次启动重置) |
|
||||
|
||||
### 4.2 WebRTCController(主控端 Android)
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 新增 | 登录界面(作为 launcher,登录后再进主界面) |
|
||||
| 新增 | HTTP 客户端调用 login / refresh |
|
||||
| 新增 | EncryptedSharedPreferences 存 refreshToken |
|
||||
| 新增 | 令牌自动刷新(加单飞锁) |
|
||||
| 修改 | `WebSocketClient` 握手加 `Authorization` 头 |
|
||||
| 修改 | 移除「设备ID」输入框,改用 `REGISTER_SUCCESS` 返回值 |
|
||||
| 修改 | **补充心跳与重连**(当前完全没有,仅被控端有) |
|
||||
| 修改 | 处理 4001 / 4003 / FORCE_LOGOUT |
|
||||
|
||||
### 4.3 WebRTCControllerWeb
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 新增 | 登录页组件 + `authStore`(沿用现有 `reactive` 风格,无需引入 Pinia) |
|
||||
| 新增 | `fetch` 封装(当前无任何 HTTP 能力),含 401 自动刷新 |
|
||||
| 新增 | `sessionStorage` 存 refreshToken |
|
||||
| 修改 | `SignalingClient.js` 用**子协议**传令牌(浏览器无法设请求头) |
|
||||
| 修改 | 移除 `App.vue` 中 `'web-' + random` 的临时 deviceId 生成 |
|
||||
| 修改 | 处理关闭码与 FORCE_LOGOUT |
|
||||
|
||||
### 4.4 webrtc_controller_flutter
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 新增依赖 | `http`(或 `dio`)+ `flutter_secure_storage` |
|
||||
| 新增 | 登录页 + 令牌管理 |
|
||||
| 修改 | `signaling_client.dart` 改用 `IOWebSocketChannel.connect(..., headers:)` |
|
||||
| 修改 | 移除 `DeviceUtils.getSerialNumber()` 作为 deviceId 的逻辑(主控端身份来自账号) |
|
||||
| 修改 | 处理关闭码与 FORCE_LOGOUT |
|
||||
|
||||
### 4.5 webrtc_controller_ios
|
||||
|
||||
| 项 | 说明 |
|
||||
|---|---|
|
||||
| 新增 | 登录视图 + `AuthService`(URLSession HTTP) |
|
||||
| 新增 | **Keychain** 存 refreshToken(当前 `DeviceUtils` 用的是 UserDefaults,不适合存凭据) |
|
||||
| 修改 | `SignalingClient.swift` 用 `URLRequest` + `Authorization` 头 |
|
||||
| 修改 | 移除 `DeviceUtils.deviceId()` 作为信令 ID 的逻辑 |
|
||||
| 修改 | 处理关闭码与 FORCE_LOGOUT |
|
||||
|
||||
---
|
||||
|
||||
## 5. 联调顺序建议
|
||||
|
||||
1. **先确认反向代理**已放行 `/api/**`
|
||||
2. 服务端配置 `JWT_SECRET`、`DEVICE_PROVISION_SECRET`、`BOOTSTRAP_ADMIN_PASSWORD`
|
||||
3. 用 curl 验证 login / provision 流程跑通
|
||||
4. 改造**被控端**(无 UI 依赖,最容易验证)
|
||||
5. 改造**Web 端**(调试最快,可验证子协议方式)
|
||||
6. 改造 Android 主控端 → Flutter → iOS
|
||||
|
||||
### curl 自测示例
|
||||
|
||||
```bash
|
||||
# 注册
|
||||
curl -X POST https://www.ttstd.com/api/auth/register \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"tester","password":"Passw0rd!"}'
|
||||
|
||||
# 登录
|
||||
curl -X POST https://www.ttstd.com/api/auth/login \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"username":"tester","password":"Passw0rd!"}'
|
||||
|
||||
# 设备激活(timestamp 用当前 Unix 秒,hmac 需按 §2.1 计算)
|
||||
curl -X POST https://www.ttstd.com/api/device/provision \
|
||||
-H 'Content-Type: application/json' \
|
||||
-d '{"sn":"TESTSN001","model":"test","nonce":"'$(uuidgen)'","timestamp":'$(date +%s)',"hmac":"..."}'
|
||||
```
|
||||
284
WebRTCSignalServer/docs/schema.sql
Normal file
284
WebRTCSignalServer/docs/schema.sql
Normal file
@@ -0,0 +1,284 @@
|
||||
-- =============================================================
|
||||
-- WebRTCSignalServer 数据库建表脚本 (MySQL 8.0+)
|
||||
--
|
||||
-- 【请手动执行,服务端代码当前仍为内存实现,不会自动建表】
|
||||
--
|
||||
-- 执行方式:
|
||||
-- mysql -u root -p < schema.sql
|
||||
--
|
||||
-- 字符集统一使用 utf8mb4 / utf8mb4_0900_ai_ci
|
||||
-- 所有时间字段使用 DATETIME(3),由应用层写入 UTC 时间
|
||||
-- =============================================================
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS `webrtc_signal`
|
||||
DEFAULT CHARACTER SET utf8mb4
|
||||
DEFAULT COLLATE utf8mb4_0900_ai_ci;
|
||||
|
||||
USE `webrtc_signal`;
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 1. 主控端用户账号
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `app_user` (
|
||||
`user_id` VARCHAR(32) NOT NULL COMMENT '用户ID,格式 usr_<20位Base62>',
|
||||
`username` VARCHAR(32) NOT NULL COMMENT '登录名',
|
||||
`password_hash` VARCHAR(100) NOT NULL COMMENT 'BCrypt 哈希(cost=12)',
|
||||
`status` VARCHAR(16) NOT NULL DEFAULT 'ACTIVE'
|
||||
COMMENT '账号状态:ACTIVE/SUSPENDED/BANNED',
|
||||
`status_until` DATETIME(3) NULL COMMENT '临时封禁到期时间,NULL 表示永久或未封禁',
|
||||
`status_reason` VARCHAR(255) NULL COMMENT '封禁原因',
|
||||
`token_version` BIGINT NOT NULL DEFAULT 1
|
||||
COMMENT '凭据版本号,递增后所有已签发令牌立即失效',
|
||||
`is_admin` TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否管理员',
|
||||
|
||||
-- 双因子认证
|
||||
`totp_secret` VARCHAR(64) NULL COMMENT 'TOTP 密钥(Base32),NULL 表示未启用',
|
||||
`totp_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'TOTP 是否已完成绑定',
|
||||
|
||||
-- 防爆破
|
||||
`failed_attempts` INT NOT NULL DEFAULT 0 COMMENT '连续登录失败次数',
|
||||
`locked_until` DATETIME(3) NULL COMMENT '锁定截止时间',
|
||||
|
||||
`last_login_at` DATETIME(3) NULL,
|
||||
`last_login_ip` VARCHAR(64) NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
|
||||
ON UPDATE CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`user_id`),
|
||||
UNIQUE KEY `uk_username` (`username`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB COMMENT='主控端用户账号';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 2. 密码历史(防止重复使用近期密码)
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `password_history` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`user_id` VARCHAR(32) NOT NULL,
|
||||
`password_hash` VARCHAR(100) NOT NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_user_created` (`user_id`, `created_at` DESC),
|
||||
CONSTRAINT `fk_pwdhist_user` FOREIGN KEY (`user_id`)
|
||||
REFERENCES `app_user` (`user_id`) ON DELETE CASCADE
|
||||
) ENGINE=InnoDB COMMENT='密码历史,建议保留最近 5 条';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 3. 登录会话(刷新令牌上下文)
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `login_session` (
|
||||
`session_id` VARCHAR(32) NOT NULL COMMENT '会话ID,格式 ses_<20位Base62>',
|
||||
`principal_id` VARCHAR(32) NOT NULL COMMENT '主体ID(user_id)',
|
||||
`principal_type` VARCHAR(16) NOT NULL DEFAULT 'USER' COMMENT 'USER/DEVICE',
|
||||
`refresh_token_hash` VARCHAR(64) NOT NULL COMMENT '当前刷新令牌的 SHA-256,轮转后更新',
|
||||
`refresh_expires_at` DATETIME(3) NOT NULL,
|
||||
`ip` VARCHAR(64) NULL,
|
||||
`user_agent` VARCHAR(256) NULL,
|
||||
`revoked` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`revoked_reason` VARCHAR(255) NULL,
|
||||
`last_seen_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`session_id`),
|
||||
KEY `idx_principal` (`principal_id`, `revoked`),
|
||||
KEY `idx_expires` (`refresh_expires_at`)
|
||||
) ENGINE=InnoDB COMMENT='登录会话,支持踢线与刷新令牌轮转';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 4. SN 白名单(出厂/部署时批量导入)
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `allowed_device` (
|
||||
`sn` VARCHAR(64) NOT NULL COMMENT '设备序列号',
|
||||
`batch` VARCHAR(64) NULL COMMENT '批次标识',
|
||||
`note` VARCHAR(255) NULL,
|
||||
`imported_by` VARCHAR(64) NULL,
|
||||
`imported_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`sn`),
|
||||
KEY `idx_batch` (`batch`)
|
||||
) ENGINE=InnoDB COMMENT='允许激活的 SN 白名单';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 5. 被控端设备
|
||||
-- SN 仅作内部主键,对外一律使用高熵不可枚举的 device_uid
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `device` (
|
||||
`device_uid` VARCHAR(32) NOT NULL COMMENT '对外设备ID,格式 dev_<22位Base62>',
|
||||
`sn` VARCHAR(64) NOT NULL COMMENT '设备序列号(内部使用,勿对外暴露)',
|
||||
`secret_hash` VARCHAR(64) NOT NULL COMMENT 'deviceSecret 的 SHA-256',
|
||||
`model` VARCHAR(64) NULL,
|
||||
`status` VARCHAR(16) NOT NULL DEFAULT 'ACTIVE'
|
||||
COMMENT 'ACTIVE/SUSPENDED/BANNED',
|
||||
`status_until` DATETIME(3) NULL,
|
||||
`status_reason` VARCHAR(255) NULL,
|
||||
`token_version` BIGINT NOT NULL DEFAULT 1,
|
||||
|
||||
-- 防骚扰配置(P1 阶段使用)
|
||||
`strict_mode` TINYINT(1) NOT NULL DEFAULT 1
|
||||
COMMENT '严格模式:仅允许已绑定账号连接',
|
||||
`quiet_start` TIME NULL COMMENT '勿扰时段开始',
|
||||
`quiet_end` TIME NULL COMMENT '勿扰时段结束',
|
||||
|
||||
`provisioned_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`last_online_at` DATETIME(3) NULL,
|
||||
|
||||
PRIMARY KEY (`device_uid`),
|
||||
UNIQUE KEY `uk_sn` (`sn`),
|
||||
KEY `idx_status` (`status`)
|
||||
) ENGINE=InnoDB COMMENT='被控端设备身份';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 6. 设备激活 nonce(防重放)
|
||||
-- 建议配合定时任务清理过期记录,或改用 Redis
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `provision_nonce` (
|
||||
`nonce` VARCHAR(64) NOT NULL,
|
||||
`sn` VARCHAR(64) NULL,
|
||||
`expires_at` DATETIME(3) NOT NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`nonce`),
|
||||
KEY `idx_expires` (`expires_at`)
|
||||
) ENGINE=InnoDB COMMENT='激活请求 nonce,用于防重放';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 7. 设备绑定关系(P1 阶段:主控端唯一寻址凭证)
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `device_binding` (
|
||||
`binding_id` VARCHAR(36) NOT NULL COMMENT 'UUID',
|
||||
`device_uid` VARCHAR(32) NOT NULL,
|
||||
`user_id` VARCHAR(32) NOT NULL,
|
||||
`role` VARCHAR(16) NOT NULL DEFAULT 'OPERATOR'
|
||||
COMMENT 'OWNER/OPERATOR/VIEWER',
|
||||
`alias` VARCHAR(64) NULL COMMENT '主控端自定义备注名',
|
||||
`status` VARCHAR(16) NOT NULL DEFAULT 'ACTIVE' COMMENT 'ACTIVE/REVOKED',
|
||||
`bound_by` VARCHAR(32) NULL,
|
||||
`bound_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`expire_at` DATETIME(3) NULL COMMENT '绑定过期时间,NULL 表示长期有效',
|
||||
|
||||
PRIMARY KEY (`binding_id`),
|
||||
UNIQUE KEY `uk_device_user` (`device_uid`, `user_id`),
|
||||
KEY `idx_user_status` (`user_id`, `status`),
|
||||
KEY `idx_device_status` (`device_uid`, `status`)
|
||||
) ENGINE=InnoDB COMMENT='设备与账号绑定关系';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 8. 配对码(P1 阶段:建立绑定)
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `pairing_code` (
|
||||
`code_hash` VARCHAR(64) NOT NULL COMMENT '配对码的 SHA-256',
|
||||
`device_uid` VARCHAR(32) NOT NULL,
|
||||
`attempts` INT NOT NULL DEFAULT 0 COMMENT '错误尝试次数,超限即失效',
|
||||
`used` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`expires_at` DATETIME(3) NOT NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`code_hash`),
|
||||
KEY `idx_device` (`device_uid`),
|
||||
KEY `idx_expires` (`expires_at`)
|
||||
) ENGINE=InnoDB COMMENT='一次性配对码';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 9. 被控端黑名单(P1 阶段:拉黑骚扰账号)
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `device_blacklist` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`device_uid` VARCHAR(32) NOT NULL,
|
||||
`blocked_user_id` VARCHAR(32) NOT NULL,
|
||||
`reason` VARCHAR(255) NULL,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
UNIQUE KEY `uk_device_user` (`device_uid`, `blocked_user_id`),
|
||||
KEY `idx_device` (`device_uid`)
|
||||
) ENGINE=InnoDB COMMENT='被控端黑名单';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 10. 连接会话记录(WebRTC 通话审计)
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `connection_session` (
|
||||
`session_id` VARCHAR(36) NOT NULL,
|
||||
`binding_id` VARCHAR(36) NULL,
|
||||
`device_uid` VARCHAR(32) NOT NULL,
|
||||
`user_id` VARCHAR(32) NOT NULL,
|
||||
`auth_type` VARCHAR(16) NULL COMMENT 'NONE/CODE/PASSWORD',
|
||||
`started_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
`ended_at` DATETIME(3) NULL,
|
||||
`end_reason` VARCHAR(64) NULL,
|
||||
|
||||
PRIMARY KEY (`session_id`),
|
||||
KEY `idx_device_started` (`device_uid`, `started_at` DESC),
|
||||
KEY `idx_user_started` (`user_id`, `started_at` DESC)
|
||||
) ENGINE=InnoDB COMMENT='远程控制会话记录';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 11. 审计日志(只追加,不修改)
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `audit_log` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`actor_type` VARCHAR(16) NOT NULL COMMENT 'USER/DEVICE/ADMIN/SYSTEM',
|
||||
`actor_id` VARCHAR(64) NULL,
|
||||
`action` VARCHAR(64) NOT NULL
|
||||
COMMENT 'LOGIN/LOGIN_FAILED/LOGOUT/REGISTER/PROVISION/BAN/UNBAN/KICK/BIND/UNBIND/BLACKLIST...',
|
||||
`target_type` VARCHAR(16) NULL,
|
||||
`target_id` VARCHAR(64) NULL,
|
||||
`result` VARCHAR(16) NOT NULL DEFAULT 'SUCCESS' COMMENT 'SUCCESS/FAILURE',
|
||||
`ip` VARCHAR(64) NULL,
|
||||
`user_agent` VARCHAR(256) NULL,
|
||||
`detail` VARCHAR(512) NULL COMMENT '注意:敏感信息需脱敏后再写入',
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_actor` (`actor_id`, `created_at` DESC),
|
||||
KEY `idx_action` (`action`, `created_at` DESC),
|
||||
KEY `idx_created` (`created_at` DESC)
|
||||
) ENGINE=InnoDB COMMENT='安全审计日志';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 12. 骚扰举报(P2 阶段风控)
|
||||
-- =============================================================
|
||||
CREATE TABLE IF NOT EXISTS `abuse_report` (
|
||||
`id` BIGINT NOT NULL AUTO_INCREMENT,
|
||||
`device_uid` VARCHAR(32) NOT NULL COMMENT '举报方(被控端)',
|
||||
`reported_user_id` VARCHAR(32) NOT NULL COMMENT '被举报账号',
|
||||
`reason` VARCHAR(255) NULL,
|
||||
`handled` TINYINT(1) NOT NULL DEFAULT 0,
|
||||
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
|
||||
PRIMARY KEY (`id`),
|
||||
KEY `idx_reported` (`reported_user_id`, `created_at` DESC),
|
||||
KEY `idx_handled` (`handled`)
|
||||
) ENGINE=InnoDB COMMENT='骚扰举报记录';
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 清理建议(可配置为 MySQL Event 或应用层定时任务)
|
||||
-- =============================================================
|
||||
-- DELETE FROM provision_nonce WHERE expires_at < NOW();
|
||||
-- DELETE FROM pairing_code WHERE expires_at < NOW();
|
||||
-- DELETE FROM login_session WHERE refresh_expires_at < NOW() OR revoked = 1;
|
||||
-- DELETE FROM audit_log WHERE created_at < DATE_SUB(NOW(), INTERVAL 180 DAY);
|
||||
|
||||
|
||||
-- =============================================================
|
||||
-- 初始管理员账号
|
||||
-- =============================================================
|
||||
-- 不在此处插入明文密码。请通过环境变量启动服务自动创建:
|
||||
-- BOOTSTRAP_ADMIN_USERNAME=admin
|
||||
-- BOOTSTRAP_ADMIN_PASSWORD=<强密码>
|
||||
-- 或注册后手动提升权限:
|
||||
-- UPDATE app_user SET is_admin = 1 WHERE username = 'admin';
|
||||
Reference in New Issue
Block a user