- 后端
- 认证鉴权
- 单点登录
【免费下载链接】cas
Apereo CAS - Identity & Single Sign On for all earthlings and beyond.
导读
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 表达式遵循两条基本规则:
- 表达式必须封装在
${...}语法内。例如file://${#systemProperties['tier']}/file.json。 - 预定义变量必须以
#字符开头。例如#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值 |
randomNumber2 | 2 位随机数字 |
randomNumber4 | 4 位随机数字 |
randomNumber6 | 6 位随机数字 |
randomNumber8 | 8 位随机数字 |
randomString4 | 4 字符随机单词 |
randomString6 | 6 字符随机单词 |
randomString8 | 8 字符随机单词 |
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()) |
此外,从源码可以确认解析器还注册了文档未列出的两个动态变量,同样可直接使用:
| 变量 | 说明 | 源码依据 |
|---|---|---|
randomNumber12 | 12 位随机数字(注意源码实现当前调用的是randomNumeric(8),实际返回 8 位数字) | SpringExpressionLanguageValueResolver.java |
randomPort | 3000~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)方法采用两段替换:
- 先用
StringSubstitutor(parameters, "${", "}")完成普通参数(无#前缀)的直接字符串替换; - 再对以
#开头的变量执行 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.
相关推荐
Spring表达式语言SpEL源码深度解析
Spring表达式语言SpEL源码深度解析 本文深入解析Spring表达式语言 SpEL 的核心架构和实现原理。首先详细分析SpEL解析器的分层架构设计,包括接
示例工程文档深入解析Spring SpEL中的Expression接口
深入解析Spring SpEL中的Expression接口 概述 Spring表达式语言 SpEL 是Spring框架中一个功能强大的表达式语言,它能够在运行时
示例工程文档Apereo CAS 全局认证策略(Global Authentication Policy)配置与自定义扩展指南
Apereo CAS 全局认证策略(Global Authentication Policy)配置与自定义扩展指南 本文聚焦 Apereo CAS 中的 全局认
后端认证鉴权单点登录
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考