Identity 认证服务

一行命令注册账号、拿 JWT,用于 devnull.cn 各服务间的身份验证。

账号与设备也可以在网页上管理:管理控制台(用 access_key + secret_key 登录,secret_key 只在浏览器里参与哈希)。

安装 + 注册/初始化

下载后执行 devnull-cli init 即可创建账号(工作量证明,不需要邮箱), 凭据写入 ~/.devnull/credentials(0600,与 Python SDK 共用同一份)。

下载 devnull-cli(linux/amd64 示例),一条命令完成注册并把凭证存到 ~/.devnull/credentials:

curl -fsSL https://files-1252667388.cos.ap-beijing.myqcloud.com/pub/devnull-cli-releases/latest/devnull-cli-linux-amd64 \
  -o /usr/local/bin/devnull-cli && chmod +x /usr/local/bin/devnull-cli && devnull-cli init

其他平台把文件名换成对应的即可:

平台文件名
Linux amd64devnull-cli-linux-amd64
Linux arm64devnull-cli-linux-arm64
macOS Inteldevnull-cli-darwin-amd64
macOS Apple Silicondevnull-cli-darwin-arm64
Windowsdevnull-cli-windows-amd64.exe

下载地址前缀:https://files-1252667388.cos.ap-beijing.myqcloud.com/pub/devnull-cli-releases/latest/

拿 JWT

devnull-cli token

用 ~/.devnull/credentials 里保存的凭证签一个断言,换回 15 分钟的 EdDSA 访问令牌并打印。 其他常用子命令:devnull-cli whoami(看 account_id/角色/scope)、 devnull-cli rekey(1.x 老账号换一次 2.0 凭据)。

AK/SK 换 JWT(theid 2.0,推荐)

不再把派生密码发到服务端:客户端用 secret_key 签一个 60 秒、一次性、 aud 绑定本服务的 HS256 断言,换来 15 分钟的 EdDSA 访问令牌。secret_key 永不 上网;令牌由 JWKS 公钥在资源服务本地验签,资源服务不再持有任何共享密钥。

pip install theid

python3 - <<'EOF'
import theid
print(theid.Client().authorization_headers())
EOF

手工调用等价于(签名细节见 /identity/.well-known/identity-configuration):

curl -s -X POST https://devnull.cn/identity/oauth/token \
  -d grant_type=urn:ietf:params:oauth:grant-type:jwt-bearer \
  -d assertion=$ASSERTION \
  -d audience=thedns \
  -d 'scope=profile:read device:read'
{"access_token":"eyJ...","token_type":"Bearer","expires_in":900,"scope":"profile:read device:read","account_id":"016869757"}

资源服务验签只需要公钥:

pip install "theid[verify]"

from theid import JWKSClient
jwks = JWKSClient()   # 公钥缓存 300 秒,未知 kid 自动刷新一次
claims = jwks.verify(token, audience='thedns', issuer='https://devnull.cn/identity')

老账号要先轮换一次(服务端只存了 SK 的哈希,无法反推)。用旧的 sha256(sha256(ak)+sha256(sk)) 作为密码换新的一对,旧凭据立即失效,新凭据只返回一次:

curl -s -X POST https://devnull.cn/identity/keys/rekey \
  -H 'Content-Type: application/json' \
  -d '{"access_key":"DK...","password":"<derived>"}'

令牌里的 scope 由角色推导、只能下放不能提权(user 是 profile:read,fe 再加 device:read 与 device:write);aud 决定这张令牌能用于哪些服务。老式 1 小时 HS256 令牌在迁移期内仍然有效。

验证

访问其他 devnull.cn 服务时,带上拿到的 token:

Authorization: Bearer <token>

角色与权限

每个账号持有一个角色集合,自助注册默认是 {user}。判定时取集合里最高的那一档: user < fe < admin < superadmin。服务端每次特权请求都重读数据库里的角色, 所以改角色、删账号立即生效,不用等 token 过期。

角色能做什么
user基线角色,每个账号都有、不可移除:登录、拿 JWT 给其他 devnull.cn 服务用。设备接口一律 403(连设备列表都看不到)。
fe现场工程师:激活设备(提交配对码)、看自己的设备与待激活队列、改自己设备的 device_info、撤销自己的设备。别人的设备看不到也删不掉(返回 404 而非 403)。
admin看全部设备与账号;撤销/删除任意设备;删账号;只能增减他人的 fe。
superadmin增减 fe/admin/superadmin;不能改自己的角色;系统必须始终保留至少一个 superadmin。

角色只能被 admin/superadmin 授予(见下面的账号管理接口),所以现场工程师的 fe 需要找人加;设备激活同理,纯 user 提交配对码会拿到 403。

管理控制台

管理控制台是设备与账号的管理页(公开页面,自身不存任何凭据):登录后可以 激活设备、查看与撤销设备、编辑设备备注、管理账号角色。登录时浏览器用 secret_key 签一个 60 秒、一次性的断言再换 EdDSA 令牌,secret_key 不会离开浏览器; 换来的 JWT 只存 sessionStorage,关掉标签页即失效。页面上的按钮只是"能不能点", 真正的授权判定都在服务端(越权一律 403)。多个账号(比如既是 fe 又是 admin 的两套凭据)可以存在浏览器本机一键切换 —— 那是明文存 localStorage,只建议在自用机器上这么做。

设备认证

面向客户现场的联网设备。设备首次接入时,由现场工程师用人类账号激活一次;之后设备拿着 自己的 PSK 换 JWT,续期不需要人参与。设备凭证与人类凭证完全独立:设备 token 不能用于 人类接口,人类密码泄露也不影响已激活的设备。

1. 设备注册

设备开机后注册,一次性拿到 pairing_code(显示在屏幕上给人看的)和 device_psk(存进设备安全存储,之后不再传输):

curl -s -X POST https://devnull.cn/identity/device/register \
  -H 'Content-Type: application/json' \
  -d '{"device_id":"rk3588-001","device_info":{"model":"RK3588","firmware":"1.0.0"}}'
{"pairing_code":"QCQA-636V","device_psk":"0Yh...","expires_in":600,"status":"pending"}

device_psk 只返回这一次,服务端只保留 Argon2id 哈希,丢了就重新注册。配对码 10 分钟内有效、只能用一次;确认之前 PSK 换不到 token。

2. 现场工程师确认(需要 fe 及以上角色)

工程师用人类账号登录取 JWT(devnull-cli token),把设备屏幕上的配对码提交上来。 任何 fe 及以上的人都能激活屏幕上正在显示的设备——现场语义就是"当班的工程师激活眼前的盒子":

curl -s -X POST https://devnull.cn/identity/device/confirm \
  -H "Authorization: Bearer $HUMAN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"pairing_code":"QCQA-636V"}'
{"device_id":"rk3588-001","status":"active"}

这一步同时确定设备归属:设备属于执行确认的那个账号。换个账号确认,就是把设备转移给新账号。

3. 设备换取 JWT

OAuth2 client_credentials:client_id 是 device_id, client_secret 是 device_psk。

curl -s -X POST https://devnull.cn/identity/token \
  -d grant_type=client_credentials \
  -d client_id=rk3588-001 \
  -d client_secret=$DEVICE_PSK
{"access_token":"eyJ...","token_type":"Bearer","expires_in":3600}

token 有效期 1 小时,设备侧按时续期即可(比如还剩 5 分钟时续)。payload 里 sub 是 device:rk3588-001,principal_type 是 device, owner 是归属账号。

4. 查看、编辑与撤销

# 我的设备(fe);?scope=pending 看待激活队列,?status= 过滤状态
curl -s 'https://devnull.cn/identity/devices?scope=own&status=active' \
  -H "Authorization: Bearer $HUMAN_TOKEN"

# 编辑 device_info:浅合并,保留键 label / note
curl -s -X PATCH https://devnull.cn/identity/devices/rk3588-001 \
  -H "Authorization: Bearer $HUMAN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"device_info":{"label":"3号机房","note":"靠窗那台"}}'

# 撤销(若还是未确认的注册,则直接丢掉那一行)
curl -s -X DELETE https://devnull.cn/identity/devices/rk3588-001 \
  -H "Authorization: Bearer $HUMAN_TOKEN"

列表条目带 status、device_info、时间戳,待激活时还会带 code_expires_in;owner_user_id 只在 admin 及以上调用时返回 (fe 看自己名下的设备不需要它)。列表永远不返回 pairing_code:能读到码 就等于能激活别人看不见的设备,码只存在于设备屏幕上。

撤销是可恢复的:撤销后设备重新走一遍上面的流程(重新注册 → 新的配对码 → 重新确认)就能 恢复,不需要换硬件。撤销在设备下一次换 token 时生效,最长滞后一个 token 周期。

DELETE 对已激活设备只把它置为 revoked(幂等 204),那一行会作为 审计记录留下;只有未确认的 pending 行会被直接删掉。所以要让一台已撤销设备 彻底从库里消失(比如清理测试设备):再 register 一次让它回到 pending, 然后 DELETE 即可。同一台设备想重新上岗也走这条路径。

接口与状态码

接口说明最低角色状态码
POST /device/register设备注册 / 重新激活,返回配对码和 PSK(仅一次)—(匿名)200 / 400 / 409
POST /device/confirm确认配对码,激活设备并确定归属fe200 / 400 / 401 / 403 / 404 / 410
POST /token设备用 device_id + PSK 换 JWT—(匿名)200 / 400 / 401 / 403
GET /devices设备列表:?scope=own(默认)/ pending 待激活队列 / all 全部fe200 / 401 / 403
PATCH /devices/{device_id}浅合并 device_infofe(自己的)/ admin(任意)200 / 400 / 401 / 403 / 404
DELETE /devices/{device_id}撤销设备,或清理未确认的注册fe(自己的)/ admin(任意)204 / 400 / 401 / 403 / 404
GET /me当前账号与角色集合(管理页首屏用)user200 / 401
GET /users账号列表:account_id、角色、设备数admin200 / 401 / 403
POST /users/{account_id}/roles增减角色 {"add":[],"remove":[]}(user 不可动)admin / superadmin200 / 400 / 401 / 403 / 404
DELETE /users/{account_id}删账号,名下设备一并转 revokedadmin204 / 401 / 403 / 404

401:凭证不对(不会暴露 device_id 是否存在,因此没有“设备不存在”这个错误); 403:凭证对但角色不够,或状态不允许(未确认 / 已撤销 / 拿设备 token 调人类接口); 404:配对码不存在,或设备/账号不属于你(越权也报 404,不确认别人手里有什么); 409:设备已激活,或已有未过期的配对码;410:配对码过期,重新注册即可。

设备 id 只允许字母、数字、.、_、-,最长 64 字符, 建议直接用出厂序列号(如 rk3588-001)。单个账号能注册多少设备不做限制。

账号管理(admin 及以上)

# 账号列表:account_id、角色集合、设备数
curl -s https://devnull.cn/identity/users \
  -H "Authorization: Bearer $ADMIN_TOKEN"

# 增减角色:增量式,user 不可加也不可删
curl -s -X POST https://devnull.cn/identity/users/123456789/roles \
  -H "Authorization: Bearer $SUPERADMIN_TOKEN" \
  -H 'Content-Type: application/json' \
  -d '{"add":["fe"],"remove":[]}'

# 删账号:名下设备一并转 revoked(保留记录),账号本身不可恢复
curl -s -X DELETE https://devnull.cn/identity/users/123456789 \
  -H "Authorization: Bearer $ADMIN_TOKEN"

规则:任何人不能改自己的角色;admin 只能增减别人的 fe,也管不了 admin/superadmin;最后一个 superadmin 不能被降级或删除(403); 纯 user 账号由 admin 删除,它名下的设备会转 revoked, 需要 fe 重新激活才能回到可用状态。