---
title: 设备码轮询与错误
description: 处理 Device API 的轮询状态、一次性会话，以及 deviceCode / userCode 的安全边界。
editUrl: true
head: []
template: doc
sidebar:
  order: 2
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

设备码授权客户端的大部分时间都在等待。好的实现会把 `/device-token` 当作一个状态机：pending 继续轮询，`slow_down` 放慢速度，终止错误停止流程，成功则消费这次会话。

## 轮询循环

`POST /device-authorize` 之后，保存 `deviceCode`，展示 `userCode`，然后用不快于返回 `interval` 的频率开始轮询。

```js
let intervalSeconds = authorize.interval;

while (true) {
  await sleep(intervalSeconds * 1000);

  const res = await fetch("https://device-api.sudomimus.com/device-token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({ deviceCode: authorize.deviceCode }),
  });

  const body = await res.json();

  if (res.ok) {
    return body; // { accessToken, refreshToken, claims, ... }
  }

  if (body.error === "authorization_pending") continue;
  if (body.error === "slow_down") {
    intervalSeconds = body.interval ?? intervalSeconds + 5;
    continue;
  }

  throw new Error(body.error);
}
```

如果客户端轮询太快，服务端可能返回 `slow_down`。发生这种情况后，后续轮询使用响应里的 `interval`。

## 轮询错误

`POST /device-token` 对轮询状态使用 [RFC 8628 device-flow 错误词汇](https://www.rfc-editor.org/rfc/rfc8628#section-3.5)。客户端应该按 `error` 分支处理；不要期待这里返回 Sudomimus `{ "reason": "..." }` wire reason。

| Error | 含义 | 客户端行为 |
|---|---|---|
| `authorization_pending` | 用户还没有批准或拒绝。 | 按当前间隔继续轮询。 |
| `slow_down` | 客户端轮询太快。 | 增加间隔；如果响应带 `interval`，使用它。 |
| `access_denied` | 用户拒绝、批准失败，或策略已不再允许签发。 | 停止轮询，展示拒绝/失败状态。 |
| `expired_token` | 设备授权会话已过期。 | 停止轮询；重新从 `/device-authorize` 开始。 |
| `invalid_request` | `deviceCode` 格式错误、未知，或已经被消费。 | 停止轮询；必要时重新开始。 |
| `server_error` | 批准后签发 token 失败。 | 停止轮询，报告可重试的服务故障。 |

成功响应使用 Sudomimus 普通 camelCase JSON 形状。轮询失败刻意使用 device-flow 的 `error` 词汇，这样公共客户端可以按熟悉的设备码状态机实现。

## 过期与一次性

每个设备授权会话都很短暂。生产默认值目前是 `expiresIn: 600` 秒，但客户端应该使用 `/device-authorize` 返回的值，而不是硬编码生命周期。

一旦 `/device-token` 成功，这次会话就被消费。用同一个 `deviceCode` 重复调用 `/device-token` 会返回 `invalid_request`，不能再签发第二组令牌。

## Code 处理

两个 code 的处理方式不同：

| 值 | 谁能看到 | 用途 |
|---|---|---|
| `deviceCode` | 只有发起客户端 | `/device-token` 的高熵 bearer secret |
| `userCode` | 用户和浏览器批准页面 | 用户比较并确认的短码 |

不要展示 `deviceCode`，不要写入日志，不要放进浏览器 URL，也不要发给你的应用后端，除非那个后端就是负责轮询 `/device-token` 的组件。`deviceCode` 在过期前是 bearer secret；谁拿到它，谁就能轮询并消费已批准的会话。

`userCode` 可以展示，但它不是 token，也不能独自签发 token。它的作用是让浏览器里的用户确认：自己批准的正是终端、启动器或设备界面上显示的那次客户端会话。

## 没有 client secret

设备码授权是为公共客户端准备的。`/device-authorize` 不需要 client-auth JWT，因为这样的客户端无法保护签名密钥。Sudomimus 改由应用配置来把关：

- `applicationAnchor` 必须解析到一个启用的应用。
- 应用必须有一条启用的 Layer 3 `DEVICE_CODE` ReturnRule。
- 浏览器批准仍然走应用的 Layer 1 认证规则。
- realize 出来的账户仍然要通过 Layer 2，批准才能完成。

如果你有能保护 client-auth 私钥的机密后端，[Connect](/zh-cn/connect/flow/) 或 [Connect 浏览器轮询](/zh-cn/native/overview/#浏览器轮询) 可能更合适。

## Claims 与令牌操作

`/device-token` 返回的 access token 和 refresh token 与其他 Sudomimus 流程相同。Claim 分享同样遵循应用的 claim policy 和用户的 grant state，和 Connect 或 Native direct-issue 一致。

Device API 没有 refresh 端点。初始交换完成后，使用 Connect：

- `connect-api POST /refresh`
- `connect-api POST /logout`
- `connect-api POST /introspect`
- `connect-api POST /revoke-all`

令牌生命周期调用见[管理会话](/zh-cn/guides/managing-sessions/)；精确请求/响应 schema 见 [Device API 参考](/zh-cn/api/device/)。