---
title: 原生流程
description: 为桌面应用、游戏、CLI 与服务选择浏览器轮询 Connect、Steam ticket direct-issue 或
  AccessKey direct-issue。
editUrl: true
head: []
template: doc
sidebar:
  order: 1
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

Connect 浏览器流程假设你能把浏览器跳转到 `via.sudomimus.com`，并接收回调。对 Web 应用来说没问题，但对桌面应用、游戏和无头工具来说就尴尬了。

根据客户端的能力，原生客户端有三种流程可选。其中两种是 **Native API**（`native-api.sudomimus.com`）的一次性 direct-issue 端点——Steam direct-issue 和 AccessKey direct-issue。第三种「浏览器轮询」则是一条 **Connect API** 流程（`/establish` → `/status-poll` → `/redeem`），由原生客户端自行驱动，并不运行在 Native API 上。

如果你的客户端是公共客户端，不应该拿到应用 client-auth 私钥，也不应该预置 AccessKey secret，请改用[设备码授权](/zh-cn/device/flow/)。设备码授权才是面向 CLI、启动器等公共客户端的验证码确认流程。

| 流程 | 适用场景 | 往返次数 | 端点 |
|---|---|---|---|
| **浏览器轮询** | 任何能拉起系统浏览器的原生客户端 | 3+ (establish + N×poll + redeem) | `connect POST /establish` → `/status-poll` → `/redeem` |
| **Steam direct-issue** | 通过 Steam 发行、能调用 Steamworks SDK 的游戏 | 1 | `native-api POST /direct-issue/steam-ticket` |
| **AccessKey direct-issue** | 预先拿到应用级凭据、绑定到已存在账户的 CLI / 无头服务 / 启动器 | 1 | `native-api POST /direct-issue/access-key` |

## 浏览器轮询

当原生客户端能拉起用户的系统浏览器、但又不方便接收回调 URL 时，使用轮询流程：

1. 客户端后端调用 `connect POST /establish`（带上应用的 client-auth JWT），声明 `STATUS_POLL` 返回方法，拿到 `{ exposureKey, hiddenKey }`。
2. 客户端拉起系统浏览器并打开 `https://via.sudomimus.com/?exposure-key=<exposureKey>`。
3. 用户在浏览器中完成通行密钥或邮箱验证码挑战。
4. 客户端每隔几秒调用一次 `connect POST /status-poll`，提交 `{ exposureKey, hiddenKey }`。一旦用户完成，轮询会返回 `{ status: "REALIZED", confirmationKey }`。
5. 客户端将三个密钥提交给 `connect POST /redeem`，换取 `{ accessToken, refreshToken }`。

这一方案适用于任何带默认浏览器的平台——Windows、macOS、Linux 桌面应用、Electron 等等。应用的 Layer 3 规则中必须允许 `STATUS_POLL`。

`/establish` 就是标准的、用 client-auth 签名的 Connect 请求 —— 完整形状见 [Web 应用](/zh-cn/connect/flow/) —— 只是返回方法换成 `STATUS_POLL`：

```bash
curl -X POST https://connect-api.sudomimus.com/establish \
  -H "Content-Type: application/json" \
  -H "Authorization: SudomimusClientJWT $SUDOMIMUS_CLIENT_AUTH_JWT" \
  -d '{
    "applicationAnchor": "your-application",
    "returnMethods": [ { "type": "STATUS_POLL", "payload": {} } ]
  }'
# → { "exposureKey": "exp_...", "hiddenKey": "hid_..." }
```

随后用这两把 key 每隔几秒轮询一次 `/status-poll`。轮询**不**带 client-auth JWT —— 授权它的是对 `hiddenKey` 的持有：

```bash
curl -X POST https://connect-api.sudomimus.com/status-poll \
  -H "Content-Type: application/json" \
  -d '{
    "exposureKey": "exp_...",
    "hiddenKey": "hid_..."
  }'

# 用户仍在浏览器里鉴权时：
#   { "status": "PENDING" }
# 用户完成后：
#   { "status": "REALIZED", "confirmationKey": "cnf_..." }
```

轮询返回 `REALIZED` 后，将三个密钥提交给 `connect POST /redeem`，换取访问令牌和刷新令牌。该调用与 Web 流程中的 `/redeem` 相同。

## Steam direct-issue

对于通过 Steam 发行的游戏，Sudomimus 支持一种完全不打开浏览器的静默登录。用户看不到任何登录提示；他们的 Steam 身份会直接换成一个 Sudomimus 会话。

```bash
curl -X POST https://native-api.sudomimus.com/direct-issue/steam-ticket \
  -H "Content-Type: application/json" \
  -d '{
    "applicationAnchor": "my-game",
    "steamTicketHex": "<来自 Steamworks GetAuthTicketForWebApi 的 hex 编码 ticket>",
    "steamAppId": 480
  }'
```

流程：

1. 游戏调用 Steamworks 的 `ISteamUser::GetAuthTicketForWebApi("sudomimus")`——**不要**用 `GetAuthSessionTicket`，两者是不同类型的 ticket，不能互换。identity 字符串必须是 `"sudomimus"`（大小写敏感）；其他值会被拒绝。
2. 游戏等待 `GetTicketForWebApiResponse_t` 回调，再使用该 ticket。
3. 把 ticket 字节流 hex 编码后作为 `steamTicketHex`，连同 `applicationAnchor` 和 `steamAppId` 一起 POST 到 `/direct-issue/steam-ticket`。
4. Sudomimus 向 Steam 校验 ticket，查找或创建账户，然后 —— 在顺利路径上 —— 在同一次请求中返回 `{ accessToken, refreshToken }`。如果应用要求 Steam 账户尚未提供的同意或资料，这一步会改为返回一个带 **Errand** 交接的 `403` —— 见[当 direct-issue 需要同意或资料时](#当-direct-issue-需要同意或资料时)。
5. 拿到令牌后，游戏调用 `Steamworks.CancelAuthTicket(handle)` 收尾。

整个过程用户都不会离开游戏。Steam 账户就是身份来源。

### Steam 流程的应用配置

应用必须配置：

- **Layer 1**：一条 `STEAM_TICKET` AuthenticationRule，`allowedSteamAppIds: number[]` 中包含该游戏的 Steam App ID。
- **Layer 2**：至少一条能命中的规则——通常是 `STEAM_ID`，`allowedSteamIds: ["*"]` 表示放行任何已验证的 Steam 账户，或者给出一组具体的 SteamID64 字符串。
- **Layer 3**：一条 `DIRECT_ISSUE` ReturnRule。

从未绑定过邮箱的「Steam-first」账户必须依赖 `STEAM_ID`（或 `ACCOUNT_ALIAS` / `SECTOR_SUBJECT`）类型的 Layer 2 规则；只有 `EMAIL` 规则会把它拒掉。

该端点**不**需要 client-auth JWT——Steam ticket 本身就证明了用户身份以及这个二进制有权与该应用对话。

> **对应的 Web 流程。** 浏览器中的「Sign in with Steam」按钮使用 Layer 1 的 `STEAM_OPENID`，而不是 `STEAM_TICKET`。两条路径都会解析为**同一个**用户级 Steam 身份，因此用户先从游戏登录后，也可以继续使用网页按钮登录，反之亦然，无需合并账户。`STEAM_OPENID` 的规则结构见[认证规则](/zh-cn/application-rules/authentication-rules/)。

## AccessKey direct-issue

适用于没有 Steam ticket、但目标 Sudomimus 账户已经明确的场景——CLI 工具、自定义启动器、无头服务、自动化测试。「证明」是由 Sudomimus 签发的一对凭据（`accessKeyIdentifier` + `accessKeySecret`），在开发者门户中生成后通过线下方式交付给运维方。

```bash
curl -X POST https://native-api.sudomimus.com/direct-issue/access-key \
  -H "Content-Type: application/json" \
  -d '{
    "applicationAnchor": "my-cli-tool",
    "accessKeyIdentifier": "acs_k_<uuidv4>",
    "accessKeySecret": "acs_t_<64 位小写 hex>"
  }'
```

两个凭据字符串都带有强制前缀：

- `acs_k_` —— 公开 identifier，后接 UUIDv4。
- `acs_t_` —— 密文部分，后接 64 位小写 hex（在创建时只展示一次，之后无法恢复）。

前缀是规范形式的一部分。它们让两个部分在视觉上可区分，也方便 secret scanner 通过字面子串匹配到误提交的凭据。

### AccessKey 流程的应用配置

应用必须配置：

- **Layer 1**：一条 `ACCESS_KEY_DIRECT` AuthenticationRule（空 payload）。不显式开启则默认拒绝。
- **Layer 2**：一条能匹配目标账户的规则——`EMAIL`、`STEAM_ID`、`ACCOUNT_ALIAS` 或 `SECTOR_SUBJECT`。
- **Layer 3**：一条 `DIRECT_ISSUE` ReturnRule。

**AccessKey 凭据无法创建新账户。** 凭据绑定在一个已存在的 Sudomimus 账户上；该账户被删除后，所有与之绑定的凭据在登录时都会被拒绝。

凭据通过 [`with.sudomimus.com`](https://with.sudomimus.com) 中应用详情页的 **Access keys** Tab 管理。吊销是软删除（`revokedAt` 时间戳）；轮换 = 吊销 + 重新签发。过期凭据不会自动清理，但在 handler 处会被拒绝。

该端点也不需要 client-auth JWT——access-key 的密文本身就是凭据。把 client-auth 私钥嵌进发行给运维的 CLI 里，任何人都能反编译出来，因此再叠一层并不带来真正的防御。

## 当 direct-issue 需要同意或资料时

两个 direct-issue 端点都是一次性的**读取者**——它们没法弹出同意界面，也没法让用户敲入一封邮箱。所以当应用要求一条用户尚未授权的[声明（claim）](/zh-cn/concepts/identity-claims/)，或要求账户还没有的数据（例如纯 Steam 账户没有邮箱）时，这次调用不能就这么成功。它会改为返回一个带 **[Errand](/zh-cn/native/claims-and-errand/)** 的 `403` —— 一段短暂的浏览器侧行，用户在那里完成这些工作：

```json
{
  "reason": "ClaimConsentRequired",
  "claims": {
    "email":     { "requirement": "REQUIRED", "state": "UNKNOWN" },
    "firstName": { "requirement": "OPTIONAL", "state": "UNKNOWN" },
    "lastName":  { "requirement": "OFF",      "state": "UNKNOWN" }
  },
  "errand": {
    "errandKey": "ernd_...",
    "url": "https://via.sudomimus.com/errand?key=ernd_...",
    "expiresAt": "2026-06-10T12:30:00Z"
  }
}
```

`reason` 是 `ClaimConsentRequired`（用户必须同意共享某条 required 声明）或 `RequiredClaimDataMissing`（同意已就位，但账户数据缺失）二者之一。Steam 与 AccessKey direct-issue 在这里行为完全一致。补救方式：

1. 在用户的**系统浏览器**中打开 `errand.url`。页面会引导用户完成所有待办的登录、数据录入和同意。
2. 每隔约 2 秒轮询 `GET /errand/{errandKey}/status`（`native-api`），直到它报告 `COMPLETED` —— 或者干脆让用户在你的 UI 里告诉你他完成了。
3. 把**同一个** direct-issue 调用重试一次。它现在会成功返回 `{ accessToken, refreshToken }`。

```bash
curl https://native-api.sudomimus.com/errand/ernd_.../status
# → { "status": "PENDING" }    用户仍在浏览器里操作
# → { "status": "COMPLETED" }  完成 —— 重试 direct-issue
# → { "status": "EXPIRED" }    已过期/已消费/未知 —— 重新跑 direct-issue 拿一个新的 errand
```

任一端点的 `200` 也会带一个 `claims` 块（形状与 `403` 里的相同），所以即便成功，你也能看出哪些可选声明被共享了、哪些被保留了。完整契约 —— 30 分钟存活期、为什么重试会复用同一个 `errandKey`、以及两种安全层级（仅同意 vs. 需要登录）—— 见 [Errand](/zh-cn/native/claims-and-errand/)。

## 该用哪种

| 你的客户端是… | 用 |
|---|---|
| Web 应用 | Connect + `via.sudomimus.com`（常规流程） |
| 能拉起浏览器的桌面应用 / Electron / 移动端 | Connect + 通过系统浏览器打开 `via.sudomimus.com` + `connect /status-poll` |
| 没有 client secret 的公共 CLI / 启动器 | [设备码授权](/zh-cn/device/flow/) |
| 通过 Steam 发行的游戏 | `native-api POST /direct-issue/steam-ticket`（静默，一次往返） |
| 通过预签发凭据绑定到已知账户的 CLI / 无头服务 | `native-api POST /direct-issue/access-key`（一次往返，使用 AccessKey） |

无论走哪条路径，最终返回的**令牌格式都是一样的**——你的应用只需要处理同一种 access token 结构，与用户的认证方式无关。