☰
Apereo CAS SAML2 IdP 动态元数据:Metadata Query Protocol(MDQ)配置与源码实现解析
2026/9/25 3:22:56 网站建设 项目流程
  • 后端
  • 认证鉴权
  • 单点登录

【免费下载链接】cas

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

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

本文围绕 Apereo CAS 的 SAML2 Identity Provider(IdP)能力,讲解如何通过 Metadata Query Protocol(MDQ,元数据查询协议)从远程元数据服务器动态拉取 SP 的元数据。内容包括注册服务的 JSON 配置方法、cas.authn.saml-idp.metadata.mdq配置项说明,以及结合 CAS 源码对占位符替换、ETag 缓存、备份文件兜底等底层机制的完整剖析。

什么是 MDQ,CAS 为什么需要它

在传统的 SAML IdP 集成中,SP(Service Provider)的元数据通常通过静态文件、固定 URL 或人工维护的方式提供给 IdP。当 SP 的断言签名密钥、SSO 端点等配置发生变更时,IdP 端往往需要同步更新文件并重启或等待缓存过期。

CAS 支持 Metadata Query Protocol(MDQ)来解决这一问题。MDQ 是一种 REST 风格的 API,允许客户端按 entityID 动态查询实体元数据。配置后,CAS 的 SAML 服务会在运行时向 MDQ 服务器发起查询请求,实时获取最新的 SP 元数据,从而避免人工维护静态元数据文件。原始文档见 Configuring-SAML2-DynamicMetadata-MDQ.md。

配置一个使用 MDQ 的 SAML 注册服务

要让某个 CAS SAML 服务通过 MDQ 服务器获取元数据,核心做法是将注册服务定义中的metadataLocation指向 MDQ 查询服务器实例。文档给出的完整配置示例如下:

{ "@class" : "org.apereo.cas.services.SamlRegisteredService", "serviceId" : "the-entity-id-of-the-sp", "name" : "SAMLService", "id" : 10000003, "evaluationOrder" : 10, "metadataLocation" : "https://mdq.server.org/entities/{0}" }

配置要点:

  • serviceId:SP 的 entityID,是 CAS 匹配请求时识别该服务的主要依据;
  • metadataLocation:MDQ 查询服务器的地址。其中的{0}是一个 entityID 占位符,由 CAS 在运行时动态处理并替换为实际要查询的实体 ID(经 URL 编码后填入);
  • 该属性支持逗号分隔的多个位置,CAS 会依次处理多个元数据地址。

源码如何识别一个 MDQ 位置

从源码结构看,CAS 判断某个metadataLocation是否为 MDQ 配置的依据非常明确——位置必须以/entities/{0}结尾。这一逻辑实现在 SamlUtils.java:

public static boolean isDynamicMetadataQueryConfigured(final String metadataLocation) { return StringUtils.isNotBlank(metadataLocation) && metadataLocation.trim().endsWith("/entities/{0}"); }

MDQ 解析器通过该方法决定自己是否“认领”这个服务,而通用的 URL 元数据解析器则会主动排除 MDQ 形式的位置(见 UrlResourceMetadataResolver.java),二者互不干扰:

// MetadataQueryProtocolMetadataResolver.supports() return locations.stream().anyMatch(SamlUtils::isDynamicMetadataQueryConfigured);

运行时流程:占位符替换、ETag 与备份兜底

MDQ 的核心实现类是 MetadataQueryProtocolMetadataResolver.java,它继承自UrlResourceMetadataResolver并覆盖了三个关键方法,对应 MDQ 与普通 URL 拉取元数据的所有差异。

1. 动态确定要查询的 entityID 与 URL

在getMetadataLocationsForService中,解析器优先从查询条件CriteriaSet中取出EntityIdCriterion携带的 entityID;若不存在,则回退到注册服务自身的serviceId:

val entityId = Optional.ofNullable(entityIdCriteria) .map(EntityIdCriterion::getEntityId) .orElseGet(service::getServiceId); if (StringUtils.isBlank(entityId)) { throw new SamlException("Unable to determine entity id to fetch metadata via MDQ for " + service.getName()); }

随后每个候选位置中的{0}占位符都会被替换为 URL 编码后的 entityID(location.replace("{0}", EncodingUtils.urlEncode(entityId))),得到最终的 MDQ 查询 URL。这也解释了为什么文档强调{0}是“运行时动态处理并替换”的——同一个metadataLocation配置可以服务不同的实体。

2. 请求构造:Basic 认证、Content-Type 与 If-None-Match

fetchMetadata方法构造实际 HTTP 请求,请求头与参数全部来自 MDQ 专用配置(即cas.authn.saml-idp.metadata.mdq属性组):

val metadata = samlIdPProperties.getMetadata().getMdq(); headers.put(HttpHeaders.CONTENT_TYPE, metadata.getSupportedContentType()); headers.put(HttpHeaders.ACCEPT, "*/*"); ... val exec = HttpExecutionRequest.builder() .basicAuthPassword(metadata.getBasicAuthnPassword()) .basicAuthUsername(metadata.getBasicAuthnUsername()) .method(HttpMethod.GET) .url(metadataLocation) .headers(headers) .proxyUrl(service.getMetadataProxyLocation()) .build();

可以观察到几个实战相关细节:

  • Basic 认证:basicAuthnUsername/basicAuthnPassword用于对接需要认证的 MDQ 服务器;
  • 内容类型:请求携带Content-Type头,其值由supportedContentType配置控制;
  • ETag 条件请求:若本地存在上一轮拉取产生的备份文件,CAS 会读取文件属性中缓存的 ETag 并以If-None-Match头发出条件请求(headers.put("If-None-Match", etag)),支持 MDQ 服务器返回 304 以节省带宽。MDQ 解析器重写了shouldHttpResponseStatusBeProcessed直接返回true,意味着 304 等非 2xx 状态也会被显式处理,而不是按失败丢弃;
  • 代理支持:沿用注册服务的metadataProxyLocation。

3. 响应处理:写备份文件、失败回退

getMetadataResolverFromResponse定义了成功与失败两条路径:

  • 成功(2xx):将响应体写入本地备份文件,把响应头中的ETag保存为文件自定义属性(user:ETag),再用InMemoryResourceMetadataResolver从备份文件构建内存解析器供后续使用;
  • 失败(非 2xx):如果本地备份文件存在,则直接回退使用该备份文件构建解析器,保证 SSO 不因 MDQ 服务器瞬时故障而中断;若备份也不存在,则抛出SamlException("Unable to get entity from MDQ server and a backup file does not exist.");
  • 5xx 或无响应:在fetchMetadata中直接抛出UnauthorizedServiceException(日志记录Unable to fetch metadata from [...]),由上层UrlResourceMetadataResolver捕获并转换为SamlException。

另外,MDQ 场景下备份文件的命名与普通 URL 拉取不同:文件名前缀取的是service.getServiceId()的 SHA 摘要(见 UrlResourceMetadataResolver.java 中的getBackupMetadataFilenamePrefix),即同一个 SP 无论查询条件如何变化,备份都落在同一个稳定路径上,这正是 ETag 条件请求与故障回退能正常工作的前提。

cas.authn.saml-idp.metadata.mdq配置项说明

文档末尾引用的cas.authn.saml-idp.metadata.mdq属性组对应源码类 MDQSamlMetadataProperties.java,其字段与用途如下:

配置项类型默认值说明
cas.authn.saml-idp.metadata.mdq.basic-authn-usernameString无连接 MDQ 服务器时使用的 Basic 认证用户名
cas.authn.saml-idp.metadata.mdq.basic-authn-passwordString无连接 MDQ 服务器时使用的 Basic 认证密码
cas.authn.saml-idp.metadata.mdq.supported-content-typeStringtext/xml向 MDQ 服务器发起请求时携带的Content-Type值,源码注释标明默认支持text/xml

这些属性挂在 SamlIdPMetadataProperties.java 的mdq字段下,与core、http、fileSystem等其它元数据属性组并列,因此 MDQ 配置不影响其它元数据解析方式的行为。

MDQ 解析器在整体解析计划中的位置

MDQ 解析器不是孤立存在的,它是 CAS SAML 注册服务元数据解析链中的一环。在 SamlIdPMetadataConfiguration.java 中,CAS 注册了六个默认解析器 Bean,并通过@Order指定优先级:

解析器 Bean顺序适用场景
metadataQueryProtocolMetadataResolverHIGHEST_PRECEDENCE(最高)以/entities/{0}结尾的 MDQ 位置
jsonResourceMetadataResolver+1metadataLocation为 JSON
fileSystemResourceMetadataResolver+2文件系统路径
urlResourceMetadataResolver+3普通 HTTP(S) URL
classpathResourceMetadataResolver+4classpath 资源
groovyResourceMetadataResolver+5Groovy 脚本

这些解析器由defaultSamlRegisteredServiceMetadataResolutionPlanConfigurer依次注册进DefaultSamlRegisteredServiceMetadataResolutionPlan,再由带缓存的SamlRegisteredServiceDefaultCachingMetadataResolver统一调度(缓存加载逻辑见chainingMetadataResolverCacheLoader)。因此 MDQ 拉取到的元数据同样享受 CAS 的缓存策略,不会每次 SSO 都发起远程查询;SamlRegisteredServiceCacheKey在构造缓存键时也专门对 MDQ 配置做了区分处理(见 SamlRegisteredServiceCacheKey.java)。

需要注意的是,整套配置仅在启用了 SAML IdP 功能模块(CasFeatureModule.FeatureCatalog.SAMLIdentityProvider)时生效,且SamlIdPMetadataConfiguration类上带有@ConditionalOnFeatureEnabled条件注解;对应模块为cas-server-support-saml-idp及元数据解析扩展模块cas-server-support-saml-idp-metadata。

测试验证

仓库中 MetadataQueryProtocolMetadataResolverTests.java 对该解析器做了单元测试,验证了supports判定、占位符替换后的查询地址构造以及请求头组装等行为,可作为对接自有 MDQ 服务器时的预期行为参照。

小结

  • CAS 通过把注册服务的metadataLocation配置为以/entities/{0}结尾的 MDQ 地址来启用动态元数据查询,{0}在运行时被替换为 URL 编码的 entityID,支持逗号分隔多个地址;
  • 请求层面支持 Basic 认证(basic-authn-username/basic-authn-password)、可配置Content-Type(默认text/xml),并基于本地备份文件自动携带If-None-MatchETag 头;
  • 拉取成功后元数据落盘为备份文件;查询失败时可回退到备份文件,彻底失败才报错,兼顾了实时性与可用性;
  • MDQ 解析器在解析计划中优先级最高,但只“认领” MDQ 形式的位置,其余位置仍由 JSON/文件系统/URL 等解析器按序处理,整体接入对现有 SAML IdP 部署无侵入。
  • 后端
  • 认证鉴权
  • 单点登录

【免费下载链接】cas

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

项目地址:https://gitcode.com/gh_mirrors/ca/cas
点击查看免费下载
上一篇:408数据结构代码题终极攻略:5大核心算法模板快速掌握
下一篇:asc-devkit矩阵乘法最佳实践

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

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

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

立即咨询