---
title: 令牌与验证
description: Access token、refresh token、OIDC ID token 的完整字段参考 ——
  每种令牌携带什么、如何验证、TTL 区间、邮箱字段如何选取。
editUrl: true
head: []
template: doc
sidebar:
  order: 6
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

import { LinkCard } from "@astrojs/starlight/components";

Sudomimus 签发三种令牌，由不同的密钥签名、通过不同的机制验证。本页是令牌验证与字段内容的唯一参考 —— 每个 claim 的含义，以及决定字段取值的规则。

## 一览

| 令牌 | 签发者 | 签名密钥 | 验证方式 | 携带的内容 |
|---|---|---|---|---|
| **Access token** | Connect（`/redeem`、`/refresh`、`/direct-issue/*`、`/token`） | **应用的** token-signing 私钥 | `POST /info` → `applicationPublicKey` | `subject`、`firstName`、`lastName?`、`emailAddress?` |
| **Refresh token** | Connect（`/redeem`、`/direct-issue/*`、`/token`） | **应用的** token-signing 私钥 | 与 access token 相同 | `subject` |
| **OIDC ID token** | OIDC（`/token`） | **平台级** OIDC 签名密钥 | `oidc.sudomimus.com/.well-known/jwks.json` | `sub`、`iss`、`aud`、`exp`、`iat`、`nonce?`、`auth_time?`、`email?`、`name?` |

三者都是用 **RS256**（RSA-2048）签名的 JWT。

:::note[响应外层不止有令牌]
本页讲的是令牌*内部*有什么。而递送这些令牌的 `/redeem`、`/refresh`、`/direct-issue/*` 响应，还会在令牌旁边附带一个顶层 **`claims` 块** —— 一份逐条声明的视图，列出应用请求了什么、用户决定了什么，正是靠它你才能判断 `emailAddress` 这类可选字段*为什么*缺席。见 [`claims` 块](/zh-cn/concepts/identity-claims/#claims-块)。
:::

## Access token 与 refresh token

这是你的应用后端日常打交道的两种令牌。两者都由 Sudomimus 用**与你应用绑定**的密钥对签发 —— 每个应用都有自己独立的一对，轮换一个应用的密钥不会影响其他应用。

### 验证流程

应用使用自己的 **token-signing 公钥** 验签，这把公钥通过 `POST /info` 拉取。推荐流程：

1. 启动时调用 `POST /info` 传入自己的 `applicationAnchor`，缓存返回的 `applicationPublicKey`。
2. 每次接收到请求时，解析 JWT，检查 header 里的 `kty === "Access"`（refresh token 是 `"Refresh"`），校验 `exp`，并用缓存的公钥验签。
3. 在开发者门户中轮换应用密钥之后，清空缓存并重新拉取。

官方 SDK 已经把这些事做好了 —— 详见 [SDK 文档](/zh-cn/reference/sdks/) 中的 `verifyAccessToken`。

**Connect 令牌没有 JWKS。** 验证是 per-application 的，这是设计上有意为之：应用只需要知道一把和自己 `applicationAnchor` 绑定的公钥；任意一个应用的密钥泄露也不会影响其他应用。

### `kty` 声明

Sudomimus 的 access token / refresh token JWT 在 header 里带一个自定义 `kty` 字段，用来明确令牌类型：

- Access token：`kty: "Access"`（大小写敏感）
- Refresh token：`kty: "Refresh"`（大小写敏感）

如果你在做 access token 校验时碰到 `kty: "Refresh"`（反之亦然），应当直接拒绝 —— 官方 SDK 已经做了这件事，但如果你手动验签，这是最容易漏掉的检查之一。

### TTL 区间

| | 默认值 | 最小值 | 最大值 |
|---|---|---|---|
| Access token | 3 小时 (10800s) | 60 秒 | 7 天 (604800s) |
| Refresh token | 30 天 (2592000s) | 1 天 (86400s) | 365 天 (31536000s) |

规则上和 inquiry 上的 TTL 覆盖都受这套区间限制。当多个 TTL 同时命中（例如一条 Layer 1 规则 + 一条 Layer 3 inquiry 约束），Sudomimus 把它们 fold 成**最小值**；如有必要，refresh TTL 会被提升到不小于 access TTL。

### Access token 字段

Sudomimus 的 access token 与 refresh token 把所有**标准 JWT claim**（`kty`、`iss`、`aud`、`sub`、`iat`、`exp`）放在 **JWT header** 中，而 **body** 只放**应用专属字段**（`subject`、`firstName`、`lastName`、`emailAddress`）。如果你手动验证，请从 header 而非 payload 读取 `iss` / `aud` / `exp` —— 官方 SDK 会替你处理这点。

```json
// JWT header
{
  "alg": "RS256",
  "kty": "Access",
  "iss": "sudomimus.com",
  "aud": "<applicationAnchor>",
  "sub": "<refreshTokenIdentifier>",
  "iat": <epoch>,
  "exp": <epoch>
}
```

```json
// JWT body
{
  "subject": "<sector subject>",
  "firstName": "<string>",
  "lastName": "<string，可选>",
  "emailAddress": "<string，可选>"
}
```

| Claim | 含义 |
|---|---|
| `kty` | 总是 `"Access"`。任何不匹配的令牌都应拒绝。 |
| `subject` | 应用可见的**扇区主体（sector subject）**，即按（账户 × 扇区）生成的不透明标识符（例如 `sub_9SQ5535CRWNDDM2T`）。应用应使用该值标识用户。它在同一扇区内对同一用户保持稳定，但可以由用户轮换，并且在不同扇区之间互不相同。请将其视为不透明值，**不要**解析。原始账户 UUID 永远不会写入令牌。 |
| `firstName`、`lastName` | 用户的显示名称。`lastName` 可选。 |
| `emailAddress` | 与本次登录关联的已验证邮箱。选取规则见下文。当账户无任何已验证邮箱时（例如纯 Steam 或纯 AccessKey 账户）省略。 |
| `iss` | 总是 `"sudomimus.com"`。 |
| `aud` | 签发本令牌的应用的 `applicationAnchor`。 |
| `sub` | 签发本 access token 的 **refresh token** 的标识符。用于吊销关联，**不是**用户标识。 |
| `iat`、`exp` | 标准 JWT 签发时间 / 过期时间，单位秒（epoch）。 |

### Refresh token 字段

同样的 header / body 划分也适用 —— 标准 claim 在 header，扇区主体在 body。

```json
// JWT header
{
  "alg": "RS256",
  "kty": "Refresh",
  "iss": "sudomimus.com",
  "aud": "<applicationAnchor>",
  "iat": <epoch>,
  "exp": <epoch>
}
```

```json
// JWT body
{
  "subject": "<sector subject>"
}
```

这里的 `subject` 与 access token 上携带的扇区主体相同。它**仅供参考** —— Sudomimus 通过 refresh token 的内部句柄来识别它，而非读取此 body —— 所以应用不应以它作为吊销或存储的键。

Refresh token body 故意不包含用户显示名和邮箱 —— 这些字段在每次签发新 access token 时都会从账户行重新读取，确保用户更新资料后 access token 上的值保持最新。

### `emailAddress` 字段的选取规则

Access token 上的 `emailAddress` 在签发时按一条确定性规则选取：

1. **如果用户用 email-OTP 登录**，字段就是他输入并通过验证码证明持有的那封邮箱。
2. **否则** —— passkey、Steam ticket、AccessKey 或其他任何登录方式 —— 字段是该账户在 `EmailIdentity` 表中的**主要邮箱**。
3. **如果账户根本没有任何已验证邮箱**（例如纯 Steam 账户未挂邮箱），字段直接省略。你的验签代码应当把 `emailAddress` 视为可选。

「主要邮箱」默认是账户最先获得的那封已验证邮箱，用户也可以在 With 门户的登录方式页面中改选。Access token 一旦签发，通过 `POST /refresh` 续期时会保留原本的 `emailAddress` 值，所以应用可以在一次会话内安全地缓存它。

## OIDC ID token

当应用作为 **OIDC 接入方** 集成时，`/token` 端点会在 `access_token` 之外再返回一个 `id_token`。这个 ID token 由平台级 OIDC 签名密钥签发（**不是**应用专属密钥），通过 `https://oidc.sudomimus.com/.well-known/jwks.json` 上的 JWKS 验证。

ID token 字段遵循 OpenID Connect 标准：

```json
{
  "iss": "https://oidc.sudomimus.com",
  "sub": "<sector subject>",
  "aud": "<client_id>",
  "exp": <epoch>,
  "iat": <epoch>,
  "nonce": "<来自 /authorize（如有）>",
  "auth_time": <epoch（如有）>,
  "email": "<授予 'email' scope 时存在>",
  "name": "<授予 'profile' scope 时存在>"
}
```

| Claim | 含义 |
|---|---|
| `iss` | 总是 `"https://oidc.sudomimus.com"`。 |
| `sub` | **扇区主体** —— 与配对的 access token 上 `subject` 字段相同的（账户 × 扇区）值，**不是**原始账户 UUID。在你所属扇区内对同一用户稳定，可轮换，且不同扇区之间不同。 |
| `aud` | 接入方的 OIDC `client_id`。 |
| `exp`、`iat` | 标准 JWT 时间字段，单位秒（epoch）。 |
| `nonce` | 首次签发时回显接入方在 `/authorize` 时传入的值。按 [OIDC core 1.0 §12.1](https://openid.net/specs/openid-connect-core-1_0.html#RefreshingAccessToken)，**refresh-token grant 不会**再回显。 |
| `auth_time` | 用户实际完成认证的时间（秒）。refresh-token grant 会保留原值。 |
| `email` | 用户邮箱 —— 与配对的 access token 上的值相同。仅在授予了 `email` scope 时存在。 |
| `name` | 用户全名，由 `firstName lastName` 拼成。仅在授予了 `profile` scope 时存在。 |

OIDC 签名密钥会定期轮换；JWKS 在轮换期间会同时发布当前生效和最近退役的两把密钥，保证验签连续可用。

### OIDC 流程下的 access token

OIDC `/token` 同时也返回一个 `access_token`，形状与上面的 Connect access token **完全相同** —— 同样的 `kty`、同样的 per-application 签名密钥、同样的字段集合。区别只在签发路径；签发出来后，你的应用以完全相同的方式（`POST /info` → 缓存公钥）验签。

完整接入方流程（含 `/userinfo` 和 `/end-session`）见 [OIDC 接入方](/zh-cn/oidc/flow/)。

## 我该用哪种

- **Connect 协议**（access / refresh token 通过 `POST /info` 验签）：你的应用后端直接和 Sudomimus 对接。最低开销，无额外跳转。
- **OIDC**（ID token 通过 JWKS 验签）：你的应用已经接了某个标准 OIDC 库，或者你需要让第三方系统作为接入方。Sudomimus 此时扮演 IdP。

同一个应用通常只用其中一种 —— 没必要两种都接。

## 相关阅读

<LinkCard
    title="OIDC 接入方"
    description="完整接入方流程 —— discovery、/authorize、/token、/userinfo、/end-session、scope。"
    href="/zh-cn/oidc/flow/"
/>

<LinkCard
    title="会话管理"
    description="登录之后的会话生命周期 —— /refresh、/introspect、/logout、/revoke-all。"
    href="/zh-cn/guides/managing-sessions/"
/>

<LinkCard
    title="SDK 参考"
    description="官方 SDK 已经帮你做好了 `verifyAccessToken`（验签、kty、exp 全自动）。"
    href="/zh-cn/reference/sdks/"
/>