☰
Apereo CAS SAML2 IdP 中 NameID 的格式选择与值构造详解
2026/9/25 10:50:52 网站建设 项目流程
  • 后端
  • 认证鉴权
  • 单点登录

【免费下载链接】cas

Apereo CAS - Identity & Single Sign On for all earthlings and beyond.

项目地址:https://gitcode.com/gh_mirrors/ca/cas
点击查看免费下载

在 Apereo CAS 作为 SAML2 IdP(身份提供方)的部署中,<saml2:NameID>是断言中用于标识"本次认证用户"的核心元素,其取值与格式往往直接决定服务提供方(SP)能否正确识别用户。本文基于 CAS 官方文档 SAML2 NameID Selection 的完整配置示例,结合 CAS 源码中 NameID 构建与用户名解析的实际实现路径,讲清楚两件事:如何为某个已注册的 SAML 服务指定所需的 NameID 格式(requiredNameIdFormat),以及如何控制 NameID 的最终取值(usernameAttributeProvider),并覆盖 email、unspecified、transient、persistent 四种典型格式的完整可运行服务定义。

核心概念:格式与取值是两条独立的配置线

每个 SAML 服务(SamlRegisteredService)可以单独指定一个所需的 NameID 格式(required Name ID format)。如果未定义,CAS 会去查阅 SP 元数据(metadata)中声明的受支持格式来选定。另一方面,NameID 的值始终是"认证后的用户名"——如果你已经为该服务配置了特定的属性作为返回给该服务的认证用户标识(即 PrincipalId 的定制发布,参考 CAS 属性发布与 PrincipalId 定制),那么该属性值就会连同正确的格式一起用于构造 NameID。

从源码结构看,这套逻辑集中在 SamlProfileSamlNameIdBuilder 中:

  • getSupportedNameIdFormats先从元数据适配器(context.getAdaptor().getSupportedNameIdFormats())收集 SP 声明支持的格式;若元数据为空,则回退使用urn:oasis:names:tc:SAML:2.0:nameid-format:transient作为默认;
  • 若注册服务上配置了requiredNameIdFormat,该格式会被addFirst插入到受支持格式列表的最前面,从而优先选用;
  • getRequiredNameIdFormatIfAny还会读取 AuthnRequest 中NameIDPolicy声明的格式,若该格式不在受支持列表中,CAS 会记录告警日志,提示"请求的格式可能不会被满足",并列出元数据中的实际支持集合;
  • 最终通过SamlAttributeBasedNameIdGenerator按所选格式对用户名进行编码。

这意味着服务定义上的requiredNameIdFormat具有"强制覆盖"语义:它排在元数据格式之前被评估,第一个能成功编码出 NameID 的格式即胜出。

requiredNameIdFormat与skipGeneratingTransientNameId均为 SamlRegisteredService 的一级字段。前者为普通字符串属性;后者带有@JacksonInject(value = "skipGeneratingTransientNameId"),即在 JSON 服务定义中以顶层布尔字段出现,默认false(正常生成一次性临时值)。

requiredNameIdFormat 的合法取值

SamlProfileSamlNameIdBuilder.parseAndBuildRequiredNameIdFormat方法对配置值做了大小写不敏感的子串匹配与归一化,可识别的格式包括:

配置值(可含子串)归一化结果(OpenSAML NameIDType 常量)
...nameid-format:emailAddressurn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress
...nameid-format:transienturn:oasis:names:tc:SAML:2.0:nameid-format:transient
...nameid-format:persistenturn:oasis:names:tc:SAML:2.0:nameid-format:persistent
...nameid-format:entityurn:oasis:names:tc:SAML:2.0:nameid-format:entity
...nameid-format:X509Subjecturn:oasis:names:tc:SAML:2.0:nameid-format:X509Subject
...nameid-format:WindowsDomainQualifiedNameurn:oasis:names:tc:SAML:1.1:nameid-format:WindowsDomainQualifiedName
...nameid-format:kerberosurn:oasis:names:tc:SAML:1.1:nameid-format:kerberos
...nameid-format:encryptedurn:oasis:names:tc:SAML:2.0:nameid-format:encrypted
其他 / 空白urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified

注意匹配方式是"包含即命中"(Strings.CI.contains),因此写全 URN 或只写格式关键片段均可被正确识别;无法识别的值一律落到unspecified。

此外,CAS 在单点登出(SLO)消息构造中同样会消费该字段:SamlIdPProfileSingleLogoutMessageCreator 优先取服务定义中的requiredNameIdFormat作为登出消息里的 NameID 格式,保证 SSO 与 SLO 两侧的用户标识格式一致。

四种典型格式的服务定义(完整示例)

以下四个 JSON 服务定义均基于 SamlRegisteredService,可直接用于 JSON 服务注册文件(如services/*.json)。

1. Email Address 格式

使用emailAddress格式,并以mail属性值作为最终 NameID 值:

{ "@class": "org.apereo.cas.services.RegisteredService", "serviceId": "the-entity-id-of-the-sp", "name": "SAML Service", "metadataLocation": "/path/to/sp-metadata.xml", "id": 1, "requiredNameIdFormat": "urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress", "usernameAttributeProvider" : { "@class" : "org.apereo.cas.services.PrincipalAttributeRegisteredServiceUsernameProvider", "usernameAttribute" : "mail" } }

原文档示例中@class写为org.apereo.cas.support.saml.services.SamlRegisteredService;SamlRegisteredService继承自BaseWebBasedRegisteredService,具体选择哪个类取决于所用服务注册实现,此处保留原文档写法亦可被序列化定制器识别。

2. Unspecified 格式(带 scope 拼接)

使用unspecified格式,并以sysid属性值为值、example.org为 scope,最终 NameID 值为<sysid-attribute-value>@example.org:

{ "@class": "org.apereo.cas.services.RegisteredService", "serviceId": "the-entity-id-of-the-sp", "name": "SAML Service", "metadataLocation": "/path/to/sp-metadata.xml", "id": 1, "requiredNameIdFormat": "urn:oasis:names:tc:SAML:2.0:attrname-format:unspecified", "usernameAttributeProvider" : { "@class" : "org.apereo.cas.services.PrincipalAttributeRegisteredServiceUsernameProvider", "usernameAttribute" : "sysid", "scope": "example.org" } }

scope 的拼接逻辑来自 BaseRegisteredServiceUsernameAttributeProvider.scopeUsernameIfNecessary:只要设置了scope,解析出的用户名就会被格式化为用户名@scope的形式。这是为 SP 提供带域名限定标识的低成本手段。

3. Transient 格式(跳过一次性值生成)

使用transient格式,但跳过按规范生成一次性随机值,直接以cn属性的大写形式作为 NameID 值:

{ "@class": "org.apereo.cas.services.RegisteredService", "serviceId": "the-entity-id-of-the-sp", "name": "SAML Service", "metadataLocation": "/path/to/sp-metadata.xml", "id": 1, "requiredNameIdFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:transient", "skipGeneratingTransientNameId" : true, "usernameAttributeProvider" : { "@class" : "org.apereo.cas.services.PrincipalAttributeRegisteredServiceUsernameProvider", "usernameAttribute" : "cn", "canonicalizationMode" : "UPPER" } }

源码中的行为对照(SamlProfileSamlNameIdBuilder.getNameIdValueFromNameFormat):

  • 当格式为transient且skipGeneratingTransientNameId为true时,直接使用认证后的 principal id 作为值;
  • 否则调用persistentIdGenerator.generate(principalId, entityId)生成一个基于 principal 与 SP entityId 的不可逆一次性标识。

canonicalizationMode的取值对应CaseCanonicalizationMode枚举(如NONE、LOWER、UPPER、MIXED等),在 BaseRegisteredServiceUsernameAttributeProvider.resolveUsername 中于去除模式(removePattern)与 scope 处理之后应用。

4. Persistent 格式(Shibboleth 兼容算法)

使用cn属性值创建 persistent NameID,采用 Shibboleth 兼容的生成算法:

{ "@class": "org.apereo.cas.services.RegisteredService", "serviceId": "the-entity-id-of-the-sp", "name": "SAML Service", "metadataLocation": "/path/to/sp-metadata.xml", "id": 1, "requiredNameIdFormat": "urn:oasis:names:tc:SAML:2.0:nameid-format:persistent", "usernameAttributeProvider" : { "@class" : "org.apereo.cas.services.AnonymousRegisteredServiceUsernameAttributeProvider", "persistentIdGenerator" : { "@class" : "org.apereo.cas.authentication.principal.ShibbolethCompatiblePersistentIdGenerator", "salt" : "aGVsbG93b3JsZA==", "attribute": "cn" } } }

其中 ShibbolethCompatiblePersistentIdGenerator 的关键实现细节:

  • salt 处理:salt字段按 Base64 解释(示例中aGVsbG93b3JsZA==解码即helloworld)。若未提供 salt,首次生成时会随机产生一个 16 位字符串作为默认值——从源码看这意味着不显式配置 salt 将导致每次部署重启后 salt 变化,persistent 标识不再稳定,因此生产环境必须显式固定 salt;
  • digest 算法:prepareMessageDigest使用MessageDigest.getInstance("SHA")(注意是 legacy SHA-1,与 Shibboleth 兼容),当指定了 service(此处为 SP 的 entityId)时,先写入 service 与 principal 并分别以!分隔,再对 salt 做摘要,最终 Base64 无换行编码输出;
  • attribute 字段:determinePrincipalIdFromAttributes优先取attribute指定的属性值(示例中为cn)参与哈希,属性不存在时回退到默认 principal id。

AnonymousRegisteredServiceUsernameAttributeProvider作为usernameAttributeProvider的容器,负责把内部配置好的persistentIdGenerator挂到服务定义上;其用户名解析结果即生成的 persistent id,再交由 NameID 编码器以persistent格式包装输出。

usernameAttributeProvider 的取值解析链路

四个示例中的PrincipalAttributeRegisteredServiceUsernameProvider与AnonymousRegisteredServiceUsernameAttributeProvider都派生自 BaseRegisteredServiceUsernameAttributeProvider,共享一组"后处理"配置项,可配合上文示例按需叠加:

字段默认值作用
canonicalizationModeNONE大小写规范化:NONE/LOWER/UPPER/MIXED(MIXED为大写姓氏 + 小写名)
scope空非空时输出用户名@scope
removePattern空非空时按正则从用户名中剔除匹配片段
encryptUsernamefalsetrue时经RegisteredServiceCipherExecutor加密用户名(要求服务配置了加密密钥)

PrincipalAttributeRegisteredServiceUsernameProvider 的取值优先级值得注意:

  1. usernameAttribute支持逗号分隔的多个属性名,按顺序取第一个命中的值;
  2. 优先在属性发布策略(attribute release policy)解析出的属性集中查找;找不到时再退回 principal 原始属性;
  3. 两者都没有时会打印告警日志并回退到默认 principal id,而非报错——配置了错误属性名不会让断言构建失败,只会静默地改变 NameID 的值,排障时应关注Principal [{}] does not have an attribute [{}]这条 info 日志。

处理顺序为:解析出原始用户名 →removePattern剔除 →scope拼接 → 大小写规范化 →(可选)加密。这一顺序保证了示例 3 中UPPER规范化发生在 scope 拼接之后,若需要"仅属性值大写、scope 保持小写",可调整属性值本身或通过removePattern等手段控制。

与 SP 元数据和 AuthnRequest 的交互

完整流程可概括为:

  1. getSupportedNameIdFormats:SP 元数据声明的格式 + (如有)requiredNameIdFormat置顶;元数据无声明时兜底 transient;
  2. getRequiredNameIdFormatIfAny:读取 AuthnRequest 的NameIDPolicy格式,若与受支持集合不符则告警(不中断流程,请求的格式"可能不被满足");
  3. determineNameId:按格式顺序逐个尝试编码,第一个成功即返回;
  4. finalizeNameId:按服务定义补充nameIdQualifier(nameIdQualifier字段,或从 IdP 元数据推断)与SPNameQualifier(serviceProviderNameIdQualifier字段,默认取 SP 的 entityId)。这两个字段在 SamlRegisteredService 中定义,并分别有skipGeneratingNameIdQualifier、skipGeneratingServiceProviderNameIdQualifier开关(或在字段值上写none)可以整体关闭,用于对接不接受 qualifier 的 SP。

测试用例 SamlProfileSamlNameIdBuilderTests 覆盖了上述格式选择与值构造行为,可作为回归验证的参考。

小结

  • requiredNameIdFormat决定 NameID 的格式,且在元数据格式中优先级最高;未识别的值会静默落到unspecified。
  • NameID 的值由usernameAttributeProvider决定:属性选择(含 scope、大小写规范化、正则剔除、加密后处理),transient 格式还受skipGeneratingTransientNameId控制是否真正生成一次性值,persistent 格式则通过ShibbolethCompatiblePersistentIdGenerator的 salt + attribute 组合产生稳定的匿名标识。
  • 两条线共同决定了断言中<saml2:NameID>的最终形态,配置时务必同时校验 SP 元数据声明、AuthnRequest 中的NameIDPolicy与服务定义三者的一致性。
  • 后端
  • 认证鉴权
  • 单点登录

【免费下载链接】cas

Apereo CAS - Identity & Single Sign On for all earthlings and beyond.

项目地址:https://gitcode.com/gh_mirrors/ca/cas
点击查看免费下载
上一篇:洛雪音乐助手架构深度解析:现代Electron应用的多源音乐聚合方案
下一篇:6个实用技巧:深度掌握洛雪音乐助手的高效使用方法

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询