☰
Apereo CAS Spring Expression Language(SpEL)配置指南:${...} 表达式、预定义变量与安全解析机制
2026/9/25 7:11:44 网站建设 项目流程
  • 后端
  • 认证鉴权
  • 单点登录

【免费下载链接】cas

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

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

导读

Apereo CAS 的许多内部组件允许在配置值中使用 Spring Expression Language(SpEL)语法,以便在运行时动态获取系统属性、环境变量、随机数、时间日期等值,让静态配置文件具备“编程式”的动态能力。本文以 Configuration-Spring-Expressions.md 为骨架,结合仓库源码深入讲解表达式封装规则、全部预定义变量、典型用法与底层安全解析实现,读完即可在 CAS 的注册服务策略、属性释放、通知模板等配置中熟练编写并验证 SpEL 表达式。

为什么 CAS 需要 Spring Expression Language

CAS 的配置体系虽然以静态属性文件为主,但不少组件希望访问系统变量、环境属性,或者需要更动态、更程序化的策略才能正常工作。例如:根据系统属性拼装外部服务 URL、为随机测试数据生成随机端口、按时间窗口控制服务访问、在通知模板中注入当前时间等。

为此,CAS 从 6.2.0 起引入了专门的解析器SpringExpressionLanguageValueResolver,其核心实现位于 SpringExpressionLanguageValueResolver.java。该解析器在底层使用 Spring 的 SpEL 引擎(SpelExpressionParser+StandardEvaluationContext)与 Apache Commons Text 的StringSubstitutor完成${...}模板替换,使任意配置字符串都可以内嵌表达式求值。

表达式语法与封装规则

CAS 中的 SpEL 表达式遵循两条基本规则:

  1. 表达式必须封装在${...}语法内。例如file://${#systemProperties['tier']}/file.json。
  2. 预定义变量必须以#字符开头。例如#uuid、#randomString6。

从源码看,解析器通过TemplateParserContext("${", "}")定义模板边界,凡是命中${...}的片段都会交给 SpEL 求值;求值结果会替换原占位符。SpringExpressionLanguageValueResolver.hasExpressionMarkers()还会检测字符串中是否包含${或#{,用于判断一个值是否需要走表达式解析路径。

预定义变量一览

CAS 为表达式上下文预置了一批变量,全部由SpringExpressionLanguageValueResolver的构造方法与initializeDynamicVariables()方法注册。下表完整覆盖官方文档列出的变量:

变量说明
systemProperties系统属性(System.getProperties())映射,通常在启动时加载一次
sysProps同systemProperties
environmentVars环境变量(System.getenv())映射,通常在启动时加载一次
environmentVariables同environmentVars
envVars同environmentVars
env同environmentVars
tempDir临时目录路径(取自FileUtils.getTempDirectoryPath())
uuid自动生成的UUID值
randomNumber22 位随机数字
randomNumber44 位随机数字
randomNumber66 位随机数字
randomNumber88 位随机数字
randomString44 字符随机单词
randomString66 字符随机单词
randomString88 字符随机单词
localDateTime使用系统默认时区的当前日期/时间
localDateTimeUtc使用UTC的当前日期/时间
localDate使用系统默认时区的当前日期
localDateUtc使用UTC的当前日期
zonedDateTime使用系统默认时区的当前带时区日期时间
zonedDateTimeUtc使用UTC的当前带时区日期时间
localStartWorkDay当前工作日的开始时间,即当天8am
localEndWorkDay当前工作日的结束时间,即当天5pm(源码实现为起始时间 +9 小时)
localStartDay当天零点(一天开始)
localEndDay当天23:59:59(一天结束,源码取23时59分59秒)
zoneId系统默认时区 ID(ZoneId.systemDefault().getId())

此外,从源码可以确认解析器还注册了文档未列出的两个动态变量,同样可直接使用:

变量说明源码依据
randomNumber1212 位随机数字(注意源码实现当前调用的是randomNumeric(8),实际返回 8 位数字)SpringExpressionLanguageValueResolver.java
randomPort3000~9999 范围内的随机端口号SpringExpressionLanguageValueResolver.java

当解析器绑定到 SpringApplicationContext后,还会额外注册applicationContext变量(一个Supplier<ApplicationContext>),供需要访问 Bean 容器的表达式使用,参见withApplicationContext方法。

典型使用示例

官方文档给出的示例在仓库中均有对应实现与测试佐证:

  • 假设系统属性tier的值为production,配置值file://${#systemProperties['tier']}/file.json将解析为file://production/file.json。
  • 假设环境变量tier的值为qa,配置值file://${#environmentVariables['tier']}/file.json将解析为file://qa/file.json。
  • 使用${#randomString6}将得到一个 6 字符随机单词,例如qemguz。
  • 使用${#randomNumber8}将得到一个 8 位随机数字,例如75915283。
  • 使用${T(java.util.Locale).GERMAN.getLanguage()}将解析为de(德语语言代码)。

其中T(...)类型引用的解析依赖解析器内置的类型白名单机制(AllowListTypeLocator),默认仅允许java.util.Locale,并限定其getLanguage()方法可被调用。

在 SpringExpressionLanguageValueResolverTests.java 中,可以找到上述全部示例的单元测试断言,包括:

  • resolver.resolve("${'Hello World'}")返回Hello World;
  • 设置系统属性cas.user后,${#systemProperties['cas.user']}返回Apereo CAS;
  • ${#environmentVars['HOME']}可读取宿主机HOME环境变量;
  • file://${#systemProperties['cas.dir']}/file.json在cas.dir=etc/cas/config时解析为file://etc/cas/config/file.json;
  • 各类随机数、随机字符串、时间日期变量均能返回非空结果。

这些测试印证了官方文档的每一项用法在实际代码中均可直接复现。

源码级实现原理

单例解析器与上下文初始化

SpringExpressionLanguageValueResolver以单例形式暴露(getInstance()),构造时完成静态变量的注册:

  • 系统属性:System.getProperties()同时注册为systemProperties与sysProps;
  • 环境变量:System.getenv()同时注册为environmentVars、environmentVariables、envVars、env;
  • 临时目录与系统时区:tempDir、zoneId。

动态变量(随机数、UUID、时间日期等)并非在构造时一次性固定,而是在每次getInstance()调用时通过initializeDynamicVariables()重新生成,因此每次解析都会拿到“新鲜”的随机值与时间值。

模板替换的两段式处理

format(contents, parameters)方法采用两段替换:

  1. 先用StringSubstitutor(parameters, "${", "}")完成普通参数(无#前缀)的直接字符串替换;
  2. 再对以#开头的变量执行 SpEL 求值替换,且允许从传入的parameters映射中补充查找变量(lookupVariable覆写逻辑)。

这使解析器既能处理静态占位符,也能处理完整的 SpEL 表达式,两种写法可混用在同一字符串中。

安全约束(SpEL 沙箱化)

CAS 对 SpEL 求值环境做了严格的“降权”处理,防止表达式注入执行任意 Java 代码:

  • 类型定位白名单:AllowListTypeLocator只允许解析java.util.Locale等显式登记的类型,其他类型一律抛出SpelEvaluationException(TYPE_NOT_FOUND);
  • 方法解析白名单:configureContext中为每个允许类型注册独立的ReflectiveMethodResolver,仅当目标类型命中白名单时才放行方法调用,否则抛出AccessException;
  • 禁用 Bean 解析:setBeanResolver(null);
  • 禁用构造器调用:setConstructorResolvers(List.of());
  • 只读属性访问:属性访问器仅保留DataBindingPropertyAccessor.forReadOnlyAccess(),表达式只能读不能写。

正因如此,测试中resolver.resolve("${'Hello World'.concat('!')}")会直接抛出SpelEvaluationException——任意方法调用(除白名单外)是不被允许的,这也是官方文档特意只给出T(java.util.Locale).GERMAN.getLanguage()这一类受限示例的原因。

在 CAS 中的实际使用场景

解析器被大量 CAS 组件复用,配置者可在以下典型位置使用${...}表达式:

  • 注册服务访问策略与用户名解析:如 StaticRegisteredServiceUsernameProvider.java、TimeBasedRegisteredServiceAccessStrategy.java、GroovyRegisteredServiceAccessStrategy.java 等都会调用SpringExpressionLanguageValueResolver.getInstance()求值;
  • 属性释放策略:如 ReturnEnvironmentAttributeReleasePolicy.java、ReturnStaticAttributeReleasePolicy.java 支持用表达式读取环境变量、随机数等生成返回给客户端的属性值;
  • 通知消息模板:邮件与短信正文构建器(如 EmailMessageBodyBuilder.java、SmsBodyBuilder)允许在模板中嵌入时间日期、UUID 等动态内容;
  • 认证适配与 MFA 触发:IP 地址智能检测(如BlackDotIPAddressIntelligenceService)、基于脚本的 MFA 触发器(ScriptedRegisteredServiceMultifactorAuthenticationTrigger)等也会借助表达式求值完成运行时决策。

以官方文档的经典写法为例,在注册服务的配置片段中可这样使用:

{ "@class": "org.apereo.cas.services.CasRegisteredService", "serviceId": "https://app.example.org/.*", "name": "Example", "usernameAttributeProvider": { "@class": "org.apereo.cas.services.DefaultRegisteredServiceUsernameProvider", "canonicalizeUsername": false }, "properties": { "@class": "java.util.HashMap", "serviceTier": { "@class": "org.apereo.cas.services.DefaultRegisteredServiceProperty", "values": [ "java.util.HashSet", [ "${#systemProperties['tier']}" ] ] } } }

当 CAS 以-Dtier=production启动时,该属性值会被动态解析为production;若改用环境变量注入,则可写成${#environmentVariables['tier']}。

使用注意事项

  • 表达式仅出现在${...}内、变量以#开头:不含这些标记的普通字符串原样返回,不会触发解析;
  • 随机数与时间变量每次解析都会重新生成,适合用于一次性占位(如临时文件名、会话标记),不要假设它们跨解析保持稳定;
  • 方法调用与类型引用受限:除白名单类型外,无法调用任意 Java 方法或构造对象,这是 CAS 有意为之的安全边界;
  • 系统属性/环境变量映射在启动时加载:运行中修改系统属性通常不会反映到已缓存的 Map 中(不过测试中通过System.setProperty后解析仍能读到值,因为解析的是同一 JVM 的System.getProperties()引用);
  • randomNumber12变量名保留但当前实现返回 8 位数字:从源码结构看属于已知的实现细节,若依赖 12 位长度请改用randomNumber8或自行拼接表达式。

验证方法

不需要额外搭建运行环境即可验证上述行为:直接运行仓库中的单元测试类 SpringExpressionLanguageValueResolverTests.java,它会断言官方文档列出的全部示例结果,包括系统属性、环境变量、随机值、时间日期以及受限类型T(java.util.Locale)的解析行为。这也是在修改或扩展自己的表达式配置前最快速的自检手段。

  • 后端
  • 认证鉴权
  • 单点登录

【免费下载链接】cas

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

项目地址:https://gitcode.com/gh_mirrors/ca/cas
点击查看免费下载
上一篇:generative-ai-for-beginners 第 5 课:高级提示词工程实战 —— 从零样本到思维链的七种技术及输出控制
下一篇:数据结构实战:gh_mirrors/blog37/blog中的栈、队列与二叉搜索树实现

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

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

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

立即咨询