- 后端
- 认证鉴权
- 单点登录
【免费下载链接】cas
Apereo CAS - Identity & Single Sign On for all earthlings and beyond.
导读
本文讲解 Apereo CAS 中一种高度灵活的 MFA 触发方式:编写自定义 Groovy 脚本,根据服务请求、注册服务定义、认证上下文与请求对象等输入,动态决定激活哪个多因素认证(MFA)提供方。读完本文,你将掌握 Groovy MFA 触发器的脚本骨架、五个入参的含义、配置属性cas.authn.mfa.groovy-script.location的用法,以及底层触发器的调用链与自动化测试验证方式,从而把任意复杂的 MFA 判定规则(例如"特定应用 + 特定用户属性组合才触发 Duo")落地为一段可维护的 Groovy 脚本。
一、Groovy 触发器在 CAS MFA 触发体系中的位置
CAS 的 MFA 触发机制(Trigger Machinery)负责在认证流程中"寻找下一个事件",本身对具体 MFA 提供方完全无感知。除了 SAML2、OIDC 等协议内置的内部触发器外,CAS 支持十余种可配置的外部触发器,包括全局触发器、按应用(Per Application)触发器、全局 Principal 属性触发器、自适应(Adaptive)触发器、REST 触发器、Opt-In 请求参数触发器等。完整清单见 Configuring-Multifactor-Authentication-Triggers.md。
其中Groovy 触发器属于"按应用 + 脚本化"的类别:它不像 Per Application 触发器那样只做简单的serviceId匹配,而是把判定逻辑完全交给开发者自己的 Groovy 脚本,脚本可以综合服务、注册服务、认证上下文、HTTP 请求等多维度信息,最终返回一个 MFA 提供方 ID,CAS 据此激活对应流程。这种触发方式适合规则复杂、变化频繁、需要快速迭代而又不想反复改配置重编译的场景。
注意:与大多数 MFA 触发器一样,Groovy 触发器的实际生效通常要求原始认证请求携带
service参数;否则可能在首次认证成功后、后续带参请求中才被激活(详见 Configuring-Multifactor-Authentication-Triggers.md 中的 Service Requirement 说明)。测试触发器时请务必带上service参数。
二、前置条件:启用 Apache Groovy 脚本支持
Groovy 触发器依赖 CAS 的脚本执行引擎,因此首先必须把脚本模块引入 WAR Overlay:
org.apereo.cas:cas-server-core-scripting该模块的启用方式与要求详见 Apache-Groovy-Scripting.md。若未引入该模块,Groovy 运行库不会被拉入项目,相关脚本功能(包括 MFA 触发器)将在运行时失效。
此外,CAS 默认以动态方式(基于 Groovy 元对象协议)执行脚本,也支持通过系统属性切换为静态编译模式:
-Dorg.apereo.cas.groovy.compile.static=true在CompileStatic模式下,脚本会被做类型检查,动态语法(如无类型声明的属性访问)通常需要改写;本文示例以动态模式为准。
三、脚本骨架与五个入参
Groovy MFA 触发器脚本的规范结构如下(这是原文档给出的通用骨架):
import java.util.* class SampleGroovyEventResolver { def run(final Object... args) { def (service,registeredService,authentication,httpRequest,logger) = args ... return "mfa-duo" } }脚本类必须提供一个run(Object... args)方法,CAS 在每次触发评估时调用它,并把如下五个参数按固定顺序传入:
| 参数 | 说明 |
|---|---|
service | 表示请求中携带的 incoming service(若有),即Service对象,可通过.id取服务标识,如service.id == "https://www.example.com"。 |
registeredService | 表示服务注册表(Service Registry)中与该请求对应的服务定义对象,即RegisteredService。 |
authentication | 表示已建立的认证事件对象,其中包含 Principal,可通过authentication.principal.attributes访问用户属性。 |
httpRequest | 表示HttpServletRequest对象,可用于读取请求头、参数等。 |
logger | 日志对象,用于输出日志,如logger.info(...)。 |
返回值为字符串:返回的字符串即为 CAS 应当激活的 MFA 提供方 ID;返回null(或空)则表示本次不触发 MFA。
从源码看入参的实际传递方式
上述五个入参并非文档约定,而是由底层触发器实现直接构造的。在 GroovyScriptMultifactorAuthenticationTrigger.java 中,isActivated(...)方法把service、registeredService、authentication、httpServletRequest以及 CAS 自身的LOGGER打包成参数数组:
val args = new Object[]{service, registeredService, authentication, httpServletRequest, LOGGER}; val provider = this.watchableScript.execute(args, String.class); LOGGER.debug("Groovy script run for [{}] returned the provider id [{}]", registeredService, provider);然后处理脚本返回值:
- 若返回值为空白(
StringUtils.isBlank),返回Optional.empty(),表示不触发; - 若返回值等于
ChainingMultifactorAuthenticationProvider.DEFAULT_IDENTIFIER(即"mfa-composite",见 ChainingMultifactorAuthenticationProvider.java),则通过MultifactorAuthenticationProviderSelector从全部可用提供方中选择组合(composite)提供方,实现"多提供方链式触发"; - 其余情况按返回的 ID 在应用上下文中解析对应的 MFA 提供方,解析不到时相应异常会被抛出。
同时,触发器的order默认为Ordered.LOWEST_PRECEDENCE,即 Groovy 触发器在触发链中处于较后位置;当应用上下文中没有任何可用 MFA 提供方时,会直接抛出AuthenticationException。
四、完整示例:按应用 + 用户属性触发 Duo 认证
原文档给出了一个可直接落地的示例脚本:当请求应用是https://www.example.com,且已认证的 Principal 的mail属性值包含email@example.org时,触发 Duo Security MFA(mfa-duo):
import java.util.* class MyExampleScript { String run(final Object... args) { def (service,registeredService,authentication,httpRequest,logger) = args if (service.id == "https://www.example.com") { logger.info("Evaluating principal attributes [{}]", authentication.principal.attributes) def mail = authentication.principal.attributes['mail'] if (mail.contains("email@example.org")) { logger.info("Found mail attribute with value [{}]", mail) return "mfa-duo" } } return null } }要点解析:
service.id是请求服务标识,与示例中的应用 URL 精确匹配;注意RegisteredService定义通常用正则匹配,而这里是比较具体 ID;authentication.principal.attributes['mail']取的是 Principal 属性中的mail,返回结果一般是List/Collection结构,因此直接用contains(...)判断是否包含目标邮箱;- 最后
return null是关键兜底分支——只要不满足条件就明确返回null,避免脚本因隐式返回值(例如最后一个logger.info(...)的返回值)意外触发 MFA; - Duo Security MFA 提供方 ID
mfa-duo的完整配置见 DuoSecurity-Authentication.md。
除了返回单个提供方 ID,脚本也可以返回mfa-composite(即ChainingMultifactorAuthenticationProvider.DEFAULT_IDENTIFIER)来触发多提供方组合流程,这一能力同样有自动化测试覆盖(见下文第六节verifyCompositeProvider)。
五、配置属性:指定 Groovy 脚本位置
脚本编写完成后,通过如下配置属性指定其资源位置,即可启用 Groovy MFA 触发器:
cas.authn.mfa.groovy-script.location=file:/etc/cas/config/GroovyMfaTrigger.groovy- 属性前缀:
cas.authn.mfa.groovy-script; - 值类型:
SpringResourceProperties.location,即一个 SpringResource,可以是文件路径、classpath:资源或 URL;location为必填属性(@RequiredProperty),定义见 SpringResourceProperties.java,对应配置模型类中的MultifactorAuthenticationProperties.groovyScript(见 MultifactorAuthenticationProperties.java); - 官方测试即采用
classpath:形式,例如cas.authn.mfa.groovy-script.location=classpath:/GroovyMfaTrigger.groovy(见 BaseMultifactorAuthenticationTriggerTests.java); - 若脚本资源被设置为随变更自动重载(CAS 默认会 watch 资源变化),在 Linux 上可能需要调大 inotify 实例上限:向
/etc/sysctl.conf添加fs.inotify.max_user_instances = 256,并用cat /proc/sys/fs/inotify/max_user_instances检查当前值;如要禁用资源 watcher,可设置系统属性org.apereo.cas.util.io.PathWatcherService=false(详见 SpringResourceProperties.java)。
配置如何驱动 Bean 装配
该配置并非"魔法生效",而是由自动装配逻辑显式条件化加载的。在 CasCoreMultifactorAuthenticationWebflowAutoConfiguration.java 中:
groovyScriptMultifactorAuthenticationTriggerBean 仅在cas.authn.mfa.groovy-script.location属性存在且脚本工厂(ExecutableCompiledScriptFactory)可用时才会创建(BeanCondition.on("cas.authn.mfa.groovy-script.location").exists());- Bean 上标注
@RefreshScope,脚本运行时通过scriptFactory.fromResource(groovyScript)包装为可执行的watchableScript,因此支持配置/资源热刷新; - 该 Trigger 随后被注入
groovyScriptAuthenticationPolicyWebflowEventResolver,并作为 delegate 挂载到主 Webflow 事件解析链中(同文件 L119-L130、L512-L534)。
这解释了为什么"只要配置了脚本位置,触发器就会自动生效"——缺配置时该 Bean 不会创建,触发链自动跳过 Groovy 评估。
六、源码级行为验证:自动化测试如何覆盖触发器
仓库中 Groovy 触发器的行为有完整测试佐证,可作为理解其语义的权威参考。
1. 触发器单元测试
GroovyScriptMultifactorAuthenticationTriggerTests.java 覆盖以下行为:
verifyOperationByProvider:脚本返回某提供方 ID 时isActivated结果为 present;当测试服务为nomfa时脚本返回null,结果为 absent;verifyCompositeProvider:当脚本返回mfa-composite(ChainingMultifactorAuthenticationProvider.DEFAULT_IDENTIFIER)时,能够正确解析出复合提供方;verifyBadInputParameters:authentication为null时不触发;registeredService或service为null时仍可正常触发(脚本逻辑决定);verifyNoProvider:应用上下文中无任何 MFA 提供方时,抛出AuthenticationException。
测试所用的真实脚本位于 GroovyMfaTrigger.groovy,展示了不依赖类定义的函数式脚本写法,以及service.id判断 +null返回的实践:
def run(final Object... args) { def service = args[0] as WebApplicationService def registeredService = args[1] def authentication = args[2] def httpRequest = args[3] def logger = args[4] if ("nomfa".equalsIgnoreCase(service?.id)) { return null } if ("composite".equalsIgnoreCase(service?.id)) { return ChainingMultifactorAuthenticationProvider.DEFAULT_IDENTIFIER } return TestMultifactorAuthenticationProvider.ID }2. Webflow 事件解析器测试
GroovyScriptMultifactorAuthenticationPolicyEventResolverTests.java 通过cas.authn.mfa.groovy-script.location=classpath:GroovyMfaResolver.groovy注入脚本,验证 Groovy 触发器产生的 MFA 事件能正确汇入 Webflow 认证事件解析链;配套脚本 GroovyMfaResolver.groovy 展示了用logger.info("Testing MFA")记录评估日志并返回"mfa-dummy"的写法。
七、脚本编写最佳实践与注意事项
结合原文档示例与源码实现,总结如下实践建议:
- 始终显式返回:所有不满足触发的分支都要
return null,避免依赖 Groovy 隐式返回最后一个表达式的值; - 日志辅助排障:充分利用传入的
logger打印评估上下文(如authentication.principal.attributes),并配合触发器源码中的LOGGER.debug(...)输出(记录返回的 provider id)进行问题定位; - 先判空再取值:
service、registeredService可能为null(测试verifyBadInputParameters已验证该场景),访问service.id前建议判空或使用service?.id; - 返回值语义:返回单个提供方 ID(如
mfa-duo)触发单个 MFA;返回mfa-composite触发多提供方组合;返回null/空白不触发; - 与服务注册配合:
registeredService参数携带了服务注册表中的完整定义,可据此在脚本中按服务策略做更细粒度的分支; - 热更新:借助
@RefreshScope与资源 watcher,脚本变更可动态生效,但要注意 inotify 实例上限(见第五节)与脚本语法在静态编译模式下的兼容性。
八、总结
Groovy MFA 触发器是 CAS 多因素认证触发体系中灵活性最高的一类方案:只需编写一个run(Object... args)脚本并配置cas.authn.mfa.groovy-script.location,即可把"何时触发、触发哪个提供方"的判定完全掌握在自己手中。其底层由 GroovyScriptMultifactorAuthenticationTrigger 驱动、由 CasCoreMultifactorAuthenticationWebflowAutoConfiguration 条件化装配,并有单元测试与 Webflow 集成测试双重验证,可作为生产环境"按应用 + 按用户属性组合触发 MFA"的可靠实现路径。相关文档与源码均在仓库内可继续深入研读:触发器总览见 Configuring-Multifactor-Authentication-Triggers.md,Groovy 基础能力见 Apache-Groovy-Scripting.md,Duo 提供方配置见 DuoSecurity-Authentication.md。
- 后端
- 认证鉴权
- 单点登录
【免费下载链接】cas
Apereo CAS - Identity & Single Sign On for all earthlings and beyond.
相关推荐
Apereo CAS GeoTracking 认证:使用 Groovy 脚本自定义 IP 地理位置解析
Apereo CAS GeoTracking 认证:使用 Groovy 脚本自定义 IP 地理位置解析 Apereo CAS 的 GeoTracking(地理定
后端认证鉴权单点登录Apereo CAS 密码无认证(Passwordless)与多因素认证(MFA)集成:让密码无认证流程无缝让位给 MFA
Apereo CAS 密码无认证(Passwordless)与多因素认证(MFA)集成:让密码无认证流程无缝让位给 MFA 本文基于 Apereo CAS 官方
后端认证鉴权单点登录Apereo CAS 基于 Groovy 脚本的灵活认证(Groovy Authentication)实战指南
Apereo CAS 基于 Groovy 脚本的灵活认证(Groovy Authentication)实战指南 导读 本文介绍 Apereo CAS 中一种高度
后端认证鉴权单点登录
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考