---
title: 管理会话
description: 登录之后的会话生命周期端点 —— Connect API 的 refresh、introspect、logout、revoke-all。
editUrl: true
head: []
template: doc
sidebar:
  order: 4
  hidden: false
  attrs: {}
pagefind: true
draft: false
---

import { Tabs, TabItem } from "@astrojs/starlight/components";

登录是会话的起点，不是集成的终点。Connect 提供四个端点处理登录之后的会话生命周期：**`/refresh`**、**`/introspect`**、**`/logout`**、**`/revoke-all`**。本页是它们的唯一参考。

走 OIDC 流程的用户另请参考 [OIDC 指南中的 `/end-session`](/zh-cn/oidc/flow/#5-结束会话) —— 它有相关但更窄的用途。

## 一览

| 端点 | 鉴权凭据 | 幂等？ | 影响范围 |
|---|---|---|---|
| `POST /refresh` | refresh token 本身 | 否（轮换 refresh token —— 每个令牌只能用一次） | 单个会话 |
| `POST /introspect` | access token 本身 | 只读 | 单个会话 |
| `POST /logout` | refresh token 本身 | 是（两次调用都返回 `revoked: true`） | 单个会话 |
| `POST /revoke-all` | Client-auth JWT（RS256） | 是 | 某账户在调用应用下的所有会话 |

这四个端点都不需要额外的基础设施 —— 复用你最初集成时就已经有的密钥。

## `/refresh` —— 延续会话

用 refresh token 换取一个新的 access token **和一个新的 refresh token**。刷新采用**严格轮换**（[OAuth 2.1 BCP §4.14.2](https://www.rfc-editor.org/rfc/rfc9700#section-4.14.2)）：你提交的 refresh token 在同一次调用中被消费并作废，响应里会把它的替代者交给你。请存下新的 `refreshToken`，并在下一次刷新时使用它。重复提交一个已经用过的 refresh token 会被视为令牌族被攻陷 —— 整个 refresh token 族都会被吊销，用户必须重新认证。例外是对**同一个**令牌近乎同时的并发刷新（例如两个浏览器标签页同时刷新）：它们会收敛到同一个新会话，而不会把用户登出。但在替代令牌已签发**之后**再复用旧令牌仍会触发失陷，所以无论如何都请始终存储并使用最新轮换出来的令牌。

```bash
curl -X POST https://connect-api.sudomimus.com/refresh \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "..." }'
```

响应：

```json
{
  "accessToken": "<JWT>",
  "refreshToken": "<轮换后的 JWT>",
  "claims": {
    "email":     { "requirement": "REQUIRED", "state": "GRANTED" },
    "firstName": { "requirement": "OPTIONAL", "state": "GRANTED" },
    "lastName":  { "requirement": "OFF",      "state": "UNKNOWN" }
  }
}
```

请持久化轮换后的 `refreshToken`，替换掉你刚用过的那个 —— 旧的现在已经失效。`claims` 块与 `/redeem` 返回的逐条声明视图相同 —— 如何解读见 [`claims` 块](/zh-cn/concepts/identity-claims/#claims-块)。

**鉴权**：无需 —— 持有 refresh token 即是凭据。

新的 access token 的 TTL 是最初 `/redeem`（或 `/direct-issue/*`、OIDC `/token`）时算出的那一份；refresh 不会重新决议 TTL。

### Refresh 可能因声明而失败

`/refresh` 的结果不只有成功或令牌被吊销。如果自上次签发令牌后，某条 **required** 声明不再满足，例如开发者将策略从 optional 改为 required，或用户撤销授权，本次刷新会返回 `ClaimConsentRequired`，而不会签发缺少该声明的令牌。

补救方式取决于客户端类型，因为 `/refresh` 自己无法收集同意：

- **原生客户端**（Steam / AccessKey）靠重新跑一次原本的 direct-issue 来补救，它会返回一个 [Errand](/zh-cn/native/claims-and-errand/) 交接，让用户授予同意。
- **浏览器应用**靠再次引导用户走一次常规交互式登录来补救。

这在实践中很少见 —— 只在策略或授权于会话中途变化时才会发生 —— 但请把你的刷新路径设计成弹出重新认证提示，而不是把每一次刷新失败都当成强制登出。

## `/introspect` —— 这个令牌还有效吗

向 Sudomimus 查询某个 access token 的当前状态。用它实现「跨服务及时撤销会话」—— 例如用户点击「在所有设备退出」后，希望其他标签页或服务能在有限时间内察觉。

```bash
curl -X POST https://connect-api.sudomimus.com/introspect \
  -H "Content-Type: application/json" \
  -d '{ "accessToken": "..." }'
```

响应：

```json
{
  "status": "active",
  "recommendedRecheckSeconds": 600
}
```

**`status`** 可取：

- `"active"` —— 关联的 refresh token 存在、未被挂起、未过期。
- `"revoked"` —— 关联的 refresh token 已被显式挂起（通过 `/logout` 或 `/revoke-all`）。
- `"expired"` —— 关联的 refresh token 已过期。
- `"not_found"` —— 在调用应用下找不到该令牌，或令牌无法解析。

**`recommendedRecheckSeconds`** 是 Sudomimus 建议的缓存时长，超过之后再次 introspect。目前总是 600 秒。把 access token 自己的 `exp` 当作有效期上限；introspect 是用来**及时**发现撤销的，不是用来替代签名验证。

**鉴权**：无需 —— access token 本身就是凭据。任何持有该令牌的一方都能问它是否还有效。

### 什么时候调 introspect

本地签名验证负责正确性；introspect 负责新鲜度。一个合理的搭配：

- **每次请求**都本地验证 access token 的签名和 `exp`（开销很小）。
- **机会主义地**调 `/introspect` —— 每 N 分钟一次、放进后台任务，或者在用户可见的状态变化后调用。

不要每次请求都 introspect，否则就失去了使用签名令牌的初衷。

## `/logout` —— 单会话失效

挂起一个 refresh token。其对应的 access token 在 `/introspect` 上不再被报告为 active，对它的 `/refresh` 也会失败。

```bash
curl -X POST https://connect-api.sudomimus.com/logout \
  -H "Content-Type: application/json" \
  -d '{ "refreshToken": "..." }'
```

响应：

```json
{ "revoked": true }
```

- `revoked: true` —— 令牌被挂起（或者本来已经被挂起/过期；重复调用没问题）。
- `revoked: false` —— 令牌无效或未找到。

**鉴权**：无需 —— 持有 refresh token 即可对这个会话登出（[RFC 7009](https://www.rfc-editor.org/rfc/rfc7009) 风格）。

:::note[`/logout` 不会从密码学上「销毁」 access token]
一次 `/logout` 调用只是把 refresh-token 记录挂起。已经签发出去的 access token 在 `exp` 之前仍然在语法上有效 —— 其他服务察觉的方式只有 `/introspect`（或者等到 access token 过期、`/refresh` 失败）。设计「全设备登出」UX 时要把这一点考虑进去。
:::

## `/revoke-all` —— 吊销账户所有会话

挂起某账户在调用应用下签发的所有 refresh token。用于账户接管事件响应、「在所有设备登出」，或支持团队发起的会话终止。

账户通过其 **扇区主体（sector subject）** 标识 —— 即你的应用在令牌上看到的 `subject` 值（你用来标识用户的那个 `sub`），而非原始账户 UUID。Sudomimus 会在内部把它反向映射回底层账户。

```bash
curl -X POST https://connect-api.sudomimus.com/revoke-all \
  -H "Content-Type: application/json" \
  -H "Authorization: SudomimusClientJWT $SUDOMIMUS_CLIENT_AUTH_JWT" \
  -d '{ "subject": "sub_9SQ5535CRWNDDM2T" }'
```

响应：

```json
{ "revokedCount": 3 }
```

`revokedCount` 是本次调用挂起的 refresh token 数量。已经挂起或已过期的不会被重复计数。

**鉴权**：和 `/establish` 一样需要 client-auth JWT。操作范围是隐式的 —— 只触及**调用应用**下签发给该账户的会话。无法用某应用的 client-auth 密钥撤销另一个应用的会话。

## 串起来 —— 典型会话生命周期

```
   /redeem ──► access (3h)   refresh (30d)
                 │              │
                 │              │
                 ▼              │
        每次请求都用              │
                 │              │
                 ▼  过期         │
        /refresh ◄──────────────┤  新 access + 轮换 refresh
                 │              │
                 ▼              │
                 │              │
   用户点击「登出」 ──► /logout
                                │
                                ▼
                            吊销
                                │
                  /introspect ──► "revoked"
```

OIDC 风格的 refresh 见 [OIDC 接入方 —— Refresh](/zh-cn/oidc/flow/#4-refresh)。