- 添加API客户端、加密存储和provision/token激活逻辑 - WebSocket改用Bearer令牌认证,移除REGISTER请求 - 设备ID改为服务端下发,支持令牌刷新和强制下线处理 - 新增deviceSecret加密存储和accessToken自动刷新 - 更新设备ID获取方式为出厂SN,添加安全存储依赖
8.7 KiB
8.7 KiB
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 握手鉴权
令牌传递方式(按优先级)
-
推荐(不会被写入访问日志):
Sec-WebSocket-Protocol: signal.v1, auth.<token>服务端会回显
signal.v1完成协商,绝不回显携带令牌的那一项。 -
非浏览器客户端:
Authorization: Bearer <token> -
兼容方式(不推荐,令牌可能进入网关/代理日志,服务端会打印告警):
/ws/signal?token=<token>
服务端行为
- 握手阶段完成鉴权,失败直接返回 401,不建立连接
- 单 IP 握手限流:60 秒内最多 30 次,超限返回 429
- 认证通过后,连接建立即自动完成注册并下发
REGISTER_SUCCESS, 客户端无需再发送REGISTER(旧客户端仍发送时会收到同样的响应,不会报错) - 消息中的
fromDeviceId/deviceType一律被服务端鉴权结果覆盖, 客户端伪造无效(不一致时记录 WARN 日志)
关闭码
| 码 | 含义 |
|---|---|
| 4001 | 未认证 / 认证信息缺失 |
| 4003 | 被强制下线(封禁、踢出、设备禁用) |
被强制下线前,服务端会先下发一条消息:
{ "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仍返回全部在线设备,接入绑定关系后应改为仅返回已绑定设备。