- 后端
- 认证鉴权
- 单点登录
【免费下载链接】cas
Apereo CAS - Identity & Single Sign On for all earthlings and beyond.
本文围绕 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-username | String | 无 | 连接 MDQ 服务器时使用的 Basic 认证用户名 |
cas.authn.saml-idp.metadata.mdq.basic-authn-password | String | 无 | 连接 MDQ 服务器时使用的 Basic 认证密码 |
cas.authn.saml-idp.metadata.mdq.supported-content-type | String | text/xml | 向 MDQ 服务器发起请求时携带的Content-Type值,源码注释标明默认支持text/xml |
这些属性挂在 SamlIdPMetadataProperties.java 的mdq字段下,与core、http、fileSystem等其它元数据属性组并列,因此 MDQ 配置不影响其它元数据解析方式的行为。
MDQ 解析器在整体解析计划中的位置
MDQ 解析器不是孤立存在的,它是 CAS SAML 注册服务元数据解析链中的一环。在 SamlIdPMetadataConfiguration.java 中,CAS 注册了六个默认解析器 Bean,并通过@Order指定优先级:
| 解析器 Bean | 顺序 | 适用场景 |
|---|---|---|
metadataQueryProtocolMetadataResolver | HIGHEST_PRECEDENCE(最高) | 以/entities/{0}结尾的 MDQ 位置 |
jsonResourceMetadataResolver | +1 | metadataLocation为 JSON |
fileSystemResourceMetadataResolver | +2 | 文件系统路径 |
urlResourceMetadataResolver | +3 | 普通 HTTP(S) URL |
classpathResourceMetadataResolver | +4 | classpath 资源 |
groovyResourceMetadataResolver | +5 | Groovy 脚本 |
这些解析器由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.
相关推荐
Apereo CAS 配置 SAML2 Unsolicited(IdP-Initiated)SSO 详解
Apereo CAS 配置 SAML2 Unsolicited(IdP Initiated)SSO 详解 Apereo CAS 作为 SAML2 身份提供方(I
后端认证鉴权单点登录Apereo CAS 基于 Amazon S3 的 SAML2 动态元数据管理:SP 元数据解析与 IdP 元数据托管实战
Apereo CAS 基于 Amazon S3 的 SAML2 动态元数据管理:SP 元数据解析与 IdP 元数据托管实战 Apereo CAS 作为 SAML
后端认证鉴权单点登录Apereo CAS SAML2 Git 元数据管理:SP 与 IdP 元数据的版本控制实战
Apereo CAS SAML2 Git 元数据管理:SP 与 IdP 元数据的版本控制实战 本篇技术指南聚焦 Apereo CAS 中 SAML2 元数据的
后端认证鉴权单点登录
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考