---
title: "OIDC 登录 - QiSSO"
description: "使用 Discovery、授权码与 S256 PKCE 完成登录、回调校验和业务会话建立。"
canonical_url: "https://docs.mcent.cn/qisso/oidc-login"
---
# OIDC 登录 - QiSSO

> 使用 Discovery、授权码与 S256 PKCE 完成登录、回调校验和业务会话建立。

网站与管理后台使用授权码 + S256 PKCE，由业务 BFF 保存登录事务并完成换码。建议使用支持 Discovery、PKCE 和 ID Token 校验的成熟 OIDC 客户端库。

## 读取 Discovery

管理员提供完整 Issuer，例如：

```text
https://account.mcent.cn/realms/mcent
```

请求：

```http
GET /realms/mcent/.well-known/openid-configuration
```

检查返回的 `issuer` 与已配置值完全一致，并读取 `authorization_endpoint`、`token_endpoint`、`userinfo_endpoint`、`jwks_uri` 及支持的认证方式。可选的撤销、内省或退出端点仅在元数据实际提供时使用。

Issuer 包含 realm 路径，不等同于站点 Origin。不要混用两个环境的元数据或根据其他身份服务的路径手动拼接端点。

## 1. 创建登录事务

BFF 为每次登录生成不可预测且相互独立的 `state`、`nonce` 和 PKCE `code_verifier`。将它们保存在短期、一次性的服务端事务中，绑定发起登录的浏览器会话，并记录本次 Client ID、Issuer、回调和允许的登录后目标。

Node.js 生成随机值与 S256 challenge 的示例：

```js
import { createHash, randomBytes } from 'node:crypto'

const state = randomBytes(32).toString('base64url')
const nonce = randomBytes(32).toString('base64url')
const codeVerifier = randomBytes(32).toString('base64url')
const codeChallenge = createHash('sha256')
  .update(codeVerifier)
  .digest('base64url')
```

`code_verifier` 必须是 43–128 个允许的 ASCII 字符；challenge 使用无填充的 Base64URL 编码。示例只演示参数生成，完整事务保存、过期和消费由 BFF 实现。

## 2. 跳转授权端点

将浏览器重定向到 Discovery 给出的 `authorization_endpoint`，通过 URL 编码传递：

<table>
<thead>
  <tr>
    <th>
      参数
    </th>
    
    <th>
      值
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        client_id
      </code>
    </td>
    
    <td>
      <code>
        example-app
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        redirect_uri
      </code>
    </td>
    
    <td>
      <code>
        https://app.example.com/login/callback
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        response_type
      </code>
    </td>
    
    <td>
      <code>
        code
      </code>
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        scope
      </code>
    </td>
    
    <td>
      <code>
        openid profile email
      </code>
      
      ，按应用允许范围选择
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        state
      </code>
    </td>
    
    <td>
      本次事务的随机 state
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        nonce
      </code>
    </td>
    
    <td>
      本次事务的随机 nonce
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        code_challenge
      </code>
    </td>
    
    <td>
      本次 verifier 的 S256 摘要
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        code_challenge_method
      </code>
    </td>
    
    <td>
      <code>
        S256
      </code>
    </td>
  </tr>
</tbody>
</table>

QiSSO 处理账号登录、需要的 MFA 与应用授权。业务系统不接收用户的 QiSSO 密码。

## 3. 校验回调

BFF 收到回调后，先完成以下检查，再发送换码请求：

1. 找到与当前浏览器绑定、尚未过期且未消费的登录事务。
2. 精确匹配 `state`；拒绝缺失、重复或不匹配的参数。
3. 处理协议错误，例如用户取消授权。回调含 `iss` 时，校验其与本次 Issuer 一致；多 Issuer 场景按客户端库的防混淆机制处理。
4. 校验成功回调中的授权码，原子消费事务，防止重放。失败时清理事务并重新发起登录。

回调参数不写入日志或分析上报，不把授权码复制到前端存储。登录后跳转目标只能来自预先校验的业务路径。

## 4. 在服务端换码

向 Discovery 的 `token_endpoint` 发送表单。当前标准 token 路径为 `/realms/mcent/oauth2/token`，实际请求仍以元数据为准。

```http
POST <token_endpoint>
Authorization: Basic <按应用配置编码的客户端凭据>
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&code=<authorization_code>&redirect_uri=<registered_callback>&code_verifier=<original_verifier>
```

以上示例适用于 `client_secret_basic`。使用 `client_secret_post` 时按登记的方式在表单提交 `client_id` 与 `client_secret`；公共客户端使用 `none`，提交 `client_id` 而不发送 Secret。所有字段均需要正确的表单编码，不混用客户端认证方式。

授权码一次有效。回调、Client ID 和 verifier 必须与原事务一致；换码失败后重新发起登录，不反复重放授权码。

## 5. 验证身份并创建会话

使用 Discovery 的 `jwks_uri` 获取公钥，由 OIDC 库校验 ID Token 的签名、允许的算法、`iss`、`aud`、`exp` 及本次 `nonce`，多受众时继续执行 `azp` 等协议校验。不要仅解码 JWT 就信任其中内容。

业务身份使用 `(iss, sub)` 关联；不要用可修改的邮箱、手机号或用户名作为跨系统稳定主键。需要 UserInfo 时，用 Access Token 访问 `userinfo_endpoint`，并确认返回的 `sub` 与已验证的 ID Token 一致。

BFF 校验成功后创建自己的会话，浏览器仅接收业务会话 Cookie。生产 Cookie 使用 `HttpOnly`、`Secure` 和与登录返回方式匹配的 `SameSite` 策略；业务写操作继续执行自身 CSRF 与权限检查。令牌只在服务端安全保存，不写入 `localStorage`。

## 刷新与退出

仅在应用允许 `refresh_token`、scope 包含 `offline_access` 且实际收到 Refresh Token 时启用刷新。按相同客户端认证方式提交 `grant_type=refresh_token` 和当前 Refresh Token；若响应提供新 Refresh Token，安全替换旧值，并协调并发刷新。失败时终止本地会话或重新登录，不无限重试。

退出首先销毁本地会话、Cookie 与服务端保存的令牌。需要同时退出 QiSSO 时，只使用环境实际提供的标准退出机制及已登记跳转地址。关闭一个业务页面或清除本地 Cookie 不能推断统一身份会话已退出。

协议细节参考 [OpenID Connect Core](https://openid.net/specs/openid-connect-core-1_0.html) 与 [PKCE RFC 7636](https://www.rfc-editor.org/rfc/rfc7636)。上线前按[联调与排错](https://docs.mcent.cn/qisso/troubleshooting)验证完整流程。


## Sitemap

See the full [sitemap](https://docs.mcent.cn/sitemap.md) for all pages.
