初始化、设备二维码与六位码登录
Agent 默认 Exchange 为 wss://exchange.ivjn.us,CLI/Console 管理 API 地址为 https://exchange.ivjn.us。原有配置优先,不会覆盖自建 Exchange 地址。
SaaS 注册与首次登录
remote-cli register --username alice --email alice@example.com
# 按隐藏输入提示设置密码,注册后向邮箱发送验证码。
remote-cli verify-email --username alice
# 隐藏输入邮件中的 8 位数字验证码,然后登录。
remote-cli login --email alice@example.com
# init 是 login 的别名,不带 --email 时交互输入邮箱和密码。
remote-cli init
# 原有令牌仍可使用:
remote-cli login --server https://exchange.ivjn.us --token-stdin
注册与登录默认连接 https://exchange.ivjn.us,自建服务可加 --server。登录以邮箱为账号身份,地址会去空格并转成小写,格式不合法会直接拒绝。用户名是注册时的工作区名称(3-64 位字母、数字或 ._-)。账号密码不设长度或字符格式限制;服务端仅保存密码哈希。自动化可使用 --password-stdin,不要把密码写进命令参数。新账号验证邮箱前不能登录。CLI 只保存返回的会话令牌,不保存密码,注册等待验证期间保留已有登录。
每次注册创建独立工作区,账号是该工作区的管理员。设备、成员、远程会话、指标和审计均隔离,不同工作区可使用相同设备 ID。六位码批准会把设备或新成员加入批准人的工作区。加入已有工作区应使用该管理员提供的令牌或六位码授权。
Android Console 同样支持注册、密码登录、已有令牌登录和六位码登录。密码只用于当前请求,凭证仍保存到 SecureStore。
密码登录会话有效期 30 天,每个账号最多 32 个有效会话;重复登录不挤掉其他客户端。POST /api/auth/logout 使用当前 Bearer 会话令牌注销,已建立的 WebSocket 最多 5 秒内关闭;到期后重新登录。设备与原有运维令牌保留原生命周期。
纯令牌身份没有密码账号,不能通过邮箱找回。当前不包含计费或跨实例 WebSocket 路由。
邮箱验证与密码找回
remote-cli resend-verification --email alice@example.com
remote-cli verify-email --username alice
remote-cli forgot-password --email alice@example.com
remote-cli reset-password --email alice@example.com
# 按提示输入邮件验证码、新密码,再重新登录。
# 已登录的旧密码账号绑定或更换邮箱,需要再次验证当前密码。
remote-cli set-email --email alice@example.com
remote-cli verify-email --username alice
验证与找回使用不同用途的 8 位数字验证码,15 分钟有效,连续输错 5 次后失效,重新发信会使旧码失效;每个账号每种用途一分钟内只能发一次。验证与重置命令可用 --code 自动化,默认隐藏输入验证码。重置成功会撤销该账号的全部登录会话,已建立的 WebSocket 最多 5 秒内断开;不影响工作区内其他成员和设备。
Console 注册页填写邮箱,随后进入验证页;登录页提供 验证邮箱 和 忘记密码。更换邮箱在验证完成前保留原地址。重发验证和找回请求返回统一提示,不暴露邮箱是否已注册。
邮件功能需要迁移 0004_email.sql 和 SMTP 配置:SMTP_HOST、SMTP_PORT、SMTP_FROM、SMTP_USERNAME、SMTP_PASSWORD、SMTP_SECURITY。默认 STARTTLS,支持隐式 TLS;非加密 SMTP 仅允许本地测试回环地址。SMTP 凭据放服务环境文件,不提交到 Git。不配置邮件服务时,新版注册和发信接口返回 503,既有账号登录不受影响,因此生产启用前必须完成发信配置。SMTP 暂时失败可能留下待验证账号,恢复后使用重发验证邮件,不必重复注册。
Exchange 账号 API
| 方法与路径 | 鉴权 | 用途 |
|---|---|---|
POST /api/auth/register |
无,限流 | {username,password,email},创建待验证账号,返回 202 和 verification_required,不签发会话 |
POST /api/auth/login |
无,限流 | {email,password},签发独立登录会话 |
GET /api/auth/me |
运维令牌或登录会话 | 返回 name、tenant_id、role |
POST /api/auth/logout |
当前登录会话 | 撤销当前会话;不撤销其他客户端 |
POST /api/auth/verify-email |
无,限流 | {username,code},消费邮箱验证码 |
POST /api/auth/resend-verification |
无,限流 | {email},向待验证地址重发邮件 |
POST /api/auth/forgot-password |
无,限流 | {email},向已验证地址发送找回验证码 |
POST /api/auth/reset-password |
无,限流 | {email,code,password},修改密码并撤销旧会话 |
POST /api/auth/email |
当前登录会话和账号密码 | {email,password},申请绑定或更换邮箱,验证后生效 |
登录成功返回 username、email(若已绑定)、tenant_id、token、expires_in,并设置 Cache-Control: no-store。账号密码使用 Argon2id 默认参数加盐保存;每个 TCP 来源 IP 每分钟最多 20 次账号请求,每个用户名每分钟最多 10 次,密码哈希并发最多 4 个。不信任客户端提供的转发 IP;反向代理后的客户端共享 IP 配额,公共部署应在可信入口配置额外限流。
Rust Exchange 默认关闭公开注册。需显式设置 EXCHANGE_ALLOW_REGISTRATION=true 并配置 SMTP 才能启用;false 关闭新注册,已有账号仍可登录。
让 Agent 显示六位码
Android Remote Agent:默认地址已填好,点击 Connect with a six-digit code。应用会显示一个 5 分钟有效的六位码。在已登录的管理员 CLI 上执行:
remote-cli pair 012345
# 可选:覆盖默认 ID 和显示名称
remote-cli pair 012345 --device-id my-android --name "我的手机"
CLI 获取连接码时显示设备名、身份类型和建议设备 ID,确认是自己的设备后授权。默认 ID 使用设备名的小写 ASCII 字母、数字和 ._-,去掉空格及其他字符;无可用字符时使用 device,最多 64 字符。同一工作区重名时依次追加数字,例如 realmev5、realmev51、realmev52,必要时截短基础 ID。Exchange 在创建时处理并发冲突,CLI 打印最终分配的 ID;手动指定的 ID 重名也追加数字。Android 使用系统设备名(不可用时使用型号),原生 Agent 使用主机名。Agent 自动取得独立设备令牌、加密保存并启动连接。不会自动开启辅助功能或屏幕控制。取消、过期或鉴权失败不会替换已有配置;Agent 正在连接时需先停止。
电脑、路由器与 Linux Agent 添加设备
Windows、macOS、OpenWrt、Linux Agent 无需手工填写设备令牌,任选一种方式:
| 平台 | 方式一:配对码 / 设备二维码 | 方式二:粘贴邀请 |
|---|---|---|
Windows / macOS(桌面 --ui,双击启动) |
本地页面 Add this device → Get pairing code,显示六位码和二维码 | 本地页面 Invite 文本框粘贴后点 Join with invite |
| OpenWrt(luci-app-remote) | LuCI 服务 → 远程管理 → 添加设备 → 获取配对码,显示六位码和二维码 | 同页 方式二:邀请内容 粘贴后点 加入 |
| Linux / 所有平台命令行 | remote-agent pair(终端显示六位码和字符二维码) |
remote-agent join '<邀请内容>',或 remote-agent join - 从标准输入读取 |
方式一:Agent 申请 5 分钟有效的六位码,同时显示二维码,内容为 remote://pair?code=012345&server=wss%3A%2F%2Fexchange.ivjn.us(不含任何凭证)。管理员任选:
Android Console 添加设备 → 输入配对码 → 扫描设备二维码(相机扫描,核对服务端一致后查询并确认批准);或输入六位码;Web Console 批准配对码 可输入六位码或粘贴 remote://pair?...;或 remote-cli pair 012345(也接受该 URI)。批准后 Agent 自动保存令牌并连接。
方式二:管理员在 Android Console 添加设备 → 扫码添加 生成一次性邀请,点击 复制邀请(电脑 / 路由器粘贴);或运行 remote-cli qr --device-id pc-01 --text。邀请是二维码中的 JSON({"type":"remote-enrollment",...}),5 分钟内有效、只能使用一次。也接受设备令牌二维码(remote-cli device qr)的内容。
保存位置与连接:
- 桌面 UI:写入当前配置文件(默认
%LOCALAPPDATA%\remote-agent\config.json/~/.config/remote-agent/config.json)并立即连接。 - OpenWrt:写入 UCI
remote-agent.main(server / device_id / token,enabled=1),再重启 procd 服务。 - 命令行:默认写入桌面配置路径(OpenWrt 默认写 UCI),
--config PATH指定 JSON 文件,--uci强制写 UCI;保存后在前台连接(--no-connect只保存)。如已有服务在用该配置,请重启服务。
已配置凭证时默认拒绝替换:命令行需 --force,桌面 UI 与 LuCI 会先确认。已有的令牌方式(--device-id/--token、配置文件、LuCI 设置页)不变。
remote-agent pair # 显示六位码 + 二维码,批准后保存并连接
remote-agent pair --server wss://your-exchange.example.com --config ./agent.json
remote-agent join '{"type":"remote-enrollment","version":1,...}'
remote-agent join - < invite.txt --no-connect
原生配对要求 wss://,仅本机回环允许 ws:// 以便测试;Android 和 Console 配对要求 HTTPS/WSS。
Console 六位码登录
Android Remote Console 登录页点击 使用六位码登录;Web Console(/console)登录卡片选择 六位码 标签,点击获取登录码。管理员可任选其一批准:
remote-cli pair 012345 --operator-name mobile-alice
# 只读客户端:追加 --role readonly
或在已登录的 Web Console 工作空间点击 批准配对码(仅管理员可见;只读操作员无此入口),输入对方屏幕上的六位码,核对类型与标签后批准。
默认创建独立 operator 账号,不复制管理员自身的令牌。管理员可明确指定 --role admin 或 --role readonly。Android Console 收到授权后再次验证凭证,保存到 SecureStore 并进入设备列表;Web Console 将操作员凭证保留在当前页面内存中(与账号 / 令牌登录相同),进入设备列表。取消或失败时仍可使用原有账号或令牌登录,不会清掉后续其他登录方式的输入。
Remote Agent 扫 CLI 码登录
remote-cli qr --device-id my-phone
# 输出 SVG 供手机扫描:
remote-cli qr --device-id my-phone --output ./phone-login.svg
在已登录管理员 CLI 上生成二维码,手机 Remote Agent 点击 Scan connection QR code,确认服务端和设备 ID 后点击 Connect。二维码只携带随机 256 位临时票据,5 分钟有效,只能绑定一台设备,不含长期设备令牌。扫描后由 Exchange 签发设备令牌,手机通过 Keystore 保存并连接。二维码对应的设备 ID 必须是新的;不会替换或轮换已有设备。
相同手机领取请求携带固定随机 claim_id,网络断开后重试可以取回同一结果;不同 claim_id 不能重复领取。保存后手机确认清除票据。临时票据在进程内存中保存,Exchange 重启会失效;已开始领取的结果另保留最多 5 分钟供原客户端重试。
| 方法与路径 | 鉴权 | 用途 |
|---|---|---|
POST /api/pairing/invite |
管理员 | {device_id,name?},生成票据,限制每管理员每分钟 10 次、全局 256 个 |
POST /api/pairing/claim |
Bearer <ticket> |
{claim_id},返回独立设备凭证;重试必须沿用同一 32 位十六进制 claim_id |
POST /api/pairing/claim/complete |
同上 | 保存成功后确认清除 |
生成设备二维码
# 创建设备,同时在终端显示二维码
remote-cli device add my-android --qr
# 新建 SVG 文件,可在电脑打开后供手机扫描
remote-cli device add another-phone --qr-output ./another-phone.svg
# 使用手里已有的设备令牌,不创建或轮换设备
remote-cli device qr my-android --output ./my-android.svg
# 明确轮换令牌并生成二维码;原令牌立即失效
remote-cli device rotate-token my-android --qr
device qr 以隐藏输入读取设备令牌,也支持 --token-stdin。设备二维码只包含设备令牌,不能使用运维令牌。输出文件拒绝覆盖,包含凭证,应妥善保管并在使用后删除。二维码要求 HTTPS/WSS。
Android Console:创建新设备后会直接显示设备二维码;设备详情的 轮换令牌并生成二维码 会先说明断线影响并要求确认。服务端只保存令牌哈希,不能从已存在的设备记录恢复旧二维码,因此不会为了查看二维码而静默轮换令牌。
Exchange 配对 API
| 方法与路径 | 鉴权 | 用途 |
|---|---|---|
POST /api/pairing/start |
无,限流 | 提交 kind: device/operator、label,取得 code、poll_token、expires_in: 300、interval: 3 |
GET /api/pairing/{code} |
管理员运维令牌 | 查看申请类型、标签和剩余时间,不返回凭证 |
POST /api/pairing/approve |
管理员运维令牌 | 提交 code、kind、identity、可选 name、role,创建独立设备或账号 |
POST /api/pairing/poll |
Authorization: Bearer <poll_token> |
返回 pending 或 approved;批准结果含 credentials.kind/identity/token |
POST /api/pairing/complete |
同上 | 客户端保存成功后清除临时授权;重复调用安全 |
GET /ws/pairing |
无,限流 | 原生 Agent 配对 WebSocket,依次接收 code、approved 或 expired 事件,保存后发送文本 complete |
六位码只用来让已认证管理员定位并确认请求,不能用来轮询或换取凭证。轮询凭证为随机 256 位秘密,不放入 URL。重复批准被拒绝;身份创建失败会释放授权占用以便重试。批准结果可在同一私有轮询凭证下重读,避免一次网络丢包导致凭证永久丢失;客户端保存后确认清除。
等待授权与批准待领取分别最多保留 5 分钟。未配置 REDIS_URL 时,临时会话只保存在当前 Exchange 进程内存中,重启后需重新领码,多实例需把配对请求固定到同一实例。配置了 Redis 后,配对条目与限流计数写入 Redis(与 5 分钟 TTL 对齐),多实例可任意分发、不必粘性。每个实际 TCP 来源 IP 每分钟最多领取 10 次,管理员每分钟最多查询或批准 20 次;内存模式另有全局最多 256 个待处理会话的上限。代理转发的客户端 IP 不作为可信限流依据;同一反向代理后的用户共享来源额度。过期数据由 TTL/定时清理清除,响应设置 Cache-Control: no-store,不记录配对秘密或签发令牌明文。
后端已有设备 ID/账号不能被配对请求覆盖。管理员应核对代码和申请端;不要批准他人诱导提供的登录码。
连接码 API:POST /api/pairing/start 可提交可选 device_name,响应返回该设备名。管理员 GET /api/pairing/{code} 返回 device_name 和当前工作区的 suggested_device_id,查询不会占用 ID。POST /api/pairing/approve 的设备 identity 可省略,成功返回 200 和 {identity}(最终 ID,不含令牌)。原生 WebSocket /ws/pairing?device_name=... 支持 URL 编码的设备名。