> 原始 Markdown 孪生体（构建期从源 Markdown 生成）。渲染页：https://docs.gatellm.io/zh-CN/howto/configure-sso · 文档索引：https://docs.gatellm.io/zh-CN/llms.txt


# 配置 SSO 企业登录

SSO（Single Sign-On，单点登录）让你用企业身份系统（IdP，Identity Provider，身份提供方，如 Azure Entra / Okta）签发的 token 调用网关，或用企业账号登录控制台，而不必给每个人/每个程序单独发网关密钥。

入口：控制台 → **设置** → **SSO** 页签。配置存在数据库里，改完即时生效，不需要重启，也不需要任何环境变量。

SSO 在网关里做**两件互相独立的事**，可以只开其一，也可以同一 provider 两件都开：

1. **数据面认证**：你的程序持 IdP 发的 Bearer token 调 `/v1/...` 等代理接口；网关验签后，从 token 里取一个名字，去**访问密钥**表里找同名的真实密钥，用那把密钥的权限放行。
2. **控制台登录**：人在登录页点"用企业账号登录"，跳去 IdP 认证，回来后网关从 id_token 取用户名，去**控制台用户**表里找同名用户，命中才让进。

> 核心原则：**网关不凭空造身份**。数据面要预先建好同名访问密钥，控制台要预先建好同名控制台用户；找不到一律拒绝（401），不会临时合成一个。

> **别和 [上游 SSO 凭证登录](/zh-CN/howto/upstream-sso-login.md) 搞混**：本页讲『用企业 IdP 认证**调用网关的人**』；那一页讲『管理员登录**上游厂商**（如 Kiro）拿一份网关转发用的刷新凭证』。方向相反。

## 30 秒搞懂四个词

后面会反复出现，先认一下：

- **issuer（签发地址）**：token 里的 `iss` 字段，是 IdP 写的一句"这张 token 是我发的"，是个网址。网关拿它**对暗号**——token 的 `iss` 必须等于你在这里填的 issuer，网关才肯认这张 token 归这个 provider 管。
- **audience（受众）**：token 里的 `aud` 字段，是"这张票开给哪个应用的"。网关拿它确认 token 是开给你这个网关的，不是开给别的应用的。
- **discovery 文档（IdP 的自我介绍）**：每个 IdP 都有一份公开的 JSON 自我介绍，里面写全了四件事——① 我是谁 `issuer`、② 我的公钥清单在哪 `jwks_uri`、③ 登录网址、④ 换 token 网址。**网关优先读这份自我介绍来拿这些网址，而不是自己猜。**
- **JWKS / 公钥清单**：IdP 的公钥列表，网关用它验"token 没被人篡改"。

## 三种 provider，每个框填什么

页面上每个 provider 都有这几个核心框（三种类型通用）：

| 框 | 填什么 |
|----|--------|
| **ID** | 给这个 provider 起的内部名字（如 `corp-azure`），URL 里会用到，建好别改。 |
| **Issuer URL（签发地址）** | 按你填的原样使用，网关不会从域名/租户去拼。token 的 `iss` 须等于它（**末尾斜杠带不带都行**，网关比对时会自动归一）。 |
| **Audience（受众）** | 须等于 token 的 `aud`。仅数据面用。 |
| **Client ID** | 你在 IdP 注册的应用 ID。控制台登录用它换 token + 校验 id_token 受众；SPA 模式还用它做数据面的客户端精确匹配。 |
| **属性映射 name_expr** | 数据面：从 token 的哪个 claim 取出"名字"，去访问密钥表找同名密钥（JSONPath 或表达式，如 `$.oid`、`$.upn`）。 |
| **控制台用户名 username_expr** | 控制台登录：从 id_token 的哪个 claim 取用户名，去控制台用户表找同名用户。 |

类型（**Type**）选 `azure` / `okta` / `generic`：前两者网关知道它们的网址规律、能自动推导；`generic` 用于任何标准 OIDC，登录/换 token 网址要你手填。

高级折叠区里还有：`jwks_uri`（手填公钥地址，一般不用）、**使用 OIDC 发现**开关、**Discovery URL（发现文档地址，可选）**、时钟容差、公钥刷新间隔。下面 Azure 专节会讲 `discovery_url` 什么时候必须填。

## Azure 专节（重点）

Azure 是最容易配错的一家，因为它的网址体系分裂、v1/v2 token 形态不同。按下面抄，基本一次过。

### issuer 填什么

- **全球 v2**：去 Entra 概览页复制 Directory (tenant) ID（一串 GUID），填进模板 `https://login.microsoftonline.com/<粘贴租户ID>/v2.0`，整串就是 issuer。**末尾 `/v2.0` 不能省**，也别填 `common`、`organizations` 或 `{tenantid}`。
- **v1 / 中国云 / 主权云**：直接填 token 里的 `iss`（解 token 看 `iss` 字段，或按租户拼 `https://sts.chinacloudapi.cn/<租户ID>`）。这种 token 的 `iss` 末尾常带斜杠——没关系，网关比对会自动归一。

### audience 何时带 `api://`

看你在 portal 里 **Expose an API → Application ID URI** 设成了什么：

- 设成裸 GUID（默认）→ audience 填裸 GUID（与 Client ID 相同）。
- 设成 `api://<client_id>` → token 的 `aud` 会带 `api://` 前缀，**这里也必须照填 `api://<client_id>`**，不能只填裸 GUID，否则对不上 → 401。

### discovery_url 去哪找（v1 / 中国云 / 主权云必填）

**为什么需要它**：v1 / 中国云 token 的 `iss` 在 `sts.*` 域名，但 IdP 的自我介绍（discovery 文档）在 `login.*` 域名——两个不同的网址。issuer 只用来对暗号，**不能**用来拼自我介绍地址（拼出来 404）。所以要单独告诉网关自我介绍在哪。全球 v2 因为 `iss` 和自我介绍同址，**不用填**，留空即可。

**首选：portal 直接抄，零猜测。**

1. 进 Entra portal（中国云是 `portal.azure.cn`）→ 左侧 **应用注册（App registrations）** → 点你那个应用。
2. 左侧菜单 **终结点（Endpoints）**。
3. 右侧面板里 **"OpenID Connect 元数据文档"** 那一行，**整行复制**填进 **Discovery URL**。

> 这条通常是 v2 的（带 `/v2.0`），**没关系，能用**：同一应用同一租户的签名密钥 v1 和 v2 共用一组，所以 v2 自我介绍里的公钥照样验得了你的 v1 token。

**备选：手拼模板**（`{tid}` 用 token 里的 `tid`，或 portal 概览页的 Directory (tenant) ID）：

- 中国云：`https://login.partner.microsoftonline.cn/{tid}/v2.0/.well-known/openid-configuration`
- 全球云：`https://login.microsoftonline.com/{tid}/v2.0/.well-known/openid-configuration`

> ⚠️ 中国云的 login 域名是 **`login.partner.microsoftonline.cn`**，**不是** `chinacloudapi.cn`——后者是 Microsoft Graph 的域名，别混。以 portal『终结点』面板为准。

**怎么确认拼对了**：浏览器或 `curl` 打开这个 discovery_url，应返回一段 JSON 且含 `jwks_uri`；再打开里面的 `jwks_uri`，看得到公钥列表就稳了。

### name_expr：把 claim 映射成访问密钥的名字

- **v1 access token 有 `upn`**（如 `alice@corp.example.com`）：若你的访问密钥 name 建的是 `@` 前面的部分，用表达式削掉域名：`{{ replace(claim:$.upn, "@.*$", "") }}` → `alice`。
- **v2 access token 没有 `upn`**：用 `$.oid`（用户对象 ID 全串），并把访问密钥的 name 也建成这串 OID。
- 前提：**访问密钥表里得有 name 等于解析结果的那把密钥**，否则 401。

## 完整配置例子

### 例子 A：Azure 中国云 v1（带 discovery_url）

```jsonc
{
  "id": "azure-cn",
  "type": "azure",
  "enabled": true,
  // 对暗号：逐字等于 token 的 iss（末尾斜杠带不带都行）。
  // 下面的租户/对象 ID 是示例占位符，请换成你自己在 portal 概览页
  // 复制的 Directory (tenant) ID / Application (client) ID。
  "issuer": "https://sts.chinacloudapi.cn/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
  // 票开给谁：逐字等于 token 的 aud。这里带 api:// 是因为 portal 把
  // Application ID URI 设成了 api://<client_id>。
  "audience": "api://11111111-2222-3333-4444-555555555555",
  "client_id": "11111111-2222-3333-4444-555555555555",
  // 数据面：upn 削掉 @ 后面 → alice（访问密钥表须有 name=alice）。
  "access_key": {
    "enabled": true,
    "name_expr": "{{ replace(claim:$.upn, \"@.*$\", \"\") }}"
  },
  // ★ 自我介绍地址：抄 portal『终结点』面板 "OpenID Connect 元数据文档" 那一行。
  // 公钥地址由它自动给出，不用再手填 jwks_uri。
  "discovery_url": "https://login.partner.microsoftonline.cn/aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee/v2.0/.well-known/openid-configuration"
}
```

### 例子 B：全球 v2（不填 discovery_url，老配置零改动）

```jsonc
{
  "id": "azure-global",
  "type": "azure",
  "enabled": true,
  "issuer": "https://login.microsoftonline.com/<你的租户ID>/v2.0",
  "audience": "<你的 aud>",
  "client_id": "<你的 client id>",
  "access_key": { "enabled": true, "name_expr": "$.oid" }
  // 没有 discovery_url → 网关按 issuer 自动拼自我介绍地址（v2 拼得对）。
}
```

## 多个 provider 怎么工作

你可以同时配 Azure + Okta + 通用 OIDC 多家。网关**不会**拿你的 token 去逐家验签：它先解出 token 的 `iss`，对暗号找到**唯一**认领的 provider，只验一次签。各家 issuer 互不相同，所以一个 token 至多被一家认领。

**没被任何 provider 认领的 token**（不是企业 IdP 发的 JWT、或普通 `sk-...` 密钥）会**回落到网关内置的访问密钥认证**——SSO 不会误拒你原来的密钥。

## 验证

配完别等踩坑才查 401 表，先端到端验证一遍。

### 1. 取一张测试 token

拿一张 IdP 签发、audience 等于你给网关配的值的 access token。以 Azure 为例可直接用 Azure CLI：

```bash
# --resource 填你配的 audience（带 api:// 前缀就照填）
az account get-access-token \
  --resource <你的 audience> \
  --query accessToken -o tsv
```

Okta / 通用 OIDC 用各自 IdP 的发 token 流程（OAuth 授权码流或 client_credentials）获取。

### 2. 验证数据面认证

用这张 token 作 Bearer 调模型列表端点，期望 **200**：

```bash
curl -s -o /dev/null -w "%{http_code}\n" http://localhost:7890/v1/models \
  -H "Authorization: Bearer <上一步拿到的 token>"
```

返回 200 说明验签通过、且访问密钥表里找到了同名密钥。返回 401 就按下方排查表逐条对。

### 3. 验证控制台企业登录

打开控制台登录页点「用企业账号登录」，跳转 IdP 认证后回来应能进控制台（前提：控制台用户表里已有与 id_token 用户名同名且启用的用户）。首次进入后到「控制台用户」页确认该用户存在、角色正确。

## 常见 401 排查表

| 现象 / 怀疑点 | 怎么查 / 怎么改 |
|--------------|----------------|
| token 是 v1 / 中国云，issuer 对不上 | issuer 填 token 的 `iss` 原样；末尾斜杠不用纠结。若 `iss` 域名是 `sts.*`，**必须**另填 `discovery_url`（login 域名）。 |
| audience 对不上 | 看 portal『Expose an API → Application ID URI』：是 `api://<id>` 就带 `api://` 填，是裸 GUID 就填裸 GUID。 |
| 验签失败 / 拉不到公钥 | `discovery_url` 域名对不对（中国云是 `login.partner.microsoftonline.cn`，不是 `chinacloudapi.cn`）；用浏览器打开它应返回含 `jwks_uri` 的 JSON。 |
| 签名验过但仍 401 | name_expr 取不到值，或取出的名字在**访问密钥表里没有同名密钥**。v2 access token 没有 `upn`/`azp` 时别用它们，改 `$.oid`。 |
| 控制台登录回不来 / 报 not_in_list | id_token 的 username_expr 取出的用户名，在**控制台用户表里没有同名且启用的用户**。 |
| token 过期 | 解 token 看 `exp`；过期就重新获取 token。 |
| 配了 SSO 但 token 像没走 SSO | 确认该 provider `enabled` 与 `access_key.enabled` 都开了；确认 SSO 插件已随镜像内置启用（官方镜像默认启用）。 |

## 下一步

- [控制台登录与角色](/zh-CN/console/login-and-roles.md) — 建控制台用户、理解角色与会话。
- [访问密钥与密钥组字段](/zh-CN/reference/access-keys-groups-fields.md) — 建数据面要用的同名访问密钥。
- [端点与认证](/zh-CN/reference/endpoints.md) — 认证顺序：SSO 与内置密钥如何衔接。
