单点登录让应用依赖身份提供方的认证结果,用户不必把提供方的密码交给每一个应用。应用仍要负责自己的授权决定、本地会话和接入配置;机密客户端还可能持有客户端凭据。委托认证不会消除应用的安全边界,也不保证以后每次登录都无需用户交互。是否需要重新认证,仍然受提供方策略、当前会话和应用要求影响。

先分清协议与令牌

SAML 与 OpenID Connect 都可支持联合身份,但它们是不同协议,不能简化成可以互换的 XML 与 JSON 编码。本文聚焦 OIDC:它在 OAuth 2.0 之上定义身份层,通过 ID Token 表达认证信息。访问令牌用于访问资源,格式也可能不同。一个看起来像 JWT 的字符串并不自动成为 ID Token;资源服务器更不能因为认识签发者,就把任意一种令牌当作访问许可。

对于 OIDC 身份令牌,应按所选流程的规则校验预期签发者、客户端受众、过期时间,以及请求时发送的 nonce,同时执行适用的授权方和认证上下文检查。签名正确但发给另一个客户端的令牌,仍然不适用。验签密钥要属于已信任的签发者,不能让未验证的签发者字符串驱动程序去任意元数据服务器取得“可信”密钥。

允许的算法和密钥类型,应由验证器的配置确定,而不是由不可信输入自由选择。头部声明必须符合该策略,并与实际密码学操作一致。HS256 本身并非错误:OIDC 定义了使用适当共享秘密的协商式 MAC 签名。把 RSA 公钥当作这个共享秘密,才属于算法混淆。陌生的密钥编号也可能来自合法轮换,但无论如何都不能成为跳过验证的理由。

坏掉的样子

最容易误导人的成功是解码成功。Base64url 与 JSON 解析可以显示一组貌似可信的声明,却没有证明是谁签发了它。管理员身份、正确受众和未来过期时间,都可以被随意编造。不要把真实令牌粘贴到在线解码网站或事故日志;其中可能含有个人信息和仍可使用的凭据。诊断应经过批准的路径,并且只查看确有必要的字段。

登录请求的绑定与令牌验证同样重要。应采用持续维护的 OIDC 接入组件,并按客户端类型与流程使用重定向校验、请求关联及 PKCE 等保护。RFC 9700 给出了 OAuth 安全指导。只检查几个 JWT 字段,不能替代完整协议处理;如果接收了属于另一次请求的令牌,即使签名是真的,也仍可能产生安全问题。

快速验证

把脚本保存为 inspect-sso-fixture.py,在保持断言开启的情况下运行 python3 inspect-sso-fixture.py。它只使用虚构数据,不连接提供方,不读取密钥或凭据,也不访问网络或第三方软件包。这是一项完整的“解码不等于认证”演示,不是供生产使用的令牌解析器,更不是自行实现身份验证的起点:

import base64
import json


def encode(value):
    raw = json.dumps(value, separators=(",", ":")).encode()
    return base64.urlsafe_b64encode(raw).rstrip(b"=").decode()


def inspect_fixture(token):
    header, payload, signature = token.split(".")
    decode = lambda part: json.loads(base64.urlsafe_b64decode(
        part + "=" * (-len(part) % 4)))
    return {"header": decode(header), "claims": decode(payload),
            "signature_text": signature, "verified": False}


header = {"alg": "RS256", "kid": "invented-key"}
claims = {"iss": "https://issuer.example", "aud": "billing",
          "sub": "user-7", "exp": 2000000000}
first = encode(header) + "." + encode(claims) + ".not-a-signature"
second = encode(header) + "." + encode(dict(claims, sub="admin")) + ".not-a-signature"
rows = [inspect_fixture(token) for token in (first, second)]
assert [row["claims"]["sub"] for row in rows] == ["user-7", "admin"]
assert all(row["header"]["alg"] == "RS256" for row in rows)
assert all(row["verified"] is False for row in rows)
print("decoded subjects: user-7, admin")
print("cryptographic verification performed: False")

两个字符串都声称使用 RS256,面向 billing,并来自同一个签发者。把用户改成 admin 不需要任何签名密钥,因为最后一段只是编造的文字。脚本会打印两个身份,并明确报告没有进行密码学验证。它没有验证签名、有效期、受众或发现文档;结果中的假值专门防止读者把一份成功解析的输出误认成已经认证的身份。

实际接入需要另外的测试集,确认错误签发者、错误受众、过期令牌、错误签名、请求 nonce 不匹配及令牌类型不适用时都会被拒绝。还应使用真实库与提供方验证受支持的密钥轮换和错误处理。这些是要在相应环境中完成的集成检查,不能声称本地解码样本已经证明了它们,更不能因为正向登录一次成功就省略负向案例。

为登出单独定义约定

提供方会话、应用会话、访问令牌与刷新令牌,分别具有自己的生命周期。登录时使用的身份令牌过期,不会自动销毁应用 cookie 或服务端会话。因此,缩短令牌期限本身,并不能回答停用账号后现有应用访问会在多久内结束。需要把这些对象及其终止条件逐一列出,避免用其中一个计时器代替整条访问链路。

JWT 的 jti 是可选编号,不是所有系统都必须执行的撤销名单查询。如果要求及时登出,应选择受支持的机制,并确认应用确实终止对应会话。OIDC 后端登出定义了需要独立验证的登出消息及会话清理动作。受支持的令牌自省回答的是某个令牌当前状态,并不会自动作废另一个独立的应用会话。应明确记录离职或停用的实际保证,以及通知失败时的处理方式。

来源