feat(controlled): 实现设备激活与安全认证流程
- 添加API客户端、加密存储和provision/token激活逻辑 - WebSocket改用Bearer令牌认证,移除REGISTER请求 - 设备ID改为服务端下发,支持令牌刷新和强制下线处理 - 新增deviceSecret加密存储和accessToken自动刷新 - 更新设备ID获取方式为出厂SN,添加安全存储依赖
This commit is contained in:
218
WebRTCSignalServer/SECURITY.md
Normal file
218
WebRTCSignalServer/SECURITY.md
Normal file
@@ -0,0 +1,218 @@
|
||||
# WebRTCSignalServer 鉴权与账号机制说明
|
||||
|
||||
本文档描述已落地的 **WebSocket 握手鉴权** 与 **账号机制**(方案 P0 阶段)。
|
||||
|
||||
> **重大变更**:`/ws/signal` 不再接受匿名连接。所有客户端必须先获取访问令牌,
|
||||
> 并在握手时携带,否则握手将以 HTTP 401 被拒绝。
|
||||
|
||||
---
|
||||
|
||||
## 1. 身份模型
|
||||
|
||||
两端能力不对称,因此采用双轨身份:
|
||||
|
||||
| | 主控端 | 被控端 |
|
||||
|---|---|---|
|
||||
| 身份根 | 用户账号(用户名 + 密码) | 设备 SN(系统签名应用可靠获取) |
|
||||
| 凭据 | accessToken + refreshToken | deviceSecret → 短期 deviceToken |
|
||||
| 信令 ID | `ctl_<sessionId>`(服务端派生) | `dev_<22位随机>`(高熵不可枚举) |
|
||||
| 信令角色 | `CONTROLLER` | `CONTROLLED` |
|
||||
|
||||
**关键设计**:SN 只作服务端内部主键,绝不作为公网可寻址 ID 暴露;
|
||||
对外一律使用随机生成的 `deviceUid`,杜绝通过猜测 SN 定位并骚扰被控端。
|
||||
|
||||
---
|
||||
|
||||
## 2. 主控端接入流程
|
||||
|
||||
```
|
||||
POST /api/auth/register { username, password } # 可关闭
|
||||
POST /api/auth/login { username, password } # -> accessToken / refreshToken
|
||||
↓
|
||||
WebSocket 握手(携带 accessToken)
|
||||
↓
|
||||
POST /api/auth/refresh { refreshToken } # accessToken 过期前刷新
|
||||
```
|
||||
|
||||
### 接口一览
|
||||
|
||||
| 方法 | 路径 | 说明 | 需认证 |
|
||||
|---|---|---|---|
|
||||
| POST | `/api/auth/register` | 注册(受 `ACCOUNT_REGISTRATION_ENABLED` 控制) | 否 |
|
||||
| POST | `/api/auth/login` | 登录,返回令牌对 | 否 |
|
||||
| POST | `/api/auth/refresh` | 刷新并轮转令牌 | 否 |
|
||||
| POST | `/api/auth/logout` | 登出当前会话 | 是 |
|
||||
| POST | `/api/auth/logout-all` | 全端登出 | 是 |
|
||||
| POST | `/api/auth/change-password` | 改密(成功后强制全端重登) | 是 |
|
||||
| GET | `/api/auth/me` | 查询当前身份 | 是 |
|
||||
| GET | `/api/auth/sessions` | 查询本账号活跃会话 | 是 |
|
||||
|
||||
认证方式:`Authorization: Bearer <accessToken>`
|
||||
|
||||
---
|
||||
|
||||
## 3. 被控端接入流程
|
||||
|
||||
被控端无法登录账号,改用「SN + 内置共享密钥 HMAC」激活:
|
||||
|
||||
```
|
||||
POST /api/device/provision { sn, model, nonce, timestamp, hmac }
|
||||
↓ 返回 deviceUid + deviceSecret(仅此一次明文返回)
|
||||
↓ deviceSecret 存入 Android Keystore
|
||||
POST /api/device/token { deviceUid, deviceSecret }
|
||||
↓ 返回短期 deviceToken(默认 15 分钟)
|
||||
WebSocket 握手(携带 deviceToken)
|
||||
```
|
||||
|
||||
### HMAC 计算方式
|
||||
|
||||
```
|
||||
hmac = HexLowercase( HMAC-SHA256( DEVICE_PROVISION_SECRET, sn + "|" + nonce + "|" + timestamp ) )
|
||||
```
|
||||
|
||||
- `timestamp`:Unix 秒;与服务端偏差超过 `provision-skew-seconds`(默认 300s)即拒绝
|
||||
- `nonce`:每次激活唯一(建议 UUID),服务端做重放检测
|
||||
- `DEVICE_PROVISION_SECRET`:内置于系统签名 APK,建议配合 R8/NDK 加固
|
||||
|
||||
激活安全校验链:`时间戳窗口 → nonce 防重放 → HMAC 签名 → SN 白名单`
|
||||
|
||||
同一 SN 重复激活会**轮换 deviceSecret 并使旧令牌立即失效**(记录 WARN 日志)。
|
||||
|
||||
---
|
||||
|
||||
## 4. WebSocket 握手鉴权
|
||||
|
||||
### 令牌传递方式(按优先级)
|
||||
|
||||
1. **推荐**(不会被写入访问日志):
|
||||
```
|
||||
Sec-WebSocket-Protocol: signal.v1, auth.<token>
|
||||
```
|
||||
服务端会回显 `signal.v1` 完成协商,**绝不回显携带令牌的那一项**。
|
||||
|
||||
2. 非浏览器客户端:
|
||||
```
|
||||
Authorization: Bearer <token>
|
||||
```
|
||||
|
||||
3. 兼容方式(**不推荐**,令牌可能进入网关/代理日志,服务端会打印告警):
|
||||
```
|
||||
/ws/signal?token=<token>
|
||||
```
|
||||
|
||||
### 服务端行为
|
||||
|
||||
- 握手阶段完成鉴权,失败直接返回 **401**,不建立连接
|
||||
- 单 IP 握手限流:60 秒内最多 30 次,超限返回 **429**
|
||||
- 认证通过后,连接建立即自动完成注册并下发 `REGISTER_SUCCESS`,
|
||||
**客户端无需再发送 `REGISTER`**(旧客户端仍发送时会收到同样的响应,不会报错)
|
||||
- 消息中的 `fromDeviceId` / `deviceType` **一律被服务端鉴权结果覆盖**,
|
||||
客户端伪造无效(不一致时记录 WARN 日志)
|
||||
|
||||
### 关闭码
|
||||
|
||||
| 码 | 含义 |
|
||||
|---|---|
|
||||
| 4001 | 未认证 / 认证信息缺失 |
|
||||
| 4003 | 被强制下线(封禁、踢出、设备禁用) |
|
||||
|
||||
被强制下线前,服务端会先下发一条消息:
|
||||
```json
|
||||
{ "type": "FORCE_LOGOUT", "payload": "<原因>" }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. 封禁与强制下线
|
||||
|
||||
令牌失效采用 **凭据版本号(tokenVersion)** 机制:账号/设备被封禁时版本号递增,
|
||||
所有已签发的令牌**立即失效**,无需维护黑名单表。同时服务端主动关闭其 WebSocket 连接。
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
|---|---|---|
|
||||
| GET | `/api/admin/users` | 账号列表 |
|
||||
| GET | `/api/admin/users/{userId}/sessions` | 账号活跃会话 |
|
||||
| POST | `/api/admin/users/{userId}/ban` | 封禁(`durationSeconds` 缺省为永久) |
|
||||
| POST | `/api/admin/users/{userId}/unban` | 解封 |
|
||||
| POST | `/api/admin/users/{userId}/kick` | 全端强制下线(不改封禁状态) |
|
||||
| POST | `/api/admin/sessions/{sessionId}/kick` | 踢出单个会话 |
|
||||
| GET | `/api/admin/device-accounts` | 设备列表(SN 已脱敏) |
|
||||
| POST | `/api/admin/device-accounts/{uid}/disable` | 禁用设备 |
|
||||
| POST | `/api/admin/device-accounts/{uid}/enable` | 启用设备 |
|
||||
| POST | `/api/admin/device-allowlist` | 批量导入 SN 白名单 |
|
||||
|
||||
管理接口凭据(二选一):
|
||||
- `X-Admin-Token: <ADMIN_TOKEN>`(兼容既有管理后台)
|
||||
- `Authorization: Bearer <accessToken>` 且账号具备管理员角色
|
||||
|
||||
---
|
||||
|
||||
## 6. 已实现的安全措施
|
||||
|
||||
| 措施 | 说明 |
|
||||
|---|---|
|
||||
| 密码存储 | BCrypt(cost=12) |
|
||||
| 防账号枚举 | 用户名不存在时执行伪哈希抹平时间差,错误提示统一 |
|
||||
| 登录防爆破 | 连续失败 5 次锁定 15 分钟;单 IP 5 分钟最多 10 次 |
|
||||
| 刷新令牌轮转 | 每次刷新更换令牌;**检测到旧令牌复用即判定泄露并吊销整个会话** |
|
||||
| 令牌用途隔离 | access / refresh / device 三类令牌互不通用 |
|
||||
| 签名比较 | 使用 `MessageDigest.isEqual` 常量时间比较,防时序侧信道 |
|
||||
| 并发会话限制 | 默认最多 5 个,超限自动踢最旧会话 |
|
||||
| 激活防重放 | 时间戳窗口 + nonce 唯一性校验 |
|
||||
| 日志脱敏 | SN 仅打印后 4 位 |
|
||||
| 错误信息模糊化 | 对外统一提示,详细原因仅记录服务端日志 |
|
||||
| 空闲超时 | 90 秒(原 10 分钟),尽早回收失联连接 |
|
||||
| Origin 收敛 | 由 `WS_ALLOWED_ORIGINS` 配置,不再硬编码 `*` |
|
||||
|
||||
---
|
||||
|
||||
## 7. 配置项
|
||||
|
||||
**生产环境必须通过环境变量配置以下三项:**
|
||||
|
||||
| 环境变量 | 说明 |
|
||||
|---|---|
|
||||
| `JWT_SECRET` | JWT 签名密钥,**至少 32 字节**。未配置时随机生成,重启后令牌全部失效且多实例无法互认 |
|
||||
| `DEVICE_PROVISION_SECRET` | 设备激活共享密钥。**未配置时所有激活请求会被拒绝** |
|
||||
| `ADMIN_TOKEN` | 管理后台令牌 |
|
||||
|
||||
其他可选项:
|
||||
|
||||
| 环境变量 | 默认值 | 说明 |
|
||||
|---|---|---|
|
||||
| `BOOTSTRAP_ADMIN_USERNAME` | `admin` | 初始管理员用户名 |
|
||||
| `BOOTSTRAP_ADMIN_PASSWORD` | 空 | 初始管理员密码,**为空则不创建账号** |
|
||||
| `ACCOUNT_REGISTRATION_ENABLED` | `true` | 是否开放自助注册 |
|
||||
| `DEVICE_SN_ALLOWLIST_ENABLED` | `false` | 是否启用 SN 白名单 |
|
||||
| `WS_ALLOWED_ORIGINS` | `*` | WebSocket 允许来源,生产应收敛为具体域名 |
|
||||
|
||||
---
|
||||
|
||||
## 8. 客户端适配清单(待办)
|
||||
|
||||
服务端已强制鉴权,以下客户端需相应改造,否则将无法连接:
|
||||
|
||||
| 客户端 | 需要的改动 |
|
||||
|---|---|
|
||||
| `WebRTCControlled`(被控端) | 获取 SN → 调用 provision → 存储 deviceSecret → 换取 token → 握手携带 |
|
||||
| `WebRTCController`(主控端 Android) | 增加登录界面 → 保存令牌 → 握手携带 → 处理 401/4003 |
|
||||
| `WebRTCControllerWeb` | 同上,浏览器端建议使用 `Sec-WebSocket-Protocol` 方式 |
|
||||
| `webrtc_controller_flutter` | 同上 |
|
||||
| `webrtc_controller_ios` | 同上 |
|
||||
|
||||
各端还需统一处理:
|
||||
- `FORCE_LOGOUT` 消息与 4001 / 4003 关闭码(提示用户并跳转登录)
|
||||
- 访问令牌过期前静默刷新(建议在剩余 1/3 有效期时触发)
|
||||
- 不再发送 `REGISTER`(发送也不会出错,但已无意义)
|
||||
|
||||
---
|
||||
|
||||
## 9. 当前实现边界
|
||||
|
||||
- **存储为内存实现**:`AccountService` / `DeviceIdentityService` 使用
|
||||
`ConcurrentHashMap`,**服务重启后账号与设备数据会丢失**。接口已按持久化预留,
|
||||
后续可替换为 MySQL 仓储而不影响调用方。
|
||||
- **限流为单机实现**:多实例部署需替换为 Redis 计数,并通过 Pub/Sub 广播踢线事件。
|
||||
- **尚未实现**(属方案 P1/P2 阶段):绑定关系寻址、配对码、被控端拉黑、
|
||||
勿扰时段、审计日志、TOTP、TURN 短期凭据。
|
||||
当前 `DEVICE_LIST` 仍返回全部在线设备,**接入绑定关系后应改为仅返回已绑定设备**。
|
||||
Reference in New Issue
Block a user