跳转至

实现登录

使用成熟 OIDC 客户端库完成授权码流程。网页应用建议由服务端完成换令牌和资料读取,浏览器仅持有应用自己的安全会话 Cookie。

1. 读取 Discovery

这个公开请求无需认证,可以直接执行:

curl --fail --silent --show-error \
  https://id.open-sai.com/realms/sai/.well-known/openid-configuration

校验返回的 issuer 完全等于 https://id.open-sai.com/realms/sai,再使用 authorization_endpoint、token_endpoint、userinfo_endpoint 和 jwks_uri 等字段。固定可信 issuer,不接受用户输入的发现地址。保持 HTTPS 证书校验。

2. 创建登录事务

每次登录分别生成并保存一次性的 state、nonce 和 PKCE code_verifier。将它们与发起登录的浏览器及应用内返回路径绑定,设置短有效期,完成或放弃登录后立即作废,不能重复使用。

PKCE 使用 43–128 个字符的随机 verifier,按 RFC 7636 计算 BASE64URL(SHA256(verifier)),去掉 Base64 填充。state 防止回调被串换;nonce 绑定 ID token;PKCE 将授权码绑定到发起换令牌的客户端,三者不能互相替代。

将浏览器重定向到发现得到的授权端点。例如,以下请求中的大写占位值均需由客户端库按本次事务生成:

GET /realms/sai/protocol/openid-connect/auth?response_type=code&client_id=example-web&redirect_uri=https%3A%2F%2Fapp.example.org%2Foidc%2Fcallback&scope=openid%20profile%20email&state=RANDOM_STATE&nonce=RANDOM_NONCE&code_challenge=GENERATED_S256_CHALLENGE&code_challenge_method=S256 HTTP/1.1
Host: id.open-sai.com

普通登录不添加 prompt=login 或 max_age=0,让有效 SSO 直接继续。修改敏感信息等需要重新验证身份的操作,应在接入时约定。正常浏览页面时不要反复要求用户输入密码。

应用内的项目、页面和未完成操作保存在本地登录事务中。redirect_uri 始终使用已登记的固定回调,不把任意返回地址拼进 OIDC 回调。

3. 接收回调并换令牌

成功回调包含 code 和 state。先核对当前浏览器事务、state 和有效期,再由应用服务端使用授权码和原 verifier 换取令牌。redirect_uri 必须与授权请求一致,授权码只能使用一次。

若回调包含 error,同样核对 state 后结束事务;不要继续换令牌。未经登记的回调地址可能直接得到错误页,而非错误重定向。

机密客户端的首选请求形式:

POST /realms/sai/protocol/openid-connect/token HTTP/1.1
Host: id.open-sai.com
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code&client_id=example-web&code=ONE_TIME_CODE&redirect_uri=https%3A%2F%2Fapp.example.org%2Foidc%2Fcallback&code_verifier=ORIGINAL_VERIFIER&client_assertion_type=urn%3Aietf%3Aparams%3Aoauth%3Aclient-assertion-type%3Ajwt-bearer&client_assertion=SIGNED_CLIENT_ASSERTION

这里的 assertion 由成熟库用登记私钥签名:iss 和 sub 为 client_id,aud 使用登记认可的 token endpoint,exp 为短有效期,并生成新的 jti。它证明客户端身份,与用户的 ID token 不同。私钥不发送给 SAI。

获批使用 client_secret_basic 时,改用库生成的 HTTP Basic 认证头,不发送 client_assertion。不要把客户端密钥放到 URL 或示例命令的参数中。无密钥客户端只有在按该类型登记后才可使用 client_id 和 PKCE,不能因换令牌失败而自行降级认证。

4. 验证 ID token

由 OIDC 库验证签名和完整协议条件后才建立应用会话:

  • 从可信 Discovery 的 jwks_uri 读取公钥,按登记的算法白名单验签;拒绝 none 和不符算法。
  • iss 精确匹配,aud 包含本客户端;多 audience 时按 OIDC 规则核验 azp。
  • 核验 exp、iat、请求时保存的 nonce,以及实际请求了 max_age 时的 auth_time。
  • 使用区分大小写的 (iss, sub) 识别账号;保存 sid,用于识别退出通知对应的登录会话。

不要用简单 Base64 解码代替验签,不将 ID token 当作任意业务 API 的访问令牌。昵称、邮箱、公开标识或计算用户名都不能代替 sub。

签名密钥可能轮换。让库按缓存策略更新 JWKS;遇到未知 kid 可刷新一次,仍无法验证则拒绝该令牌,不能跳过验签。

使用 SDK

下例展示 Authlib 的服务端客户端配置,配合你自己的登录事务存储和回调处理器使用:

import os
from pathlib import Path

from authlib.integrations.requests_client import OAuth2Session
from authlib.oauth2.rfc7523 import PrivateKeyJWT

client = OAuth2Session(
    client_id=os.environ["OIDC_CLIENT_ID"],
    client_secret=Path(os.environ["OIDC_PRIVATE_KEY_FILE"]).read_text(),
    token_endpoint_auth_method="private_key_jwt",
    redirect_uri="https://app.example.org/oidc/callback",
    scope="openid profile email",
    code_challenge_method="S256",
)
client.register_client_auth_method(PrivateKeyJWT())

配置中的私钥文件由应用部署环境提供。授权、回调、OIDC claims 校验和令牌保存仍需按上面的完整流程连接;这段初始化代码本身不构成登录实现。

也可使用 Authlib Flask OIDC 集成、Node.js openid-client或Spring Security OAuth2 Login。为所选库配置相同的 issuer、回调、PKCE S256、客户端认证和算法白名单,确认它负责 state、nonce 及 ID token 校验。不要依照库的默认值启用额外 grant 或省略 PKCE。

令牌保存在应用服务端,应用 Cookie 使用 Secure、HttpOnly 和合适的 SameSite。不要把访问/刷新令牌放在地址栏、浏览器本地存储、前端日志或监控事件里。