Files
VibeCoding/WebRTCSignalServer/SECURITY.md
tongtongstudio 6eb2c7321a feat(controlled): 实现设备激活与安全认证流程
- 添加API客户端、加密存储和provision/token激活逻辑
- WebSocket改用Bearer令牌认证,移除REGISTER请求
- 设备ID改为服务端下发,支持令牌刷新和强制下线处理
- 新增deviceSecret加密存储和accessToken自动刷新
- 更新设备ID获取方式为出厂SN,添加安全存储依赖
2026-08-01 15:13:12 +08:00

8.7 KiB
Raw Blame History

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 ) )
  • timestampUnix 秒;与服务端偏差超过 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 被强制下线(封禁、踢出、设备禁用)

被强制下线前,服务端会先下发一条消息:

{ "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. 已实现的安全措施

措施 说明
密码存储 BCryptcost=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 仍返回全部在线设备,接入绑定关系后应改为仅返回已绑定设备