Appearance
CAS AutoConfiguration 覆盖机制与 Bean 注册模式:Spring Boot 3.x 时代深度定制 CAS 的核心技巧
作者: 必码 | bima.cc
前言
在企业级单点登录(SSO)基础设施的建设中,Apereo CAS(Central Authentication Service)凭借其丰富的协议支持、灵活的认证架构和成熟的社区生态,一直是大型组织构建统一认证平台的首选开源方案。然而,CAS 作为一个拥有超过二十年历史的"重量级"框架,其内部包含了数以千计的 Spring Bean 定义和错综复杂的自动配置逻辑。对于实际落地项目的开发者而言,如何在不修改 CAS 源码的前提下,精准地覆盖默认行为、注入自定义组件,始终是 CAS 定制化开发中最核心、也最具挑战性的课题。
CAS 的定制化需求可以粗略分为三个层次:第一层是配置级别的调整,通过 application.properties 或 application.yml 修改 CAS 的运行时参数;第二层是 Bean 级别的覆盖,通过 Spring 的依赖注入机制替换 CAS 内部的默认实现;第三层是架构级别的扩展,通过自定义认证处理器、票据组件、协议适配器等深度集成企业自有系统。在这三个层次中,Bean 级别的覆盖是承上启下的关键环节——它既是配置调整的自然延伸,也是架构扩展的必要前提。而 Bean 注册与覆盖的核心机制,正是 Spring Boot 的 AutoConfiguration 体系。
本文基于我们团队实际维护的 CAS Overlay 项目(基于 CAS 7.3.4 版本),系统性地梳理 CAS 从 5.3.x 到 6.6.x 再到 7.3.x 三个大版本中 Bean 注册机制的演进脉络,深入解析 Spring Boot 3.x 时代 AutoConfiguration 机制的核心变化,并给出在 CAS 7.3 中进行 Bean 覆盖和自定义认证处理器注册的最佳实践。所有代码示例均经过教学化处理,旨在展示核心设计思路和模式,而非提供可直接复制的模板。
为什么 Bean 覆盖机制如此重要? 在实际的企业 CAS 部署中,几乎不存在"开箱即用"的场景。每个企业都有自己独特的用户存储(关系型数据库、LDAP 目录、Active Directory、NoSQL 数据库等)、独特的认证策略(多因素认证、风险自适应认证、条件访问策略等)和独特的集成需求(与 HR 系统、OA 系统、云平台的无缝对接)。这些定制需求最终都要通过 Bean 覆盖和扩展来实现。可以说,掌握了 CAS 的 Bean 覆盖机制,就掌握了 CAS 定制化开发的核心钥匙。
本文的技术定位: 本文不是 CAS 的入门教程,也不是官方文档的简单翻译。我们假设读者已经具备 Spring Boot 和 CAS 的基础知识,本文的重点是揭示 CAS 内部 Bean 注册与覆盖的底层机制,帮助读者建立系统性的认知框架。我们将从源码层面分析 CAS 的自动配置类是如何组织和加载的,@ConditionalOnMissingBean 的条件判断时机是如何确定的,以及 @Primary 和 ObjectProvider 是如何协同工作的。
本文的读者受众包括:
- 正在评估或实施 CAS Overlay 项目的架构师和高级开发工程师,需要理解 CAS 的 Bean 管理机制以制定技术方案
- 需要将现有 CAS 系统从 5.3/6.6 升级到 7.3 的基础架构团队,需要了解三代注册机制之间的差异和迁移路径
- 对 Spring Boot AutoConfiguration 机制感兴趣,希望通过 CAS 这一大型框架加深理解的技术爱好者
- 负责企业级 SSO 平台运维,需要理解 CAS 内部机制以快速排查认证链路问题的运维工程师
- 正在进行 CAS 二次开发,需要精确控制 Bean 生命周期和依赖关系的后端开发人员
阅读建议: 本文各章节之间存在一定的依赖关系。如果你是 CAS 的新手,建议按顺序阅读;如果你已经熟悉 CAS 的基本架构,可以根据需要跳转到感兴趣的章节。第二章(Spring Boot 3.x AutoConfiguration 机制)是理解后续章节的基础,建议所有读者都仔细阅读。
第一章 CAS Bean 注册机制演进全景
CAS 的 Bean 注册机制并非一成不变。随着 Spring Boot 生态的演进和 CAS 自身架构的升级,Bean 注册方式经历了三代根本性的变革。理解这一演进脉络,不仅有助于把握 CAS 的技术发展方向,更是制定版本升级策略和编写可维护定制代码的基础。
1.1 CAS 5.3:XML + Groovy + @Component 混合时代
CAS 5.3.x 发布于 2018-2019 年间,基于 Spring Boot 1.5.x 和 Spring Framework 4.3.x。这个时代的 CAS 仍然保留了大量从早期 XML 配置时代遗留下来的痕迹,Bean 注册机制呈现出明显的"过渡期"特征。
三种注册方式并存:
在 CAS 5.3 中,Bean 的注册可以通过以下三种方式实现:
┌─────────────────────────────────────────────────────────────┐
│ CAS 5.3 Bean 注册方式 │
├─────────────────┬───────────────────────────────────────────┤
│ XML 配置 │ 通过 Spring XML 文件定义 Bean │
│ (遗留方式) │ 主要存在于 cas-server-support-xxx 模块 │
├─────────────────┼───────────────────────────────────────────┤
│ Groovy 脚本 │ 通过 Groovy 脚本动态注册 Bean │
│ (灵活方式) │ 支持运行时热加载 │
├─────────────────┼───────────────────────────────────────────┤
│ @Component │ 通过注解扫描自动注册 │
│ (现代方式) │ Overlay 项目主要使用此方式 │
└─────────────────┴───────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
在 Overlay 项目中,最常用的方式是 @Component 注解配合 Spring 的组件扫描机制。CAS 5.3 的 Overlay 项目会在 src/main/java 目录下创建自定义组件,并通过 @Component、@Service 等注解将其注册到 Spring 容器中。
java
// CAS 5.3 时代的认证处理器注册方式
@Component("myCustomAuthenticationHandler")
public class MyCustomAuthenticationHandler
extends AbstractPreAndPostProcessingAuthenticationHandler {
@Autowired
@Qualifier("authenticationEventExecutionPlan")
private AuthenticationEventExecutionPlan plan;
@PostConstruct
public void registerHandler() {
plan.registerAuthenticationHandler(this);
}
@Override
public AuthenticationHandlerExecutionResult authenticate(
Credential credential) throws GeneralSecurityException {
// 自定义认证逻辑
UsernamePasswordCredential upc = (UsernamePasswordCredential) credential;
// ... 认证处理 ...
return new DefaultAuthenticationHandlerExecutionResult(
this, new DefaultHandlerResult(upc));
}
@Override
public boolean supports(Credential credential) {
return credential instanceof UsernamePasswordCredential;
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
CAS 5.3 注册方式的特点:
- 直接且简单:开发者只需添加
@Component注解,Spring 的组件扫描机制会自动发现并注册 Bean。这种方式的学习曲线最低,适合 CAS 入门项目。 - 隐式耦合:通过
@PostConstruct方法直接操作AuthenticationEventExecutionPlan,与 CAS 内部 API 存在隐式耦合。一旦 CAS 内部重构了AuthenticationEventExecutionPlan的接口,自定义代码可能需要同步修改。 - 缺乏条件控制:没有
@ConditionalOnMissingBean等条件注解的保护,容易产生 Bean 冲突。如果 classpath 中存在多个同名的@Component,Spring 容器启动时会抛出BeanDefinitionOverrideException。 - 静态绑定:Bean 在容器启动时一次性注册,不支持运行时动态调整。修改认证策略需要重启整个 CAS 服务,在高可用场景下这是不可接受的。
- 生命周期不可控:
@PostConstruct的执行时机取决于 Bean 的创建顺序,而 Bean 的创建顺序又受到依赖关系、@DependsOn注解、@Order注解等多种因素的影响,难以精确控制。
CAS 5.3 中的 XML 遗留配置:
尽管 CAS 5.3 已经大量采用注解驱动的配置方式,但在一些核心模块中仍然保留了 XML 配置文件。这些 XML 文件通常位于 cas-server-core 等模块的 src/main/resources 目录下,用于定义一些需要复杂初始化逻辑的 Bean。
xml
<!-- CAS 5.3 内部的 XML 配置示例(简化) -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="authenticationEventExecutionPlan"
class="org.apereo.cas.authentication.DefaultAuthenticationEventExecutionPlan"
init-method="initialize" />
<bean id="acceptUsersAuthenticationHandler"
class="org.apereo.cas.authentication.AcceptUsersAuthenticationHandler"
p:users-ref="acceptUsersMap" />
<util:map id="acceptUsersMap">
<entry key="admin" value="password" />
</util:map>
</beans>1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
这种 XML 配置与注解配置并存的状态,使得 CAS 5.3 的 Bean 管理机制变得复杂。开发者需要同时理解两种配置方式,并且在排查 Bean 冲突问题时需要检查两个来源。
CAS 5.3 中的 Groovy 动态配置:
CAS 5.3 还支持通过 Groovy 脚本动态注册 Bean。这种方式在需要运行时动态调整认证策略的场景下非常有用,但也带来了额外的复杂性和安全风险。
groovy
// CAS 5.3 Groovy 配置示例(简化)
import org.apereo.cas.authentication.AuthenticationHandler
import org.apereo.cas.authentication.AcceptUsersAuthenticationHandler
def handler = new AcceptUsersAuthenticationHandler()
handler.setName("groovyAcceptUsersHandler")
handler.setUsers([
"admin": "password",
"operator": "operator123"
])
// 动态注册到 Spring 容器
springContext.registerBean("groovyAcceptUsersHandler",
AuthenticationHandler.class, handler)1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
Groovy 配置的灵活性是以牺牲类型安全和启动性能为代价的。在 CAS 7.3 中,Groovy 配置方式虽然仍然可用,但已经不再是推荐的做法,取而代之的是 @RefreshScope + Actuator 的动态刷新机制。
1.2 CAS 6.6:@Configuration + @ConditionalOnMissingBean
CAS 6.6.x 发布于 2021-2022 年间,基于 Spring Boot 2.7.x 和 Spring Framework 5.3.x。这一版本标志着 CAS 全面拥抱 Spring Boot 的现代自动配置体系,XML 配置和 Groovy 脚本虽然仍然可用,但已不再是推荐的方式。
核心变化:引入 @Configuration 类和条件注解
CAS 6.6 的 Overlay 项目推荐使用 @Configuration 类来组织自定义 Bean 定义,并通过 @ConditionalOnMissingBean 注解实现与 CAS 内部默认配置的优雅共存。
java
// CAS 6.6 时代的认证处理器注册方式
@Configuration(proxyBeanMethods = false)
public class MyCustomAuthenticationConfiguration {
@Bean
@RefreshScope
@ConditionalOnMissingBean(name = "myCustomAuthenticationHandler")
public AuthenticationHandler myCustomAuthenticationHandler(
ConfigurableApplicationContext context,
CasConfigurationProperties casProperties) {
// 从 CAS 配置中读取自定义参数
var myProps = casProperties.getCustom().getProperties();
return new MyCustomAuthenticationHandler(myProps);
}
@Bean
@RefreshScope
@ConditionalOnMissingBean(name = "myAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
myAuthenticationEventExecutionPlanConfigurer(
@Qualifier("myCustomAuthenticationHandler")
AuthenticationHandler handler,
@Qualifier("acceptUsersAuthenticationHandler")
AuthenticationHandler acceptUsersHandler,
PrincipalResolver resolver) {
return plan -> {
// 注册自定义认证处理器
plan.registerAuthenticationHandler(handler);
plan.registerAuthenticationHandlerWithPrincipalResolver(
handler, resolver);
// 可选:保留 CAS 默认的静态用户认证
// plan.registerAuthenticationHandlerWithPrincipalResolver(
// acceptUsersHandler, resolver);
};
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
CAS 6.6 注册方式的特点:
- 显式声明:通过
@Configuration类集中管理 Bean 定义,结构清晰 - 条件保护:
@ConditionalOnMissingBean确保自定义 Bean 不会与 CAS 内部默认 Bean 冲突 - 刷新支持:
@RefreshScope注解使得认证处理器支持运行时动态刷新 - Lambda 简化:
AuthenticationEventExecutionPlanConfigurer使用函数式接口,注册代码更加简洁
自动配置注册方式:
CAS 6.6 仍然使用 spring.factories 文件来注册自动配置类:
# src/main/resources/META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.cas.config.MyCustomAuthenticationConfiguration1
2
3
2
3
1.3 CAS 7.3:@AutoConfiguration + imports 文件
CAS 7.3.x 发布于 2024-2025 年间,基于 Spring Boot 3.5.x 和 Spring Framework 6.x。这一版本在 Bean 注册机制上进行了最具革命性的变化——全面采用 Spring Boot 3.x 的 @AutoConfiguration 注解和 imports 文件机制。
核心变化概览:
┌──────────────────────────────────────────────────────────────────┐
│ CAS 7.3 AutoConfiguration 体系 │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ META-INF/spring/ │ │
│ │ org.springframework.boot.autoconfigure │ │
│ │ .AutoConfiguration.imports │ │
│ │ │ │
│ │ com.example.cas.config.\ │ │
│ │ CasOverlayOverrideConfiguration │ │
│ └──────────────────────┬─────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ @AutoConfiguration │ │
│ │ public class CasOverlayOverrideConfiguration { │ │
│ │ │ │
│ │ @Bean @Primary │ │
│ │ @ConditionalOnMissingBean │ │
│ │ public AuthenticationHandler customHandler() { ... } │ │
│ │ │ │
│ │ @Bean @Primary │ │
│ │ public AuthenticationEventExecutionPlanConfigurer │ │
│ │ customPlanConfigurer() { ... } │ │
│ │ } │ │
│ └────────────────────────────────────────────────────────────┘ │
└──────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
CAS 7.3 的典型配置类:
java
// CAS 7.3 时代的认证处理器注册方式
package com.example.cas.config;
import org.apereo.cas.config.CasOverlayOverrideConfiguration;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
import org.springframework.beans.factory.ObjectProvider;
@AutoConfiguration
public class CasOverlayOverrideConfiguration {
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(name = "myCustomAuthenticationHandler")
public AuthenticationHandler myCustomAuthenticationHandler(
CasConfigurationProperties casProperties) {
var myProps = casProperties.getCustom().getProperties();
return new MyCustomAuthenticationHandler(myProps);
}
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(
name = "myAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
myAuthenticationEventExecutionPlanConfigurer(
ObjectProvider<AuthenticationHandler> handlers,
ObjectProvider<PrincipalResolver> resolvers) {
return plan -> {
// 清除所有现有认证处理器
plan.getAuthenticationHandlers().clear();
// 只注册自定义的认证处理器
handlers.orderedStream()
.forEach(handler -> {
plan.registerAuthenticationHandler(handler);
plan.registerAuthenticationHandlerWithPrincipalResolver(
handler,
resolvers.getIfAvailable(
() -> new DefaultPrincipalResolver()));
});
};
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
对应的 imports 文件:
# src/main/resources/META-INF/spring/
# org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.example.cas.config.CasOverlayOverrideConfiguration1
2
3
2
3
CAS 7.3 注册方式的特点:
- 标准化:使用 Spring Boot 3.x 标准的
@AutoConfiguration注解,与 Spring 生态完全对齐 - 延迟注入:通过
ObjectProvider解决循环依赖问题,提高启动稳定性 - 优先级控制:
@Primary注解确保自定义 Bean 在存在多个候选者时被优先选择 - 全面刷新:
@RefreshScope与ObjectProvider结合,支持更深层次的运行时动态刷新
1.4 三代机制对比
下表从多个维度对 CAS 三个版本的 Bean 注册机制进行了全面对比:
| 对比维度 | CAS 5.3 | CAS 6.6 | CAS 7.3 |
|---|---|---|---|
| Spring Boot 版本 | 1.5.x | 2.7.x | 3.5.x |
| 注册方式 | @Component + 自动扫描 | @Configuration + @Bean | @AutoConfiguration + @Bean |
| 自动配置发现 | @ComponentScan | spring.factories | imports 文件 |
| 条件控制 | 无 | @ConditionalOnMissingBean | @ConditionalOnMissingBean + @Primary |
| 循环依赖处理 | @Autowired(直接注入) | @Autowired / @Qualifier | ObjectProvider(延迟注入) |
| 动态刷新 | 不支持 | @RefreshScope | @RefreshScope + ObjectProvider |
| Bean 覆盖 | 同名覆盖(无保护) | @ConditionalOnMissingBean | @Primary + @ConditionalOnMissingBean |
| 命名空间 | javax.* | javax.* | jakarta.* |
| 函数式注册 | 不支持 | Lambda(函数式接口) | Lambda + ObjectProvider |
| 配置集中度 | 分散在各个 @Component 中 | 集中在 @Configuration 类中 | 集中在 @AutoConfiguration 类中 |
| 与 CAS 内部共存 | 容易冲突 | 条件保护 | 条件保护 + 优先级控制 |
演进趋势总结:
从 5.3 到 7.3 的演进,可以归纳为三个核心趋势:
- 从隐式到显式:Bean 注册从分散的
@Component注解逐步收敛到集中的@Configuration/@AutoConfiguration类,开发者对 Bean 生命周期的控制力不断增强。 - 从静态到动态:从容器启动时一次性注册,到支持
@RefreshScope运行时动态刷新,满足了云原生场景下对配置热更新的需求。 - 从脆弱到健壮:通过
@ConditionalOnMissingBean、@Primary、ObjectProvider等机制的引入,自定义 Bean 与 CAS 内部 Bean 的共存方式从"同名覆盖"的脆弱模式,进化为"条件保护 + 优先级选择"的健壮模式。
第二章 Spring Boot 3.x AutoConfiguration 机制深度解析
要深入理解 CAS 7.3 的 Bean 覆盖机制,必须先掌握 Spring Boot 3.x 中 AutoConfiguration 体系的核心原理。本章将从机制变革、文件约定、注解特性和顺序控制四个维度,对 Spring Boot 3.x 的 AutoConfiguration 机制进行系统性解析。
2.1 spring.factories 的淘汰
历史背景:
在 Spring Boot 1.x 和 2.x 时代,自动配置类的发现机制依赖于 META-INF/spring.factories 文件。这个文件使用 Java 标准的 Properties 格式,通过 org.springframework.boot.autoconfigure.EnableAutoConfiguration 键来指定需要加载的自动配置类列表。
# Spring Boot 2.x 的自动配置注册方式(已淘汰)
# META-INF/spring.factories
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.config.MyAutoConfiguration1,\
com.example.config.MyAutoConfiguration21
2
3
4
5
2
3
4
5
淘汰原因:
Spring Boot 3.x 淘汰 spring.factories 机制的原因主要有以下几点:
- 性能问题:
spring.factories使用 Java 的Properties加载机制,需要读取整个 classpath 下所有的spring.factories文件并合并,在大型项目(如 CAS)中,这一过程可能涉及数百个 JAR 包,启动耗时显著增加。 - 语义模糊:
spring.factories文件不仅用于自动配置注册,还被用于ApplicationContextInitializer、ApplicationListener、FailureAnalyzer等多种扩展点的注册,一个文件承载了过多职责。 - 缺乏模块化支持:
spring.factories是一个扁平的键值对文件,无法表达自动配置类之间的依赖关系和加载顺序。 - 与 Jakarta EE 迁移无关但时机巧合:虽然
spring.factories的淘汰与 javax 到 jakarta 的命名空间迁移没有直接关系,但两者同时发生在 Spring Boot 3.x 中,使得这次升级的"破坏性"更加显著。
迁移影响:
对于 CAS Overlay 项目而言,这意味着所有使用 spring.factories 注册自定义自动配置类的代码都需要迁移到新的 imports 文件格式。CAS 6.6 中的如下配置:
# CAS 6.6 - spring.factories(已废弃)
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.cas.config.MyCustomAuthenticationConfiguration1
2
3
2
3
需要改为:
# CAS 7.3 - AutoConfiguration.imports(新方式)
com.example.cas.config.MyCustomAuthenticationConfiguration1
2
2
迁移过程中的常见陷阱:
在实际迁移过程中,开发者可能会遇到以下问题:
- 文件名拼写错误:
org.springframework.boot.autoconfigure.AutoConfiguration.imports这个文件名非常长,手动输入容易出错。建议从 Spring Boot 官方文档或 IDE 模板中复制。 - 文件位置错误:文件必须位于
META-INF/spring/目录下,而不是META-INF/目录下。放在错误位置的文件会被 Spring Boot 忽略。 - 行延续符问题:
spring.factories支持使用反斜杠\将长行拆分,但imports文件不支持。如果从spring.factories复制内容时没有去除行延续符,会导致类名解析失败。 - 空行和编码问题:
imports文件使用 UTF-8 编码,如果文件中包含 BOM(Byte Order Mark)头,可能导致第一行类名解析失败。 - 遗漏迁移:如果项目中存在多个模块,每个模块都有自己的
spring.factories,需要逐一检查并迁移,容易遗漏。
Spring Boot 3.x 对 spring.factories 的兼容处理:
值得注意的是,Spring Boot 3.x 并没有完全移除对 spring.factories 的支持。spring.factories 仍然可以用于注册 ApplicationContextInitializer、ApplicationListener 等其他扩展点,只是 org.springframework.boot.autoconfigure.EnableAutoConfiguration 这个键不再被识别。如果在 Spring Boot 3.x 项目中仍然使用 spring.factories 注册自动配置类,Spring Boot 会静默忽略,不会抛出异常,但自动配置类也不会被加载——这是一个非常隐蔽的问题。
2.2 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports
文件约定:
Spring Boot 3.x 引入了新的自动配置发现机制,使用位于 META-INF/spring/ 目录下的 org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件来替代 spring.factories。
文件路径与命名规则:
src/main/resources/
└── META-INF/
└── spring/
└── org.springframework.boot.autoconfigure.AutoConfiguration.imports1
2
3
4
2
3
4
注意这个文件名非常长,且必须与 Spring Boot 内部定义的常量完全一致。在实际项目中,建议通过 IDE 的代码补全功能或从 Spring Boot 官方文档中复制粘贴,避免手动输入错误。
文件格式:
与 spring.factories 的键值对格式不同,imports 文件是一个纯列表文件,每行一个全限定类名,不需要键前缀:
# 每行一个自动配置类的全限定名
com.example.cas.config.CasOverlayOverrideConfiguration
com.example.cas.config.CasCustomDataSourceConfiguration
com.example.cas.config.CasCustomTicketConfiguration1
2
3
4
2
3
4
格式注意事项:
- 文件使用 UTF-8 编码
- 空行和以
#开头的行被视为注释 - 类名前后的空白字符会被自动去除
- 不支持行延续符(即不能用
\将一个类名拆分到多行) - 如果类名对应的类不存在于 classpath 中,Spring Boot 会在启动时抛出异常
CAS 7.3 内部的 imports 文件示例:
CAS 7.3 自身也大量使用了这一机制。在 cas-server-core 等核心模块的 JAR 包中,可以找到 CAS 内部的自动配置注册文件:
# CAS 7.3 内部的 AutoConfiguration.imports(简化示例)
org.apereo.cas.config.CasCoreAuthenticationConfiguration
org.apereo.cas.config.CasCoreTicketsConfiguration
org.apereo.cas.config.CasCoreWebConfiguration
org.apereo.cas.config.CasPersonDirectoryConfiguration
org.apereo.cas.config.CasOAuth20Configuration
# ... 数百个配置类1
2
3
4
5
6
7
2
3
4
5
6
7
加载性能对比:
┌──────────────────────────────────────────────────────────────┐
│ 自动配置加载性能对比(示意) │
├──────────────────┬───────────────────────────────────────────┤
│ │ 启动时间(相对值) │
├──────────────────┼───────────────────────────────────────────┤
│ spring.factories│ ████████████████████ 100% │
│ (Spring Boot 2) │ 需扫描所有 JAR 中的 factories 文件 │
├──────────────────┼───────────────────────────────────────────┤
│ imports 文件 │ ████████████ 60% │
│ (Spring Boot 3) │ 直接按路径定位,无需全量扫描 │
└──────────────────┴───────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2.3 @AutoConfiguration 注解特性
@AutoConfiguration 是 Spring Boot 3.x 新引入的注解,用于标记自动配置类。它是对传统 @Configuration 注解的专门化扩展,提供了自动配置场景下的额外能力。
注解定义:
java
// Spring Boot 3.x - @AutoConfiguration 注解(简化版)
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(EnableAutoConfiguration.class)
public @interface AutoConfiguration {
// 指定在此自动配置之前加载的配置类
Class<?>[] before() default {};
// 指定在此自动配置之后加载的配置类
Class<?>[] after() default {};
// 指定在所有自动配置之前或之后加载
BeforeAfter beforeAfter() default BeforeAfter.NEITHER;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
与 @Configuration 的关键区别:
| 特性 | @Configuration | @AutoConfiguration |
|---|---|---|
| proxyBeanMethods | 默认 true | 默认 false(Lite 模式) |
| 条件加载 | 无 | 隐含 @ConditionalOnClass(EnableAutoConfiguration.class) |
| 顺序控制 | 通过 @Order 或 @AutoConfigureOrder | 通过 before/after 属性 |
| 语义表达 | 通用配置类 | 专门用于自动配置 |
| Spring Boot 版本 | 所有版本 | 3.0+ |
proxyBeanMethods = false 的影响:
@AutoConfiguration 默认使用 proxyBeanMethods = false,这意味着自动配置类以"Lite 模式"运行。在 Lite 模式下,Spring 不会为配置类创建 CGLIB 代理,@Bean 方法之间的调用不会被拦截,因此一个 @Bean 方法调用同类的另一个 @Bean 方法时,不会返回容器中的单例 Bean,而是直接执行方法体创建一个新对象。
java
@AutoConfiguration
public class ExampleAutoConfiguration {
// Lite 模式:这个方法每次被调用都会创建新对象
@Bean
public Foo foo() {
return new Foo();
}
// 如果 bar() 方法中调用 foo(),得到的是新创建的 Foo 实例,
// 而不是容器中的单例
@Bean
public Bar bar() {
return new Bar(foo()); // 注意:这不是容器中的 Foo 单例!
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
在 CAS 的自动配置类中,这一设计选择是合理的,因为 CAS 的配置类通常只负责声明 Bean 定义,Bean 之间通过方法参数进行依赖注入,而不是通过方法间调用。
before/after 顺序控制:
@AutoConfiguration 的 before 和 after 属性提供了一种声明式的顺序控制机制:
java
@AutoConfiguration(before = CasCoreAuthenticationConfiguration.class)
public class MyCustomAuthenticationConfiguration {
// 这个配置类会在 CAS 核心认证配置之前加载
// 因此其中定义的 Bean 可以被 CAS 核心配置感知到
}
@AutoConfiguration(after = CasCoreAuthenticationConfiguration.class)
public class MyPostAuthenticationConfiguration {
// 这个配置类会在 CAS 核心认证配置之后加载
// 因此可以使用 CAS 核心配置中定义的 Bean
}1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
2.4 自动配置加载顺序控制
在 CAS 这样的超大型框架中,自动配置类的加载顺序至关重要。错误的加载顺序可能导致 Bean 依赖无法满足、条件判断结果不符合预期等问题。
Spring Boot 3.x 的顺序控制层次:
Spring Boot 3.x 提供了多个层次的顺序控制机制,从粗粒度到细粒度依次为:
┌─────────────────────────────────────────────────────────────────┐
│ 自动配置加载顺序控制层次 │
│ │
│ 第1层:@AutoConfiguration(before/after) │
│ ├── 声明式指定前置/后置配置类 │
│ └── 适用于明确知道依赖关系的场景 │
│ │
│ 第2层:@AutoConfigureBefore / @AutoConfigureAfter │
│ ├── 注解级别的顺序声明(Spring Boot 2.x 遗留) │
│ └── 在 Spring Boot 3.x 中仍然有效 │
│ │
│ 第3层:@AutoConfigureOrder │
│ ├── 通过 Order 值控制加载优先级 │
│ └── 数值越小优先级越高 │
│ │
│ 第4层:@ConditionalOn* 条件注解 │
│ ├── 基于条件判断决定是否加载 │
│ └── 间接影响加载顺序 │
│ │
│ 第5层:imports 文件中的声明顺序 │
│ ├── 同一文件中,先声明的先加载 │
│ └── 不同文件之间,顺序不确定 │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
CAS 7.3 中的顺序控制实践:
在 CAS 7.3 的 Overlay 项目中,自定义自动配置类的加载顺序通常需要遵循以下原则:
- 自定义认证配置应在 CAS 核心认证配置之后加载:确保 CAS 的核心认证基础设施(如
AuthenticationEventExecutionPlan)已经初始化完毕。 - 自定义数据源配置应在 CAS 数据访问配置之前加载:确保自定义的数据源 Bean 能够被 CAS 的 MyBatis 集成模块感知到。
- 覆盖配置应在被覆盖配置之后加载:确保
@Primary和@ConditionalOnMissingBean的条件判断在正确的时机执行。
java
// CAS 7.3 Overlay 中的典型顺序控制
@AutoConfiguration(
after = {
CasCoreAuthenticationConfiguration.class,
CasCoreTicketsConfiguration.class,
CasPersonDirectoryConfiguration.class
}
)
public class CasOverlayOverrideConfiguration {
// 在 CAS 核心配置全部加载完毕后,再执行覆盖逻辑
// 此时 @ConditionalOnMissingBean 可以正确判断
// CAS 内部是否已经注册了某个 Bean
}1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
调试自动配置加载顺序:
当需要排查自动配置加载顺序问题时,Spring Boot 提供了 --debug 启动选项和 ConditionEvaluationReport:
properties
# application.properties
debug=true1
2
2
启动时会输出一份详细的自动配置报告,包含每个自动配置类的匹配状态和原因。此外,也可以通过 Actuator 端点查看:
properties
# 启用条件评估报告端点
management.endpoint.conditions.enabled=true
management.endpoints.web.exposure.include=conditions1
2
3
2
3
第三章 org.apereo.cas.config 包覆盖技巧
CAS 7.3 的 Bean 覆盖机制中,有一个非常精妙但容易被忽视的技巧:将自定义自动配置类放在 org.apereo.cas.config 包下。这一技巧利用了 Spring Boot 自动配置加载的内部机制,能够在不修改 CAS 源码的情况下,以最简洁的方式覆盖 CAS 的默认行为。
3.1 包路径覆盖的核心原理
Spring Boot 的自动配置类过滤机制:
Spring Boot 在加载自动配置时,会从 classpath 中扫描所有 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件,收集其中列出的全限定类名,然后通过反射加载这些类。在这个过程中,Spring Boot 会对自动配置类进行过滤和排序。
包路径作为"隐式排序键":
虽然 Spring Boot 并没有显式地将包路径作为排序依据,但 CAS 内部的自动配置类全部位于 org.apereo.cas.config 包下,且 CAS 的 imports 文件中列出的配置类按照功能模块的依赖关系进行了精心排序。当我们将自定义配置类也放在 org.apereo.cas.config 包下时,可以确保它与 CAS 内部的配置类在同一个"命名空间"中,从而更容易控制加载顺序。
更深层的原理——CAS 内部的组件扫描:
CAS 在启动时会执行组件扫描,扫描的基包通常是 org.apereo.cas。这意味着位于 org.apereo.cas.config 包下的 @Configuration 或 @AutoConfiguration 类会被 CAS 的组件扫描机制发现。更重要的是,CAS 内部的很多条件注解(如 @ConditionalOnMissingBean)的判断结果会受到组件扫描顺序的影响。
当自定义配置类位于 org.apereo.cas.config 包下时,它与 CAS 内部的配置类处于同一个组件扫描批次中。Spring 的组件扫描默认按照类名的字典序排列,因此我们可以通过精心设计自定义配置类的类名来控制其相对于 CAS 内部配置类的加载顺序。
┌─────────────────────────────────────────────────────────────────┐
│ 组件扫描顺序与类名的关系 │
│ │
│ org.apereo.cas.config 包下的类按字典序排列: │
│ │
│ 1. CasAcceptUsersAuthenticationConfiguration ← CAS 内部 │
│ 2. CasCoreAuthenticationConfiguration ← CAS 内部 │
│ 3. CasCoreTicketsConfiguration ← CAS 内部 │
│ 4. CasCoreWebConfiguration ← CAS 内部 │
│ ... │
│ N. CasOverlayOverrideConfiguration ← 自定义(排在后面) │
│ │
│ 由于 "CasOverlay..." 在字典序上排在 "CasCore..." 之后, │
│ 自定义配置类会在 CAS 核心配置类之后被处理。 │
│ 此时 @ConditionalOnMissingBean 可以正确感知到 │
│ CAS 内部已注册的 Bean。 │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
更重要的原因——@ConditionalOnMissingBean 的时序保证:
CAS 内部的自动配置类大量使用了 @ConditionalOnMissingBean 注解。这个注解的判断时机取决于配置类的加载顺序。如果自定义配置类在 CAS 内部配置类之前加载,那么 @ConditionalOnMissingBean 会认为"容器中还没有这个 Bean",从而触发自定义 Bean 的创建;如果自定义配置类在 CAS 内部配置类之后加载,那么 @ConditionalOnMissingBean 会认为"容器中已经有了这个 Bean",从而跳过自定义 Bean 的创建。
@ConditionalOnMissingBean 的判断时机详解:
@ConditionalOnMissingBean 的判断时机是一个容易被误解的问题。它的判断时机不是在 @Bean 方法被调用时,而是在 Spring 容器处理 @Configuration 类时(即所谓的"配置类解析阶段")。这意味着:
如果 CAS 内部的配置类先被解析,其中的
@Bean方法定义的 Bean 会被注册到 BeanDefinitionRegistry 中。随后解析自定义配置类时,@ConditionalOnMissingBean检查 BeanDefinitionRegistry,发现 Bean 已经存在,条件不满足,跳过自定义 Bean 的创建。如果自定义配置类先被解析,其中的
@Bean方法定义的 Bean 会被注册到 BeanDefinitionRegistry 中。随后解析 CAS 内部配置类时,@ConditionalOnMissingBean检查 BeanDefinitionRegistry,发现 Bean 已经存在,条件不满足,跳过 CAS 默认 Bean 的创建。
这就是为什么加载顺序如此关键——它直接决定了 @ConditionalOnMissingBean 的判断结果。
┌─────────────────────────────────────────────────────────────────┐
│ @ConditionalOnMissingBean 时序图 │
│ │
│ 时间轴 ──────────────────────────────────────────────► │
│ │
│ 场景A:自定义配置先加载(期望行为) │
│ ┌──────────────┐ ┌──────────────────────┐ │
│ │ 自定义配置类 │ │ CAS 内部配置类 │ │
│ │ @Conditional │ │ @Conditional │ │
│ │ OnMissingBean│ │ OnMissingBean │ │
│ │ → Bean不存在 │ │ → Bean已存在 │ │
│ │ → 创建自定义 │ │ → 跳过(使用自定义) │ │
│ │ Bean ✓ │ │ │ │
│ └──────────────┘ └──────────────────────┘ │
│ │
│ 场景B:CAS 内部配置先加载(非期望行为) │
│ ┌──────────────────────┐ ┌──────────────┐ │
│ │ CAS 内部配置类 │ │ 自定义配置类 │ │
│ │ @Conditional │ │ @Conditional │ │
│ │ OnMissingBean │ │ OnMissingBean│ │
│ │ → Bean不存在 │ │ → Bean已存在 │ │
│ │ → 创建默认Bean │ │ → 跳过(使用默认)│ │
│ │ ✗ │ │ │ │
│ └──────────────────────┘ └──────────────┘ │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
3.2 CasOverlayOverrideConfiguration 设计
设计目标:
CasOverlayOverrideConfiguration 是 CAS 7.3 Overlay 项目中最核心的自定义配置类。它的设计目标是:
- 集中管理所有 Bean 覆盖逻辑:将所有自定义 Bean 定义集中在一个配置类中,便于维护和审查。
- 精确控制加载顺序:通过
@AutoConfiguration的after属性和包路径约定,确保在 CAS 内部配置之后加载。 - 优雅处理 Bean 冲突:通过
@ConditionalOnMissingBean和@Primary的组合使用,实现与 CAS 内部 Bean 的和谐共存。 - 支持运行时刷新:通过
@RefreshScope注解,支持认证处理器的动态刷新。
完整设计示例:
java
package org.apereo.cas.config;
import org.apereo.cas.authentication.AuthenticationEventExecutionPlan;
import org.apereo.cas.authentication.AuthenticationHandler;
import org.apereo.cas.authentication.AuthenticationMetaDataPopulator;
import org.apereo.cas.authentication.principal.PrincipalResolver;
import org.apereo.cas.configuration.CasConfigurationProperties;
import org.apereo.cas.util.spring.beans.BeanSupplier;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.context.config.annotation.RefreshScope;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Primary;
/**
* CAS Overlay 项目核心覆盖配置类。
*
* 通过将此类放在 org.apereo.cas.config 包下,
* 利用 CAS 内部的 @ConditionalOnMissingBean 机制,
* 精确覆盖 CAS 的默认行为。
*/
@AutoConfiguration
public class CasOverlayOverrideConfiguration {
/**
* 自定义认证处理器。
*
* 使用 @Primary 确保在存在多个 AuthenticationHandler 时,
* 此处理器被优先选择。
* 使用 @RefreshScope 支持运行时动态刷新。
* 使用 @ConditionalOnMissingBean 防止重复注册。
*/
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(name = "myDatabaseAuthenticationHandler")
public AuthenticationHandler myDatabaseAuthenticationHandler(
CasConfigurationProperties casProperties) throws Exception {
return BeanSupplier.of(AuthenticationHandler.class)
.when(BeanSupplier.Condition.given(
casProperties.getAuthn().getDatabase().isEnabled()))
.supplier(() -> {
var dbProps = casProperties.getAuthn().getDatabase();
return new MyDatabaseAuthenticationHandler(
dbProps.getUrl(),
dbProps.getUsername(),
dbProps.getPassword(),
dbProps.getQuery(),
dbProps.getFieldPassword(),
dbProps.getFieldExpired()
);
})
.otherwise(() -> new AcceptUsersAuthenticationHandler())
.get();
}
/**
* 自定义认证事件执行计划配置器。
*
* 这是覆盖 CAS 默认认证行为的关键 Bean。
* 通过清除所有现有认证处理器并只注册自定义处理器,
* 实现对认证链路的完全控制。
*/
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(
name = "myAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
myAuthenticationEventExecutionPlanConfigurer(
ObjectProvider<AuthenticationHandler> handlers,
ObjectProvider<PrincipalResolver> resolvers,
ObjectProvider<AuthenticationMetaDataPopulator>
metaDataPopulators) {
return plan -> {
// 第一步:清除所有现有认证处理器
// 这一步会移除 CAS 默认注册的所有处理器,
// 包括 AcceptUsersAuthenticationHandler
plan.getAuthenticationHandlers().clear();
// 第二步:注册自定义认证处理器
handlers.orderedStream().forEach(handler -> {
plan.registerAuthenticationHandler(handler);
// 关联 Principal 解析器
var resolver = resolvers.getIfAvailable(
() -> new DefaultPrincipalResolver());
plan.registerAuthenticationHandlerWithPrincipalResolver(
handler, resolver);
});
// 第三步:注册认证元数据填充器(可选)
metaDataPopulators.orderedStream().forEach(populator -> {
plan.registerAuthenticationMetadataPopulator(populator);
});
};
}
/**
* 覆盖 CAS 默认的 AcceptUsers 认证处理器配置。
*
* 通过提供一个空的配置器实现,
* 防止 CAS 注册默认的静态用户认证处理器。
* 这可以消除启动时的 "acceptUsers" 警告。
*/
@Bean
@Primary
@ConditionalOnMissingBean(
name = "acceptUsersAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
acceptUsersAuthenticationEventExecutionPlanConfigurer() {
// 返回空的配置器,不注册任何认证处理器
return plan -> { /* no-op */ };
}
/**
* 覆盖 CAS 默认的 AcceptUsers 初始化 Bean。
*
* 消除启动时的警告日志:
* "AcceptUsersAuthenticationHandler is configured to accept
* a static list of usernames..."
*/
@Bean
@Primary
@ConditionalOnMissingBean(
name = "acceptUsersAuthenticationInitializingBean")
public AcceptUsersAuthenticationInitializingBean
acceptUsersAuthenticationInitializingBean() {
// 返回空的初始化 Bean,不执行任何初始化逻辑
return () -> { /* no-op */ };
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
3.3 @ConditionalOnMissingBean 的利用
CAS 内部的条件注解模式:
CAS 7.3 内部的自动配置类大量使用了 @ConditionalOnMissingBean 注解。这意味着 CAS 的设计者在编写自动配置时,已经为外部覆盖预留了"扩展点"。理解这些扩展点的分布和触发条件,是精准覆盖 CAS 行为的关键。
常见的 CAS 扩展点:
java
// CAS 内部自动配置类中的典型模式(教学示例)
@AutoConfiguration
public class CasCoreAuthenticationAutoConfiguration {
@Bean
@ConditionalOnMissingBean(name = "acceptUsersAuthenticationHandler")
public AuthenticationHandler acceptUsersAuthenticationHandler(
CasConfigurationProperties casProperties) {
// 如果容器中没有名为 "acceptUsersAuthenticationHandler" 的 Bean,
// 则创建 CAS 默认的静态用户认证处理器
var handler = new AcceptUsersAuthenticationHandler();
// ... 配置 handler ...
return handler;
}
@Bean
@ConditionalOnMissingBean(
name = "acceptUsersAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
acceptUsersAuthenticationEventExecutionPlanConfigurer(
@Qualifier("acceptUsersAuthenticationHandler")
AuthenticationHandler handler,
PrincipalResolver resolver) {
// 如果容器中没有对应的 Configurer Bean,
// 则创建默认的 Configurer 来注册 AcceptUsers 处理器
return plan -> {
plan.registerAuthenticationHandlerWithPrincipalResolver(
handler, resolver);
};
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
利用策略:
基于 CAS 内部的条件注解模式,我们可以采用以下策略进行覆盖:
- 同名 Bean 覆盖:在自定义配置类中定义与 CAS 内部同名的 Bean,配合
@Primary注解确保自定义 Bean 被优先选择。 - 前置 Bean 覆盖:在 CAS 内部配置类加载之前,预先注册一个满足
@ConditionalOnMissingBean条件的 Bean,使 CAS 的默认 Bean 定义被跳过。 - Configurer 覆盖:覆盖
AuthenticationEventExecutionPlanConfigurer类型的 Bean,在 Lambda 表达式中完全控制认证处理器的注册逻辑。
3.4 实际覆盖案例解析
案例一:覆盖默认认证处理器
需求:使用数据库认证替代 CAS 默认的静态用户认证。
CAS 默认行为:CAS 启动时会注册 AcceptUsersAuthenticationHandler,从 cas.authn.accept.users 配置中读取静态用户列表(如 admin::password)。
覆盖方案:
java
@AutoConfiguration
public class CasOverlayOverrideConfiguration {
// 步骤1:定义自定义认证处理器
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(name = "myDatabaseAuthenticationHandler")
public AuthenticationHandler myDatabaseAuthenticationHandler(
CasConfigurationProperties casProperties) {
var dbAuthn = casProperties.getCustom()
.getAuthentication().getDatabase();
return new MyDatabaseAuthenticationHandler(
dbAuthn.getUrl(),
dbAuthn.getUsername(),
dbAuthn.getPassword(),
dbAuthn.getAuthenticationQuery(),
dbAuthn.getPasswordEncoder()
);
}
// 步骤2:覆盖认证事件执行计划配置器
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(
name = "myAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
myAuthenticationEventExecutionPlanConfigurer(
@Qualifier("myDatabaseAuthenticationHandler")
AuthenticationHandler handler,
@Qualifier("defaultPrincipalResolver")
PrincipalResolver resolver) {
return plan -> {
// 清除所有现有处理器(包括 AcceptUsers)
plan.getAuthenticationHandlers().clear();
// 只注册数据库认证处理器
plan.registerAuthenticationHandlerWithPrincipalResolver(
handler, resolver);
};
}
// 步骤3:覆盖 AcceptUsers 相关 Bean,消除警告
@Bean
@Primary
@ConditionalOnMissingBean(
name = "acceptUsersAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
acceptUsersAuthenticationEventExecutionPlanConfigurer() {
return plan -> { /* no-op: 不注册 AcceptUsers 处理器 */ };
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
案例二:覆盖 OAuth2.0 配置上下文
需求:自定义 OAuth2.0 的 Token 生成和验证逻辑。
java
@AutoConfiguration
public class CasOverlayOAuthConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean(name = "customOAuth20ConfigurationContext")
public OAuth20ConfigurationContext customOAuth20ConfigurationContext(
ObjectProvider<OAuth20TokenGenerator> tokenGenerators,
ObjectProvider<OAuth20ResponseGenerator> responseGenerators,
CasConfigurationProperties casProperties) {
// 使用 ObjectProvider 延迟获取依赖,避免循环依赖
var tokenGenerator = tokenGenerators.getIfAvailable(
() -> new DefaultOAuth20TokenGenerator());
var responseGenerator = responseGenerators.getIfAvailable(
() -> new DefaultOAuth20ResponseGenerator());
return OAuth20ConfigurationContext.builder()
.tokenGenerator(tokenGenerator)
.responseGenerator(responseGenerator)
.casProperties(casProperties)
.build();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
案例三:覆盖数据源配置
需求:使用自定义的数据源配置替代 CAS 默认的数据源。
java
@AutoConfiguration(before = CasCoreDataSourceConfiguration.class)
public class CasOverlayDataSourceConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean(name = "customDataSource")
public DataSource customDataSource(CasConfigurationProperties props) {
var dsProps = props.getCustom().getDataSource();
var config = new HikariConfig();
config.setJdbcUrl(dsProps.getUrl());
config.setUsername(dsProps.getUsername());
config.setPassword(dsProps.getPassword());
config.setDriverClassName(dsProps.getDriverClassName());
config.setMaximumPoolSize(dsProps.getMaximumPoolSize());
config.setMinimumIdle(dsProps.getMinimumIdle());
config.setConnectionTimeout(dsProps.getConnectionTimeout());
config.setIdleTimeout(dsProps.getIdleTimeout());
return new HikariDataSource(config);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
第四章 认证处理器注册模式深度剖析
认证处理器(Authentication Handler)是 CAS 认证架构的核心组件。将企业自有的认证处理器正确注册到 CAS 的认证链路中,是每个 CAS Overlay 项目必须完成的关键任务。本章将从 CAS 5.3 到 7.3 的三代演进视角,深度剖析认证处理器注册模式的设计理念和实现细节。
4.1 CAS 5.3:@Component 自动扫描
设计理念:
CAS 5.3 的认证处理器注册基于 Spring 的组件扫描机制。开发者只需将自定义认证处理器类放在 Spring 的组件扫描路径下,并通过 @Component 注解标记,Spring 容器就会自动发现并注册这个 Bean。
注册流程:
┌─────────────────────────────────────────────────────────────────┐
│ CAS 5.3 认证处理器注册流程 │
│ │
│ 1. Spring 容器启动 │
│ │ │
│ ▼ │
│ 2. @ComponentScan 扫描指定包路径 │
│ │ │
│ ▼ │
│ 3. 发现带有 @Component 注解的认证处理器类 │
│ │ │
│ ▼ │
│ 4. 创建认证处理器 Bean 实例 │
│ │ │
│ ▼ │
│ 5. @PostConstruct 方法被调用 │
│ │ │
│ ▼ │
│ 6. 在 @PostConstruct 中通过 @Autowired 获取 │
│ AuthenticationEventExecutionPlan 并注册处理器 │
│ │ │
│ ▼ │
│ 7. 认证处理器就绪,可接受认证请求 │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
典型实现:
java
@Component("myLdapAuthenticationHandler")
public class MyLdapAuthenticationHandler
extends AbstractLdapAuthenticationHandler {
@Autowired
@Qualifier("authenticationEventExecutionPlan")
private AuthenticationEventExecutionPlan plan;
@Autowired
@Qualifier("personDirectoryPrincipalResolver")
private PrincipalResolver principalResolver;
@PostConstruct
public void register() {
plan.registerAuthenticationHandlerWithPrincipalResolver(
this, principalResolver);
}
@Override
protected AuthenticationHandlerExecutionResult doAuthentication(
UsernamePasswordCredential credential)
throws GeneralSecurityException, PreventedException {
// LDAP 认证逻辑
// ...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
CAS 5.3 模式的缺陷:
- 生命周期耦合:认证处理器的注册逻辑与
@PostConstruct生命周期回调绑定,如果AuthenticationEventExecutionPlan在@PostConstruct执行时尚未初始化完成,会导致NullPointerException。 - 缺乏条件保护:没有
@ConditionalOnMissingBean等条件注解,如果 classpath 中存在多个同类型的认证处理器,容易产生 Bean 冲突。 - 不支持动态刷新:认证处理器在容器启动时一次性注册,修改认证策略需要重启应用。
- 隐式依赖:通过
@Autowired直接注入AuthenticationEventExecutionPlan,依赖关系不够显式,增加了代码审查的难度。
4.2 CAS 6.6:@Bean + @RefreshScope
设计理念:
CAS 6.6 引入了 @Configuration 类和 @Bean 方法来声明认证处理器,同时引入 @RefreshScope 注解支持运行时动态刷新。这一变化使得认证处理器的注册从"隐式"变为"显式",从"静态"变为"动态"。
注册流程:
┌─────────────────────────────────────────────────────────────────┐
│ CAS 6.6 认证处理器注册流程 │
│ │
│ 1. Spring 容器启动 │
│ │ │
│ ▼ │
│ 2. 加载 spring.factories 中的自动配置类 │
│ │ │
│ ▼ │
│ 3. 实例化 @Configuration 类 │
│ │ │
│ ▼ │
│ 4. 执行 @Bean 方法,创建认证处理器实例 │
│ │ (带有 @RefreshScope 的 Bean 被包装在代理中) │
│ ▼ │
│ 5. 执行 @Bean 方法,创建 │
│ AuthenticationEventExecutionPlanConfigurer │
│ │ │
│ ▼ │
│ 6. CAS 内部收集所有 Configurer,按顺序执行 │
│ │ │
│ ▼ │
│ 7. 认证处理器注册到 AuthenticationEventExecutionPlan 中 │
│ │ │
│ ▼ │
│ 8. 认证处理器就绪 │
│ │ │
│ ▼ │
│ 9. (可选)通过 /actuator/refresh 触发 @RefreshScope 刷新 │
│ │ │
│ ▼ │
│ 10. 重新创建认证处理器实例,重新注册到 Plan 中 │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
典型实现:
java
@Configuration(proxyBeanMethods = false)
public class MyAuthenticationConfiguration {
@Bean
@RefreshScope
@ConditionalOnMissingBean(name = "myLdapAuthenticationHandler")
public AuthenticationHandler myLdapAuthenticationHandler(
CasConfigurationProperties casProperties) {
var ldapProps = casProperties.getAuthn().getLdap().get(0);
return new MyLdapAuthenticationHandler(
ldapProps.getLdapUrl(),
ldapProps.getBaseDn(),
ldapProps.getBindDn(),
ldapProps.getBindCredential(),
ldapProps.getSearchFilter(),
ldapProps.getSubtreeSearch()
);
}
@Bean
@RefreshScope
@ConditionalOnMissingBean(
name = "myAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
myAuthenticationEventExecutionPlanConfigurer(
@Qualifier("myLdapAuthenticationHandler")
AuthenticationHandler ldapHandler,
@Qualifier("acceptUsersAuthenticationHandler")
AuthenticationHandler acceptUsersHandler,
@Qualifier("personDirectoryPrincipalResolver")
PrincipalResolver resolver) {
return plan -> {
// 注册 LDAP 认证处理器
plan.registerAuthenticationHandlerWithPrincipalResolver(
ldapHandler, resolver);
// 可选:保留静态用户认证作为后备
if (casProperties.getAuthn().getAccept().isEnabled()) {
plan.registerAuthenticationHandlerWithPrincipalResolver(
acceptUsersHandler, resolver);
}
};
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
CAS 6.6 模式的改进:
- 显式依赖:通过
@Bean方法的参数声明依赖关系,Spring 容器在调用方法之前会确保所有依赖都已就绪。 - 条件保护:
@ConditionalOnMissingBean防止同名 Bean 的重复注册。 - 动态刷新:
@RefreshScope支持在不重启应用的情况下更新认证处理器配置。 - 函数式注册:
AuthenticationEventExecutionPlanConfigurer是一个函数式接口,使用 Lambda 表达式使注册代码更加简洁。
4.3 CAS 7.3:@AutoConfiguration + @Primary
设计理念:
CAS 7.3 在 6.6 的基础上进一步演进,引入了 @AutoConfiguration 注解和 @Primary 优先级控制机制。这一变化的核心目的是更好地处理自定义 Bean 与 CAS 内部 Bean 之间的优先级关系,确保自定义行为能够可靠地覆盖默认行为。
注册流程:
┌─────────────────────────────────────────────────────────────────┐
│ CAS 7.3 认证处理器注册流程 │
│ │
│ 1. Spring Boot 3.x 容器启动 │
│ │ │
│ ▼ │
│ 2. 扫描所有 AutoConfiguration.imports 文件 │
│ │ │
│ ▼ │
│ 3. 按顺序加载自动配置类 │
│ │ (CAS 内部配置类 → 自定义覆盖配置类) │
│ ▼ │
│ 4. CAS 内部配置类执行 @ConditionalOnMissingBean 判断 │
│ │ → 发现容器中已有同名 Bean → 跳过默认 Bean 创建 │
│ ▼ │
│ 5. 自定义配置类创建 @Primary 认证处理器 Bean │
│ │ (带有 @RefreshScope 的 Bean 被包装在代理中) │
│ ▼ │
│ 6. 自定义配置类创建 @Primary Configurer Bean │
│ │ │
│ ▼ │
│ 7. CAS 内部收集所有 Configurer(@Primary 优先) │
│ │ │
│ ▼ │
│ 8. 执行 Configurer,注册认证处理器到 Plan 中 │
│ │ │
│ ▼ │
│ 9. 认证处理器就绪 │
│ │ │
│ ▼ │
│ 10. ObjectProvider 延迟解析所有依赖 │
│ │ │
│ ▼ │
│ 11. (可选)通过 /actuator/refresh 触发动态刷新 │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
典型实现:
java
@AutoConfiguration
public class CasOverlayOverrideConfiguration {
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(name = "primaryAuthenticationHandler")
public AuthenticationHandler primaryAuthenticationHandler(
CasConfigurationProperties casProperties) {
// 使用 BeanSupplier 进行条件化创建
return BeanSupplier.of(AuthenticationHandler.class)
.when(BeanSupplier.Condition.given(
casProperties.getCustom().getAuthn().getDatabase()
.isEnabled()))
.supplier(() -> new MyDatabaseAuthenticationHandler(
casProperties.getCustom().getAuthn().getDatabase()))
.otherwise(() -> new AcceptUsersAuthenticationHandler())
.get();
}
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(
name = "overlayAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
overlayAuthenticationEventExecutionPlanConfigurer(
ObjectProvider<AuthenticationHandler> allHandlers,
ObjectProvider<PrincipalResolver> allResolvers) {
return plan -> {
// 清除所有现有认证处理器
plan.getAuthenticationHandlers().clear();
// 使用 ObjectProvider 延迟获取所有认证处理器
allHandlers.orderedStream()
.filter(handler ->
handler.getName().startsWith("myCustom"))
.forEach(handler -> {
plan.registerAuthenticationHandler(handler);
// 关联 Principal 解析器
allResolvers.orderedStream()
.findFirst()
.ifPresent(resolver ->
plan.registerAuthenticationHandlerWithPrincipalResolver(
handler, resolver));
});
};
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
4.4 acceptUsersAuthenticationEventExecutionPlanConfigurer 覆盖
问题背景:
CAS 默认会注册一个 AcceptUsersAuthenticationHandler,用于处理静态用户认证(即 cas.authn.accept.users 配置中定义的用户名密码对)。在生产环境中,通常会使用数据库、LDAP 等外部认证源,静态用户认证不仅没有必要,还会带来以下问题:
- 安全风险:静态用户凭证以明文形式存储在配置文件中,不符合安全审计要求。
- 维护负担:用户凭证变更需要修改配置文件并重启应用。
- 日志噪音:CAS 启动时会输出关于
AcceptUsersAuthenticationHandler的警告日志,干扰问题排查。
CAS 内部的默认注册逻辑:
java
// CAS 内部自动配置(简化示例)
@AutoConfiguration
public class CasAcceptUsersAuthenticationConfiguration {
@Bean
@ConditionalOnMissingBean(name = "acceptUsersAuthenticationHandler")
public AuthenticationHandler acceptUsersAuthenticationHandler(
CasConfigurationProperties casProperties) {
var handler = new AcceptUsersAuthenticationHandler();
handler.setUsers(casProperties.getAuthn().getAccept().getUsers());
return handler;
}
@Bean
@ConditionalOnMissingBean(
name = "acceptUsersAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
acceptUsersAuthenticationEventExecutionPlanConfigurer(
@Qualifier("acceptUsersAuthenticationHandler")
AuthenticationHandler handler,
PrincipalResolver resolver) {
return plan -> {
plan.registerAuthenticationHandlerWithPrincipalResolver(
handler, resolver);
};
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
覆盖方案:
java
@AutoConfiguration
public class CasOverlayOverrideConfiguration {
/**
* 覆盖 CAS 默认的 AcceptUsers 认证事件执行计划配置器。
*
* 通过返回一个空的 Configurer(不注册任何处理器),
* 阻止 CAS 默认的 AcceptUsersAuthenticationHandler 被注册到
* 认证链路中。
*
* @Primary 注解确保在存在多个同类型 Bean 时,
* 此 Configurer 被优先选择。
*/
@Bean
@Primary
@ConditionalOnMissingBean(
name = "acceptUsersAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
acceptUsersAuthenticationEventExecutionPlanConfigurer() {
return plan -> {
// 空实现:不注册 AcceptUsers 处理器
// CAS 的默认 Configurer 会被 @Primary 覆盖
};
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
注意事项:
@Primary注解在这里是必要的。因为 CAS 内部已经定义了一个名为acceptUsersAuthenticationEventExecutionPlanConfigurer的 Bean,如果不使用@Primary,Spring 容器在发现多个同类型候选者时会抛出NoUniqueBeanDefinitionException。@ConditionalOnMissingBean的name属性应与 CAS 内部定义的 Bean 名称一致。如果 CAS 在后续版本中更改了这个名称,覆盖逻辑需要相应调整。
4.5 消除静态凭证警告
警告信息:
CAS 启动时,如果检测到 AcceptUsersAuthenticationHandler 被配置但未使用,或者配置了不安全的静态凭证,会输出如下警告日志:
WARN [org.apereo.cas.authentication.AcceptUsersAuthenticationHandler] -
AcceptUsersAuthenticationHandler is configured to accept a static list
of usernames [admin::password] for authentication. This is NOT recommended
for production environments and should ONLY be used for testing purposes.1
2
3
4
2
3
4
警告来源分析:
这个警告来自 CAS 内部的 AcceptUsersAuthenticationInitializingBean,它实现了 Spring 的 InitializingBean 接口,在 afterPropertiesSet() 方法中执行安全检查:
java
// CAS 内部(简化示例)
public class AcceptUsersAuthenticationInitializingBean
implements InitializingBean {
private final CasConfigurationProperties casProperties;
@Override
public void afterPropertiesSet() {
var users = casProperties.getAuthn().getAccept().getUsers();
if (users != null && !users.isEmpty()) {
LOGGER.warn(
"AcceptUsersAuthenticationHandler is configured to accept "
+ "a static list of usernames [{}] for authentication. "
+ "This is NOT recommended for production environments...",
String.join(", ", users.keySet()));
}
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
消除方案:
java
@AutoConfiguration
public class CasOverlayOverrideConfiguration {
/**
* 覆盖 CAS 默认的 AcceptUsers 初始化 Bean。
*
* 返回一个空的 InitializingBean 实现,
* 阻止 CAS 内部的安全检查警告被输出。
*/
@Bean
@Primary
@ConditionalOnMissingBean(
name = "acceptUsersAuthenticationInitializingBean")
public AcceptUsersAuthenticationInitializingBean
acceptUsersAuthenticationInitializingBean() {
return () -> {
// 空实现:不执行任何初始化逻辑
// 阻止 CAS 内部的静态凭证安全检查警告
};
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
完整的覆盖清单:
为了彻底消除 CAS 默认静态用户认证的所有痕迹,需要覆盖以下三个 Bean:
| Bean 名称 | 类型 | 覆盖目的 |
|---|---|---|
acceptUsersAuthenticationHandler | AuthenticationHandler | 阻止默认认证处理器创建 |
acceptUsersAuthenticationEventExecutionPlanConfigurer | AuthenticationEventExecutionPlanConfigurer | 阻止默认处理器注册到认证链路 |
acceptUsersAuthenticationInitializingBean | AcceptUsersAuthenticationInitializingBean | 消除启动时的安全检查警告 |
第五章 @Primary 与 ObjectProvider 延迟注入
在 CAS 7.3 的 Bean 覆盖机制中,@Primary 和 ObjectProvider 是两个至关重要的工具。@Primary 解决了多 Bean 候选者的优先级选择问题,ObjectProvider 则解决了循环依赖和延迟初始化问题。本章将深入剖析这两个机制的工作原理和在 CAS 中的最佳实践。
5.1 @Primary 的作用与风险
@Primary 的基本语义:
@Primary 是 Spring Framework 提供的注解,用于在同一个类型存在多个 Bean 候选者时,标记一个"首选"的 Bean。当其他组件通过 @Autowired 注入该类型的依赖时,如果未指定 @Qualifier,Spring 容器会自动选择带有 @Primary 注解的 Bean。
java
// @Primary 的基本用法
@Configuration
public class HandlerConfiguration {
@Bean
@Primary // 标记为首选 Bean
public AuthenticationHandler databaseHandler() {
return new DatabaseAuthenticationHandler();
}
@Bean // 非首选 Bean
public AuthenticationHandler ldapHandler() {
return new LdapAuthenticationHandler();
}
}
// 注入时无需指定 @Qualifier,自动选择 @Primary Bean
@Service
public class AuthenticationService {
@Autowired // 注入的是 databaseHandler(@Primary)
private AuthenticationHandler handler;
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
在 CAS 覆盖场景中的作用:
在 CAS 7.3 的 Overlay 项目中,@Primary 的核心作用是确保自定义 Bean 在与 CAS 内部 Bean 竞争时被优先选择:
┌─────────────────────────────────────────────────────────────────┐
│ @Primary 在 CAS 覆盖中的作用 │
│ │
│ Spring 容器中存在两个同类型 Bean: │
│ │
│ ┌─────────────────────────────┐ ┌────────────────────────────┐ │
│ │ CAS 内部默认 Bean │ │ 自定义覆盖 Bean │ │
│ │ │ │ │ │
│ │ name: acceptUsers... │ │ name: acceptUsers... │ │
│ │ type: Configurer │ │ type: Configurer │ │
│ │ @Primary: 无 │ │ @Primary: 有 ✓ │ │
│ └─────────────────────────────┘ └────────────────────────────┘ │
│ │
│ 当 CAS 内部组件需要注入 Configurer 时: │
│ → Spring 自动选择带有 @Primary 的自定义 Bean │
│ → CAS 内部的默认行为被覆盖 │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Primary 的风险:
使用 @Primary 并非没有风险。以下是需要注意的几个方面:
全局影响:
@Primary的作用范围是整个 Spring 容器。一旦标记了@Primary,所有注入该类型依赖的地方都会受到影响,而不仅仅是 CAS 内部的注入点。如果项目中存在其他需要使用非@PrimaryBean 的场景,需要通过@Qualifier显式指定。多重 @Primary 冲突:如果同一个类型存在多个
@PrimaryBean,Spring 容器启动时会抛出异常。在 CAS Overlay 项目中,如果同时引入了多个自定义模块,每个模块都试图将同类型的 Bean 标记为@Primary,就会产生冲突。
java
// 错误示例:多个 @Primary 导致冲突
@AutoConfiguration
public class ModuleAConfiguration {
@Bean
@Primary // 冲突!
public AuthenticationHandler moduleAHandler() { ... }
}
@AutoConfiguration
public class ModuleBConfiguration {
@Bean
@Primary // 冲突!
public AuthenticationHandler moduleBHandler() { ... }
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
- 隐式覆盖:
@Primary的覆盖行为是隐式的——开发者可能不知道某个 Bean 已经被覆盖,直到运行时才发现行为不符合预期。建议在自定义配置类中添加清晰的注释,说明覆盖的目的和影响范围。
最佳实践:
java
@AutoConfiguration
public class CasOverlayOverrideConfiguration {
/**
* 覆盖 CAS 默认的认证事件执行计划配置器。
*
* <p>使用 @Primary 确保此 Configurer 在存在多个候选者时被优先选择。
* 这意味着 CAS 默认的 AcceptUsers 认证处理器不会被注册到认证链路中。</p>
*
* <p><b>影响范围:</b>所有通过类型注入 AuthenticationEventExecutionPlanConfigurer
* 的组件都会使用此 Configurer。如果需要使用 CAS 默认的 Configurer,
* 请通过 @Qualifier("原始Bean名称") 显式指定。</p>
*/
@Bean
@Primary
@ConditionalOnMissingBean(
name = "acceptUsersAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
acceptUsersAuthenticationEventExecutionPlanConfigurer() {
return plan -> { /* no-op */ };
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
5.2 ObjectProvider 解决循环依赖
循环依赖问题:
在 CAS 这样的超大型框架中,Bean 之间的依赖关系非常复杂,循环依赖是一个常见问题。例如:
┌─────────────────────────────────────────────────────────────────┐
│ CAS 中的循环依赖示例 │
│ │
│ AuthenticationEventExecutionPlanConfigurer │
│ │ │
│ ├── 依赖 → AuthenticationHandler │
│ │ │ │
│ │ └── 依赖 → PrincipalResolver │
│ │ │ │
│ │ └── 依赖 → │
│ │ PersonDirectoryService │
│ │ │ │
│ │ └── 依赖 → │
│ │ AttributeRepo │
│ │ │ │
│ └──────────────────────────────────────────────┘ │
│ (可能的循环依赖) │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ObjectProvider 的解决方案:
ObjectProvider<T> 是 Spring Framework 提供的延迟注入机制。它不在 Bean 创建时立即解析依赖,而是在实际需要使用时才从容器中获取 Bean 实例。这种"延迟解析"的特性有效地打破了循环依赖链。
java
// 使用 ObjectProvider 延迟注入
@AutoConfiguration
public class CasOverlayOverrideConfiguration {
@Bean
@Primary
public AuthenticationEventExecutionPlanConfigurer
myPlanConfigurer(
// 不直接注入 AuthenticationHandler,
// 而是注入 ObjectProvider<AuthenticationHandler>
ObjectProvider<AuthenticationHandler> handlers,
ObjectProvider<PrincipalResolver> resolvers) {
return plan -> {
// 在 Lambda 执行时(而非 Bean 创建时)才解析依赖
handlers.orderedStream().forEach(handler -> {
var resolver = resolvers.getIfAvailable(
DefaultPrincipalResolver::new);
plan.registerAuthenticationHandlerWithPrincipalResolver(
handler, resolver);
});
};
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
ObjectProvider 的核心 API:
| 方法 | 说明 | 使用场景 |
|---|---|---|
getIfAvailable() | 获取 Bean,不存在则返回 null | 可选依赖 |
getIfAvailable(Supplier) | 获取 Bean,不存在则使用 Supplier 创建默认值 | 带默认值的可选依赖 |
getIfAvailable(() -> null) | 获取 Bean,不存在则返回 null(Lambda 简写) | 可选依赖的简洁写法 |
orderedStream() | 获取所有匹配 Bean 的有序流 | 需要遍历所有候选 Bean |
stream() | 获取所有匹配 Bean 的无序流 | 不关心顺序的遍历 |
get() | 获取 Bean,不存在则抛出异常 | 必须存在的依赖 |
ifAvailable(Consumer) | 如果 Bean 存在则执行 Consumer | 条件性操作 |
ObjectProvider 的内部实现原理:
ObjectProvider<T> 本质上是一个工厂接口,它的实现类持有一个对 BeanFactory 的引用。当调用 getIfAvailable() 等方法时,实现类会委托给 BeanFactory 来执行 Bean 的查找和创建。这种设计使得 ObjectProvider 可以在不立即触发 Bean 创建的情况下,将依赖关系"声明"在方法签名中。
java
// ObjectProvider 的简化实现原理
public class DefaultObjectProvider<T> implements ObjectProvider<T> {
private final BeanFactory beanFactory;
private final Class<T> type;
private final String beanName;
private final boolean optional;
@Override
public T getIfAvailable() {
try {
return beanFactory.getBean(type);
} catch (NoSuchBeanDefinitionException e) {
return null;
}
}
@Override
public T getIfAvailable(Supplier<T> defaultSupplier) {
T bean = getIfAvailable();
return bean != null ? bean : defaultSupplier.get();
}
@Override
public Stream<T> orderedStream() {
// 获取所有匹配的 Bean 名称,按 @Order 排序后逐个获取
String[] beanNames = beanFactory.getBeanNamesForType(type);
Arrays.sort(beanNames, (a, b) -> {
var orderA = beanFactory.getBeanDefinition(a).getAttribute(OrderUtils.ORDER_ATTRIBUTE);
var orderB = beanFactory.getBeanDefinition(b).getAttribute(OrderUtils.ORDER_ATTRIBUTE);
return Integer.compare(
orderA instanceof Integer ? (Integer) orderA : Ordered.LOWEST_PRECEDENCE,
orderB instanceof Integer ? (Integer) orderB : Ordered.LOWEST_PRECEDENCE);
});
return Arrays.stream(beanNames)
.map(name -> beanFactory.getBean(name, type));
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
ObjectProvider 与 Optional 的区别:
有些开发者可能会将 ObjectProvider 与 Java 8 的 Optional<T> 混淆。虽然两者都用于处理"可能不存在"的值,但它们的职责完全不同:
Optional<T>是一个值容器,用于包装一个可能为 null 的值。它不涉及 Spring 容器的任何操作。ObjectProvider<T>是一个 Bean 工厂,用于从 Spring 容器中延迟获取 Bean。它涉及 Bean 的查找、创建和生命周期管理。
在 Spring 的依赖注入场景中,应优先使用 ObjectProvider 而非 Optional,因为 ObjectProvider 能够正确处理 Bean 的作用域(如 @RefreshScope、@RequestScope 等)和代理对象。
ObjectProvider 与 @Qualifier 的组合使用:
当需要延迟注入特定名称的 Bean 时,可以将 ObjectProvider 与 @Qualifier 组合使用:
java
@Bean
public AuthenticationEventExecutionPlanConfigurer myConfigurer(
@Qualifier("myDatabaseAuthenticationHandler")
ObjectProvider<AuthenticationHandler> dbHandler,
@Qualifier("myLdapAuthenticationHandler")
ObjectProvider<AuthenticationHandler> ldapHandler) {
return plan -> {
// 延迟获取特定的认证处理器
dbHandler.ifAvailable(handler ->
plan.registerAuthenticationHandler(handler));
ldapHandler.ifAvailable(handler ->
plan.registerAuthenticationHandler(handler));
};
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
5.3 OAuth20ConfigurationContext 注入实践
问题背景:
CAS 的 OAuth 2.0 模块中,OAuth20ConfigurationContext 是一个核心配置上下文对象,它聚合了 Token 生成器、响应生成器、服务注册表等多个组件。由于这些组件之间存在复杂的交叉依赖,直接注入 OAuth20ConfigurationContext 容易触发循环依赖。
使用 ObjectProvider 的解决方案:
java
@AutoConfiguration
public class CasOverlayOAuthConfiguration {
@Bean
@Primary
@ConditionalOnMissingBean(
name = "customOAuth20ConfigurationContext")
public OAuth20ConfigurationContext customOAuth20ConfigurationContext(
// 使用 ObjectProvider 延迟注入所有依赖组件
ObjectProvider<OAuth20TokenGenerator> tokenGenerator,
ObjectProvider<OAuth20ResponseGenerator> responseGenerator,
ObjectProvider<OAuth20ServiceRegistry> serviceRegistry,
ObjectProvider<OAuth20ClientRegistrationRepository>
clientRegistrationRepo,
CasConfigurationProperties casProperties) {
return new OAuth20ConfigurationContext(
// 延迟获取 Token 生成器
tokenGenerator.getIfAvailable(
DefaultOAuth20TokenGenerator::new),
// 延迟获取响应生成器
responseGenerator.getIfAvailable(
DefaultOAuth20ResponseGenerator::new),
// 延迟获取服务注册表
serviceRegistry.getIfAvailable(),
// 延迟获取客户端注册仓库
clientRegistrationRepo.getIfAvailable(),
// CAS 配置属性(非延迟注入,因为它是简单 POJO)
casProperties
);
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
延迟注入的时序分析:
┌─────────────────────────────────────────────────────────────────┐
│ ObjectProvider 延迟注入时序图 │
│ │
│ 容器启动阶段: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. Spring 创建 CasOverlayOAuthConfiguration 实例 │ │
│ │ 2. 调用 customOAuth20ConfigurationContext() 方法 │ │
│ │ 3. Spring 注入 ObjectProvider 代理对象(非实际 Bean) │ │
│ │ 4. 方法返回 OAuth20ConfigurationContext 实例 │ │
│ │ → 此时 TokenGenerator 等组件尚未被创建 │ │
│ │ → 但 OAuth20ConfigurationContext 已经被注册到容器中 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 首次使用阶段: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 5. 某个组件调用 context.getTokenGenerator() │ │
│ │ 6. ObjectProvider.getIfAvailable() 被调用 │ │
│ │ 7. Spring 容器此时才创建 TokenGenerator Bean │ │
│ │ 8. 返回 TokenGenerator 实例 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 优势:打破了创建时的循环依赖链 │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
5.4 最佳实践与注意事项
@Primary 使用原则:
- 最小化 @Primary 的使用范围:只在确实需要覆盖 CAS 默认行为的场景下使用
@Primary,不要在所有自定义 Bean 上都添加此注解。 - 配合 @ConditionalOnMissingBean 使用:
@Primary和@ConditionalOnMissingBean的组合使用可以提供双重保障——前者确保优先级,后者防止重复创建。 - 添加文档注释:在每个使用
@Primary的 Bean 定义上添加清晰的注释,说明覆盖的目的和影响范围。
ObjectProvider 使用原则:
- 可选依赖优先使用 ObjectProvider:如果一个依赖不是必须的(即容器中可能不存在该 Bean),应使用
ObjectProvider而非@Autowired(required = false)。 - 循环依赖场景必须使用 ObjectProvider:当检测到循环依赖时,将其中一个注入点改为
ObjectProvider是最简洁的解决方案。 - 提供合理的默认值:使用
getIfAvailable(Supplier)方法为可选依赖提供默认值,避免运行时NullPointerException。 - 注意 orderedStream 的性能:
orderedStream()需要对所有候选 Bean 进行排序,在候选 Bean 数量较多时可能有性能开销。如果不需要排序,使用stream()代替。
综合最佳实践示例:
java
@AutoConfiguration(after = CasCoreAuthenticationConfiguration.class)
public class CasOverlayOverrideConfiguration {
/**
* 自定义认证处理器。
*
* 设计要点:
* - @Primary:确保在多候选者场景下被优先选择
* - @RefreshScope:支持运行时动态刷新
* - @ConditionalOnMissingBean:防止重复创建
* - 方法参数使用具体类型而非 ObjectProvider:
* 因为 CasConfigurationProperties 是简单 POJO,无循环依赖风险
*/
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(name = "myAuthenticationHandler")
public AuthenticationHandler myAuthenticationHandler(
CasConfigurationProperties casProperties) {
return new MyAuthenticationHandler(
casProperties.getCustom().getAuthn());
}
/**
* 自定义认证事件执行计划配置器。
*
* 设计要点:
* - 使用 ObjectProvider 延迟注入 AuthenticationHandler 和 PrincipalResolver,
* 避免与 CAS 内部组件的循环依赖
* - 使用 orderedStream() 确保处理器按 @Order 注解排序
* - 使用 getIfAvailable() 为 PrincipalResolver 提供默认值
*/
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(
name = "myAuthenticationEventExecutionPlanConfigurer")
public AuthenticationEventExecutionPlanConfigurer
myAuthenticationEventExecutionPlanConfigurer(
ObjectProvider<AuthenticationHandler> handlers,
ObjectProvider<PrincipalResolver> resolvers) {
return plan -> {
plan.getAuthenticationHandlers().clear();
handlers.orderedStream()
.forEach(handler -> {
plan.registerAuthenticationHandler(handler);
plan.registerAuthenticationHandlerWithPrincipalResolver(
handler,
resolvers.getIfAvailable(
DefaultPrincipalResolver::new));
});
};
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
第六章 @RefreshScope 动态刷新机制
@RefreshScope 是 Spring Cloud 提供的注解,用于标记需要支持运行时动态刷新的 Bean。在 CAS 7.3 的 Overlay 项目中,@RefreshScope 与认证处理器的结合使用,使得在不重启应用的情况下更新认证策略成为可能。本章将深入解析 @RefreshScope 的工作原理,并探讨其在 CAS 认证场景下的应用和限制。
6.1 @RefreshScope 工作原理
核心机制:
@RefreshScope 的底层实现基于 Spring 的 ScopedProxy 机制。当一个 Bean 被标记为 @RefreshScope 时,Spring 容器会为它创建一个 CGLIB 代理对象。这个代理对象缓存了目标 Bean 的实例引用,当收到刷新事件时,代理会清除缓存,使得下一次方法调用触发目标 Bean 的重新创建。
┌─────────────────────────────────────────────────────────────────┐
│ @RefreshScope 工作原理 │
│ │
│ Spring 容器 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ RefreshScope Cache │ │
│ │ ┌────────────────────────────────────────────────────┐ │ │
│ │ │ "myAuthenticationHandler" → HandlerInstance_v1 │ │ │
│ │ └────────────────────────────────────────────────────┘ │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ CGLIB Proxy (myAuthenticationHandler) │ │
│ │ │ │
│ │ authenticate(credential) { │ │
│ │ // 1. 从 RefreshScope Cache 获取目标实例 │ │
│ │ var handler = cache.get("myAuthenticationHandler"); │ │
│ │ // 2. 调用目标实例的方法 │ │
│ │ return handler.authenticate(credential); │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 刷新事件触发时: │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. ContextRefresher.refresh() 被调用 │ │
│ │ 2. RefreshScope Cache 被清空 │ │
│ │ 3. 下一次方法调用时,重新创建目标 Bean 实例 │ │
│ │ → HandlerInstance_v2(使用新的配置) │ │
│ └──────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
刷新触发方式:
在 CAS 7.3 中,可以通过以下方式触发 @RefreshScope Bean 的刷新:
- Actuator 端点:
bash
# 发送 POST 请求到 /actuator/refresh 端点
curl -X POST http://localhost:8080/cas/actuator/refresh1
2
2
使用 Actuator 端点刷新时,需要注意以下几点:
- 需要在
application.properties中启用 refresh 端点:management.endpoints.web.exposure.include=refresh - 在生产环境中,应通过 Spring Security 保护 Actuator 端点,防止未授权的刷新操作
- 刷新操作是异步的,返回的响应中包含了被刷新的 Bean 名称列表
- Spring Cloud Bus(分布式环境):
bash
# 通过消息总线广播刷新事件
curl -X POST http://localhost:8080/cas/actuator/busrefresh1
2
2
在 CAS 集群部署场景中,如果多个 CAS 节点需要同时刷新配置,可以使用 Spring Cloud Bus。Spring Cloud Bus 通过消息中间件(如 RabbitMQ、Kafka)将刷新事件广播到所有节点,实现集群范围的配置同步。
yaml
# Spring Cloud Bus 配置示例(使用 RabbitMQ)
spring:
rabbitmq:
host: rabbitmq-host
port: 5672
username: guest
password: guest
cloud:
bus:
enabled: true
destination: cas-refresh-bus1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
- 编程方式:
java
@Autowired
private ContextRefresher contextRefresher;
public void refreshAuthenticationHandlers() {
// 刷新所有 @RefreshScope Bean
var keys = contextRefresher.refresh();
log.info("Refreshed configuration keys: {}", keys);
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
编程方式适用于需要将刷新操作嵌入到业务逻辑中的场景,例如在管理后台点击"重载认证配置"按钮时触发刷新。
@RefreshScope 与 Spring Cloud Config 的协同:
@RefreshScope 通常与 Spring Cloud Config 配合使用。Spring Cloud Config 负责从外部配置中心(如 Git 仓库、Vault、Consul 等)获取最新配置,@RefreshScope 负责在配置变更后重新创建受影响的 Bean。两者协同工作的流程如下:
┌─────────────────────────────────────────────────────────────────┐
│ @RefreshScope 与 Spring Cloud Config 协同流程 │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Config Server│ │ Config Client│ │ CAS Server │ │
│ │ (Git/Vault) │ │ (CAS 节点) │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │
│ 1. 管理员更新 Git 仓库中的配置文件 │ │
│ │ │ │ │
│ ▼ │ │ │
│ 2. /actuator/refresh 请求到达 Config Client │ │
│ │ │ │ │
│ ▼ ▼ │ │
│ 3. Config Client 从 Config Server 拉取最新配置 │ │
│ │ │ │ │
│ │ ▼ │ │
│ 4. Spring Environment 被更新 │ │
│ │ │ │ │
│ │ ▼ │ │
│ 5. RefreshScope 清除缓存,标记所有 @RefreshScope │ │
│ Bean 为"需要重建" │ │
│ │ │ │ │
│ │ │ ▼ │
│ 6. 下一次认证请求到达 CAS Server │ │
│ │ │ │ │
│ │ │ ▼ │
│ 7. CGLIB Proxy 检测到 Bean 需要重建 │ │
│ │ │ │ │
│ │ │ ▼ │
│ 8. 使用新的配置值创建新的 Bean 实例 │ │
│ │ │ │ │
│ │ │ ▼ │
│ 9. 使用新配置执行认证逻辑 │ │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
@RefreshScope 与 @Configuration 的关系:
需要注意的是,@RefreshScope 不仅可以标记在 @Bean 方法上,也可以标记在 @Configuration 类上。当标记在类上时,该类中所有通过 @Bean 方法定义的 Bean 都会被纳入 RefreshScope 管理。
java
// 方式一:标记在 @Bean 方法上(推荐,更精确)
@AutoConfiguration
public class CasOverlayOverrideConfiguration {
@Bean
@RefreshScope // 只有这个 Bean 支持刷新
public AuthenticationHandler myHandler() { ... }
@Bean // 这个 Bean 不支持刷新
public PrincipalResolver myResolver() { ... }
}
// 方式二:标记在 @Configuration 类上(所有 @Bean 都支持刷新)
@RefreshScope // 类级别标记
@AutoConfiguration
public class CasOverlayOverrideConfiguration {
@Bean // 支持刷新
public AuthenticationHandler myHandler() { ... }
@Bean // 也支持刷新
public PrincipalResolver myResolver() { ... }
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
6.2 认证处理器的运行时刷新
场景描述:
假设企业使用数据库认证,认证查询 SQL 配置在 application.yml 中。当需要修改认证逻辑(例如添加新的认证条件)时,传统方式需要修改配置文件并重启 CAS 服务。使用 @RefreshScope 后,只需修改配置文件并触发刷新,即可在不中断用户会话的情况下更新认证逻辑。
配置示例:
yaml
# application.yml
cas:
custom:
authn:
database:
enabled: true
url: jdbc:mysql://db-host:3306/cas_auth
username: cas_user
password: ${DB_PASSWORD}
authentication-query: >
SELECT password FROM users
WHERE username = ? AND status = 'ACTIVE'
password-encoder:
type: BCRYPT
encoding-algorithm: BCrypt1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
认证处理器实现:
java
public class MyDatabaseAuthenticationHandler
extends AbstractUsernamePasswordAuthenticationHandler {
private final String authQuery;
private final DataSource dataSource;
private final PasswordEncoder passwordEncoder;
// 构造函数接收配置参数
public MyDatabaseAuthenticationHandler(
String url, String username, String password,
String authQuery, PasswordEncoder passwordEncoder) {
this.dataSource = createDataSource(url, username, password);
this.authQuery = authQuery;
this.passwordEncoder = passwordEncoder;
}
@Override
protected AuthenticationHandlerExecutionResult doAuthentication(
UsernamePasswordCredential credential)
throws GeneralSecurityException, PreventedException {
var username = credential.getUsername();
var rawPassword = credential.getPassword();
// 查询数据库获取加密后的密码
var encodedPassword = queryPassword(username);
if (encodedPassword == null) {
throw new AccountNotFoundException(username);
}
// 验证密码
if (!passwordEncoder.matches(rawPassword, encodedPassword)) {
throw new FailedLoginException("Invalid credentials");
}
// 返回认证结果
return createHandlerResult(credential,
new DefaultPrincipalFactory().createPrincipal(username));
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
刷新流程:
┌─────────────────────────────────────────────────────────────────┐
│ 认证处理器运行时刷新流程 │
│ │
│ 步骤1:修改配置文件 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ vi application.yml │ │
│ │ # 修改 authentication-query │ │
│ │ # 添加新的认证条件 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 步骤2:触发刷新 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ curl -X POST http://localhost:8080/cas/actuator/refresh │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 步骤3:Spring Cloud 重新加载配置 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ Environment 被更新 │ │
│ │ CasConfigurationProperties 被重新绑定 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 步骤4:RefreshScope 清除缓存 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ myAuthenticationHandler 的缓存实例被清除 │ │
│ │ myAuthenticationEventExecutionPlanConfigurer 被清除 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 步骤5:下一次认证请求触发重新创建 │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 用户提交认证请求 │ │
│ │ → CGLIB Proxy 检测到缓存已清除 │ │
│ │ → 使用新的 CasConfigurationProperties 创建新的 Handler │ │
│ │ → 使用新的认证查询 SQL 执行认证 │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
│ 注意:已有的 TGT/ST 不受影响,新认证请求使用新配置 │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
6.3 刷新范围与限制
@RefreshScope 的适用范围:
@RefreshScope 并非万能的。它有其特定的适用范围和限制条件:
适用场景:
- 配置变更:当认证策略的配置参数发生变化时(如数据库连接信息、查询 SQL、超时时间等),
@RefreshScope可以确保使用最新的配置值。 - 认证策略切换:当需要在多种认证策略之间动态切换时(如从数据库认证切换到 LDAP 认证),可以通过刷新实现。
- 维护窗口:在计划维护期间,可以临时切换到"维护模式"认证处理器,拒绝所有认证请求并返回维护提示。
不适用场景:
- 代码变更:
@RefreshScope只能刷新 Bean 的配置,不能刷新 Bean 的代码。如果修改了认证处理器的 Java 代码,仍然需要重新构建和部署。 - Schema 变更:如果数据库表结构发生变化,仅刷新认证处理器可能不够,还需要确保数据源连接池也被正确刷新。
- 依赖关系变更:如果刷新某个 Bean 导致其依赖的其他 Bean 也需要更新,需要确保这些 Bean 也被标记为
@RefreshScope。
刷新的副作用:
┌─────────────────────────────────────────────────────────────────┐
│ @RefreshScope 刷新的副作用 │
│ │
│ 1. 状态丢失 │
│ ├── 刷新后,Bean 的所有实例变量都会被重置 │
│ ├── 如果 Bean 维护了运行时状态(如计数器、缓存),这些状态会丢失│
│ └── 解决方案:将运行时状态存储在外部(如 Redis) │
│ │
│ 2. 瞬时不可用 │
│ ├── 刷新过程中,Bean 正在被重新创建 │
│ ├── 如果此时有并发请求,可能会遇到短暂的延迟 │
│ └── 解决方案:使用预热策略,在刷新后主动触发一次 Bean 初始化 │
│ │
│ 3. 认证链路不一致 │
│ ├── 如果只刷新了部分 Bean,可能导致认证链路状态不一致 │
│ ├── 例如:刷新了 Handler 但未刷新 Configurer │
│ └── 解决方案:确保认证链路中的所有 Bean 都标记 @RefreshScope │
│ │
│ 4. 内存压力 │
│ ├── 刷新不是立即回收旧实例,而是等待 GC │
│ ├── 如果频繁刷新,可能导致内存中有多个旧实例等待回收 │
│ └── 解决方案:控制刷新频率,避免短时间内多次刷新 │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
6.4 生产环境应用场景
场景一:认证策略热切换
在企业合并或组织架构调整时,可能需要将认证源从系统 A 切换到系统 B。使用 @RefreshScope 可以实现零停机切换:
java
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(name = "switchableAuthenticationHandler")
public AuthenticationHandler switchableAuthenticationHandler(
CasConfigurationProperties casProperties) {
var authnConfig = casProperties.getCustom().getAuthn();
return BeanSupplier.of(AuthenticationHandler.class)
.when(BeanSupplier.Condition.given(
"database".equals(authnConfig.getActiveStrategy())))
.supplier(() -> new DatabaseAuthenticationHandler(
authnConfig.getDatabase()))
.when(BeanSupplier.Condition.given(
"ldap".equals(authnConfig.getActiveStrategy())))
.supplier(() -> new LdapAuthenticationHandler(
authnConfig.getLdap()))
.otherwise(() -> new RejectAllAuthenticationHandler())
.get();
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
配置切换:
yaml
# 切换前
cas:
custom:
authn:
active-strategy: database
# 切换后(修改配置并触发刷新)
cas:
custom:
authn:
active-strategy: ldap1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
场景二:紧急认证封锁
在安全事件响应中,可能需要紧急封锁所有认证请求。使用 @RefreshScope 可以在秒级完成封锁:
java
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(name = "emergencyAuthenticationHandler")
public AuthenticationHandler emergencyAuthenticationHandler(
CasConfigurationProperties casProperties) {
var emergency = casProperties.getCustom().getEmergency();
if (emergency.isLockdownEnabled()) {
return new RejectAllAuthenticationHandler(
emergency.getLockdownMessage());
}
return new DatabaseAuthenticationHandler(
casProperties.getCustom().getAuthn().getDatabase());
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
紧急封锁操作:
bash
# 步骤1:修改配置,启用封锁模式
# cas.custom.emergency.lockdown-enabled=true
# cas.custom.emergency.lockdown-message=系统维护中,请稍后再试
# 步骤2:触发刷新
curl -X POST http://localhost:8080/cas/actuator/refresh
# 步骤3:所有新的认证请求将被拒绝,显示维护提示1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
场景三:多数据源动态切换
在读写分离或分库分表场景下,可能需要动态切换认证数据源:
java
@Bean
@Primary
@RefreshScope
@ConditionalOnMissingBean(name = "dynamicDataSource")
public DataSource dynamicDataSource(CasConfigurationProperties props) {
var dsConfig = props.getCustom().getDataSource();
return new AbstractRoutingDataSource() {
@Override
protected Object determineCurrentLookupKey() {
// 根据配置动态选择数据源
return dsConfig.getActiveTarget();
}
@PostConstruct
public void init() {
var targetDataSources = new HashMap<Object, Object>();
dsConfig.getTargets().forEach((name, target) -> {
targetDataSources.put(name, createDataSource(target));
});
super.setTargetDataSources(targetDataSources);
super.setDefaultTargetDataSource(
createDataSource(dsConfig.getPrimary()));
super.afterPropertiesSet();
}
};
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
第七章 javax 到 jakarta 命名空间迁移
CAS 7.3 基于 Spring Boot 3.x 和 Jakarta EE 10 构建,最显著的破坏性变更之一就是 Java 命名空间从 javax.* 到 jakarta.* 的全面迁移。这一变更影响范围广泛,涉及所有使用 Java EE API 的自定义组件。本章将系统性地梳理迁移的背景、影响范围和操作指南。
7.1 迁移背景与影响范围
迁移背景:
2017 年,Oracle 将 Java EE 的管理权移交给了 Eclipse 基金会。由于 "Java" 商标归 Oracle 所有,Eclipse 基金会无法继续使用 "Java EE" 这个名称,因此将项目更名为 "Jakarta EE"。从 Jakarta EE 9 开始,所有 Java 包名从 javax.* 变更为 jakarta.*。
Spring Boot 3.x 基于 Jakarta EE 10,因此全面采用了 jakarta.* 命名空间。这意味着所有依赖 Java EE API 的代码都需要进行包名替换。
影响范围总览:
┌─────────────────────────────────────────────────────────────────┐
│ javax → jakarta 迁移影响范围 │
│ │
│ ┌────────────────────────────────────────────────────────────┐ │
│ │ Servlet API │ │
│ │ javax.servlet.* → jakarta.servlet.* │ │
│ │ javax.servlet.http.* → jakarta.servlet.http.* │ │
│ │ 影响:Filter, Servlet, HttpServlet, HttpServletRequest... │ │
│ ├────────────────────────────────────────────────────────────┤ │
│ │ Validation API │ │
│ │ javax.validation.* → jakarta.validation.* │ │
│ │ 影响:@Valid, @NotNull, Validator, ConstraintValidator... │ │
│ ├────────────────────────────────────────────────────────────┤ │
│ │ Persistence API (JPA) │ │
│ │ javax.persistence.* → jakarta.persistence.* │ │
│ │ 影响:@Entity, EntityManager, @Transactional... │ │
│ ├────────────────────────────────────────────────────────────┤ │
│ │ Mail API │ │
│ │ javax.mail.* → jakarta.mail.* │ │
│ │ 影响:MimeMessage, Session, Transport... │ │
│ ├────────────────────────────────────────────────────────────┤ │
│ │ Annotation API │ │
│ │ javax.annotation.* → jakarta.annotation.* │ │
│ │ 影响:@Resource, @PostConstruct, @PreDestroy... │ │
│ ├────────────────────────────────────────────────────────────┤ │
│ │ Security API │ │
│ │ javax.security.* → jakarta.security.* │ │
│ │ 影响:Principal, Subject, AuthPermission... │ │
│ ├────────────────────────────────────────────────────────────┤ │
│ │ WebSocket API │ │
│ │ javax.websocket.* → jakarta.websocket.* │ │
│ │ 影响:ServerEndpoint, OnMessage, Session... │ │
│ └────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
7.2 Servlet API 迁移
CAS Overlay 项目中最常见的迁移场景是 Servlet API。 CAS 的自定义 Filter、Controller、View Renderer 等组件通常直接依赖 Servlet API。
Filter 迁移:
java
// ===== 迁移前 (CAS 5.3 / 6.6) =====
import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyCustomFilter implements Filter {
@Override
public void init(FilterConfig filterConfig)
throws ServletException { /* ... */ }
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 自定义过滤逻辑
String token = httpRequest.getHeader("X-Auth-Token");
if (token != null && validateToken(token)) {
chain.doFilter(request, response);
} else {
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
@Override
public void destroy() { /* ... */ }
}
// ===== 迁移后 (CAS 7.3) =====
import jakarta.servlet.Filter;
import jakarta.servlet.FilterChain;
import jakarta.servlet.FilterConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class MyCustomFilter implements Filter {
@Override
public void init(FilterConfig filterConfig)
throws ServletException { /* ... */ }
@Override
public void doFilter(ServletRequest request,
ServletResponse response,
FilterChain chain)
throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
HttpServletResponse httpResponse = (HttpServletResponse) response;
// 自定义过滤逻辑(代码逻辑不变,仅包名变更)
String token = httpRequest.getHeader("X-Auth-Token");
if (token != null && validateToken(token)) {
chain.doFilter(request, response);
} else {
httpResponse.sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
@Override
public void destroy() { /* ... */ }
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
Controller 迁移:
java
// ===== 迁移前 (CAS 5.3 / 6.6) =====
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
@Controller("myCustomController")
public class MyCustomController {
@GetMapping("/custom/api/userinfo")
@ResponseBody
public Map<String, Object> getUserInfo(
HttpServletRequest request,
HttpServletResponse response) {
HttpSession session = request.getSession(false);
// ...
}
}
// ===== 迁移后 (CAS 7.3) =====
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.servlet.http.HttpSession;
@Controller("myCustomController")
public class MyCustomController {
@GetMapping("/custom/api/userinfo")
@ResponseBody
public Map<String, Object> getUserInfo(
HttpServletRequest request,
HttpServletResponse response) {
HttpSession session = request.getSession(false);
// ...
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
View Renderer 迁移:
java
// ===== 迁移前 (CAS 5.3 / 6.6) =====
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
public class MyCustomViewRenderer implements ViewRenderer {
@Override
public void render(Map<String, Object> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 自定义视图渲染逻辑
}
}
// ===== 迁移后 (CAS 7.3) =====
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
public class MyCustomViewRenderer implements ViewRenderer {
@Override
public void render(Map<String, Object> model,
HttpServletRequest request,
HttpServletResponse response) throws Exception {
// 自定义视图渲染逻辑(代码逻辑不变,仅包名变更)
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
7.3 影响的自定义组件清单
在 CAS Overlay 项目中,以下类型的自定义组件通常需要执行 javax 到 jakarta 的迁移:
| 组件类型 | 典型类名 | 涉及的 API | 迁移复杂度 |
|---|---|---|---|
| 自定义 Filter | MyAuthFilter, MyCorsFilter | javax.servlet.* | 低(仅包名替换) |
| 自定义 Controller | MyApiController, MyCallbackController | javax.servlet.http.* | 低(仅包名替换) |
| 自定义 View Renderer | MyPdfViewRenderer, MyExcelViewRenderer | javax.servlet.* | 低(仅包名替换) |
| 自定义 Ticket 组件 | MyTicketFactory, MyTicketValidator | javax.annotation.* | 低(仅包名替换) |
| 自定义认证处理器 | MyDatabaseAuthHandler, MyLdapAuthHandler | 通常不涉及 javax | 无 |
| 自定义 PrincipalResolver | MyPrincipalResolver | 通常不涉及 javax | 无 |
| 自定义密码编码器 | MyPasswordEncoder | 通常不涉及 javax | 无 |
| 自定义事件监听器 | MyAuthEventListener | 通常不涉及 javax | 无 |
| 邮件通知组件 | MyMailNotifier | javax.mail.* | 中(API 可能有变化) |
| JPA 数据访问组件 | MyUserRepository | javax.persistence.* | 中(API 可能有变化) |
| WebSocket 组件 | MyWebSocketHandler | javax.websocket.* | 中(API 可能有变化) |
迁移复杂度说明:
- 低复杂度:仅需将
javax.替换为jakarta.,代码逻辑无需任何修改。 - 中复杂度:除了包名替换外,某些 API 的方法签名或行为可能发生了变化,需要逐一验证。
- 高复杂度:涉及底层 API 变更,可能需要重新设计部分逻辑。
7.4 迁移检查清单
以下是一份完整的 javax 到 jakarta 迁移检查清单,可用于 CAS Overlay 项目的版本升级:
阶段一:代码扫描
- [ ] 使用 IDE 的全局搜索功能,搜索所有
import javax.servlet语句 - [ ] 搜索所有
import javax.annotation语句 - [ ] 搜索所有
import javax.mail语句 - [ ] 搜索所有
import javax.persistence语句 - [ ] 搜索所有
import javax.validation语句 - [ ] 搜索所有
import javax.websocket语句 - [ ] 搜索所有
import javax.security语句 - [ ] 搜索所有字符串中包含
javax.的引用(如 XML 配置、反射调用等)
阶段二:依赖更新
- [ ] 更新
build.gradle中的依赖声明,确保使用 Jakarta EE 10 兼容的版本 - [ ] 检查第三方库是否提供了 Jakarta EE 兼容版本
- [ ] 移除所有显式依赖
javax.*的第三方库 - [ ] 确认 Spring Boot 3.x 的依赖管理已正确覆盖传递依赖
groovy
// build.gradle 依赖更新示例
dependencies {
// CAS 7.3 核心依赖(已包含 Jakarta EE 10)
implementation "org.apereo.cas:cas-server-webapp:${casVersion}"
// 如果需要显式声明 Servlet API(通常不需要,CAS 已包含)
// 迁移前:compileOnly 'javax.servlet:javax.servlet-api:4.0.1'
// 迁移后:compileOnly 'jakarta.servlet:jakarta.servlet-api:6.0.0'
// 如果需要邮件功能
// 迁移前:implementation 'com.sun.mail:javax.mail:1.6.2'
// 迁移后:implementation 'com.sun.mail:jakarta.mail:2.1.0'
// 如果需要 JPA
// 迁移前:implementation 'javax.persistence:javax.persistence-api:2.2'
// 迁移后:implementation 'jakarta.persistence:jakarta.persistence-api:3.1.0'
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
阶段三:代码修改
- [ ] 将所有
import javax.servlet.*替换为import jakarta.servlet.* - [ ] 将所有
import javax.annotation.*替换为import jakarta.annotation.* - [ ] 将所有
import javax.mail.*替换为import jakarta.mail.* - [ ] 将所有
import javax.persistence.*替换为import jakarta.persistence.* - [ ] 将所有
import javax.validation.*替换为import jakarta.validation.* - [ ] 检查并更新 XML 配置文件中的
javax.引用 - [ ] 检查并更新模板文件(如 Thymeleaf 模板)中的
javax.引用 - [ ] 检查并更新
application.properties/application.yml中的javax.引用
阶段四:编译验证
- [ ] 执行
gradle compileJava确保编译通过 - [ ] 检查编译警告中是否有遗留的
javax.引用 - [ ] 执行
gradle test确保单元测试通过 - [ ] 执行
gradle integrationTest确保集成测试通过
阶段五:运行时验证
- [ ] 启动 CAS 服务,检查启动日志中是否有
ClassNotFoundException: javax.* - [ ] 执行完整的认证流程测试
- [ ] 测试所有自定义 Filter 的功能
- [ ] 测试所有自定义 Controller 的端点
- [ ] 测试所有自定义 View Renderer 的输出
- [ ] 如果使用了邮件功能,测试邮件发送
- [ ] 如果使用了 WebSocket,测试 WebSocket 连接
阶段六:回归测试
- [ ] 执行完整的端到端测试
- [ ] 验证所有 CAS 协议(CAS、SAML、OAuth2.0、OIDC)的功能
- [ ] 验证票据(TGT、ST、PGT、PT)的签发和验证
- [ ] 验证单点登出(SLO)功能
- [ ] 验证代理认证(Proxy Authentication)功能
- [ ] 进行性能基准测试,确认无性能退化
自动化迁移脚本:
对于大型项目,可以使用以下脚本辅助完成包名替换:
bash
#!/bin/bash
# javax-to-jakarta-migration.sh
# 在项目根目录下执行
echo "=== javax → jakarta 迁移脚本 ==="
echo "请确保已备份项目代码后再执行此脚本。"
echo ""
# 统计需要替换的文件数量
JAVA_COUNT=$(find src -name "*.java" | wc -l)
XML_COUNT=$(find src -name "*.xml" | wc -l)
YML_COUNT=$(find src -name "*.yml" -o -name "*.yaml" | wc -l)
PROP_COUNT=$(find src -name "*.properties" | wc -l)
echo "将扫描以下文件:"
echo " Java 文件: $JAVA_COUNT"
echo " XML 文件: $XML_COUNT"
echo " YAML 文件: $YML_COUNT"
echo " Properties 文件: $PROP_COUNT"
echo ""
# 查找所有 Java 文件并替换 javax.servlet → jakarta.servlet
find src -name "*.java" -exec sed -i \
's/import javax\.servlet\./import jakarta.servlet./g' {} +
echo "[✓] javax.servlet → jakarta.servlet"
# 替换 javax.annotation → jakarta.annotation
find src -name "*.java" -exec sed -i \
's/import javax\.annotation\./import jakarta.annotation./g' {} +
echo "[✓] javax.annotation → jakarta.annotation"
# 替换 javax.mail → jakarta.mail
find src -name "*.java" -exec sed -i \
's/import javax\.mail\./import jakarta.mail./g' {} +
echo "[✓] javax.mail → jakarta.mail"
# 替换 javax.persistence → jakarta.persistence
find src -name "*.java" -exec sed -i \
's/import javax\.persistence\./import jakarta.persistence./g' {} +
echo "[✓] javax.persistence → jakarta.persistence"
# 替换 javax.validation → jakarta.validation
find src -name "*.java" -exec sed -i \
's/import javax\.validation\./import jakarta.validation./g' {} +
echo "[✓] javax.validation → jakarta.validation"
# 替换 javax.websocket → jakarta.websocket
find src -name "*.java" -exec sed -i \
's/import javax\.websocket\./import jakarta.websocket./g' {} +
echo "[✓] javax.websocket → jakarta.websocket"
# 替换 XML 文件中的引用
find src -name "*.xml" -exec sed -i \
's/javax\.servlet\./jakarta.servlet./g' {} +
find src -name "*.xml" -exec sed -i \
's/javax\.annotation\./jakarta.annotation./g' {} +
echo "[✓] XML 文件中的 javax 引用已替换"
# 替换 YAML/Properties 文件中的引用
find src \( -name "*.yml" -o -name "*.yaml" -o -name "*.properties" \) \
-exec sed -i 's/javax\./jakarta./g' {} +
echo "[✓] 配置文件中的 javax 引用已替换"
echo ""
echo "=== 迁移完成 ==="
echo "请执行以下步骤验证迁移结果:"
echo " 1. gradle compileJava"
echo " 2. gradle test"
echo " 3. 手动检查 import 语句是否正确"
echo " 4. 启动 CAS 服务进行功能验证"1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
使用 OpenRewrite 进行自动化迁移:
对于更复杂的迁移场景,推荐使用 OpenRewrite 工具。OpenRewrite 是一个开源的代码重构工具,提供了专门的 javax 到 jakarta 迁移配方,能够处理不仅仅是包名替换的复杂场景。
groovy
// build.gradle 中添加 OpenRewrite 插件
plugins {
id 'org.openrewrite.rewrite' version '6.25.0'
}
rewrite {
activeRecipe('org.openrewrite.java.migrate.jakarta.JavaxMigrationToJakarta')
}
dependencies {
rewrite('org.openrewrite.recipe:rewrite-migrate-java:2.25.0')
}1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
执行迁移:
bash
# 预览迁移变更(不修改文件)
gradle rewriteDryRun
# 执行迁移(修改文件)
gradle rewriteRun1
2
3
4
5
2
3
4
5
OpenRewrite 的优势在于它不仅能够处理简单的包名替换,还能够:
- 更新 Maven/Gradle 依赖坐标
- 调整 XML 命名空间声明
- 处理反射调用中的字符串引用
- 识别并修复 API 签名变更
CAS 7.3 特有的迁移注意事项:
在 CAS 7.3 的 Overlay 项目中,除了标准的 javax 到 jakarta 迁移外,还需要注意以下 CAS 特有的变更:
CAS 的
CasServlet类已迁移到 jakarta 命名空间:如果你的自定义 Filter 或 Servlet 直接引用了 CAS 的CasServlet类,需要更新 import 语句。Thymeleaf 模板中的表达式:CAS 使用 Thymeleaf 作为模板引擎,如果自定义模板中引用了 javax 命名空间的类(如通过
T(javax.servlet.http.HttpServletRequest)),需要更新为T(jakarta.servlet.http.HttpServletRequest)。CAS 的
AbstractConfiguration类:CAS 内部的AbstractConfiguration类在 7.3 中也进行了重构,部分方法签名发生了变化。如果你的自定义配置类继承了 CAS 的AbstractConfiguration,需要检查是否需要调整。Gradle 构建脚本中的依赖声明:CAS 7.3 的 Gradle 构建脚本使用
casServer依赖块来声明 CAS 模块依赖。确保所有依赖的 CAS 模块版本一致,避免混合使用 CAS 6.x 和 7.x 的模块。
总结与展望
本文从 CAS Bean 注册机制的演进全景出发,深入解析了 Spring Boot 3.x 的 AutoConfiguration 机制、CAS 7.3 的 Bean 覆盖技巧、认证处理器注册模式的三代演进、@Primary 与 ObjectProvider 的最佳实践、@RefreshScope 动态刷新机制,以及 javax 到 jakarta 命名空间的迁移指南。
核心要点回顾:
Bean 注册机制的演进方向是"从隐式到显式,从静态到动态,从脆弱到健壮"。CAS 5.3 的
@Component自动扫描方式简单直接但缺乏控制力;CAS 6.6 的@Configuration+@ConditionalOnMissingBean方式提供了条件保护;CAS 7.3 的@AutoConfiguration+@Primary+ObjectProvider方式实现了精确的优先级控制和循环依赖解决。每一次演进都不是对前代的简单否定,而是在新的技术约束下寻求更优的解决方案。Spring Boot 3.x 的 AutoConfiguration 机制是 CAS 7.3 定制化的基石。
imports文件替代spring.factories、@AutoConfiguration注解的before/after顺序控制、@ConditionalOnMissingBean的条件保护——这些机制共同构成了一个强大而灵活的 Bean 覆盖体系。理解这些机制的底层原理,是进行 CAS 深度定制的前提。@Primary和ObjectProvider是 CAS 7.3 中解决 Bean 冲突和循环依赖的两大核心工具。@Primary通过优先级标记确保自定义 Bean 被优先选择;ObjectProvider通过延迟注入打破循环依赖链。两者的组合使用是 CAS 7.3 Overlay 项目的最佳实践。但需要注意的是,@Primary的作用范围是全局的,使用时应谨慎评估其影响范围。@RefreshScope为 CAS 认证策略的运行时动态刷新提供了可能,但其适用范围有限,且存在状态丢失、瞬时不可用等副作用,需要在生产环境中谨慎使用。建议将@RefreshScope与 Spring Cloud Config、Spring Cloud Bus 配合使用,实现集群范围的配置同步。javax 到 jakarta 的命名空间迁移是 CAS 7.3 升级中最广泛的破坏性变更,但迁移操作本身主要是机械式的包名替换,复杂度可控。关键在于全面扫描和充分测试。推荐使用 OpenRewrite 等自动化工具辅助迁移。
包路径覆盖技巧是 CAS 7.3 中最精妙的覆盖策略之一。通过将自定义配置类放在
org.apereo.cas.config包下,利用 CAS 内部的组件扫描和@ConditionalOnMissingBean机制,可以在不修改 CAS 源码的情况下,以最简洁的方式覆盖 CAS 的默认行为。
CAS 7.3 Overlay 项目配置速查表:
以下是 CAS 7.3 Overlay 项目中常用的 Spring Boot 3.x 配置项:
yaml
# application.yml - CAS 7.3 Overlay 核心配置
spring:
# 允许 Bean 覆盖(自定义 Bean 覆盖 CAS 默认 Bean 所需)
main:
allow-bean-definition-overriding: true
# 路径匹配策略(CAS 的某些端点需要 Ant 风格的路径匹配)
mvc:
pathmatch:
matching-strategy: ant_path_matcher
# 国际化配置
web:
locale-resolver: fixed
locale: zh_CN
# Actuator 端点配置(用于 @RefreshScope 刷新和健康检查)
management:
endpoint:
refresh:
enabled: true
health:
enabled: true
conditions:
enabled: true
endpoints:
web:
exposure:
include: refresh,health,conditions,info1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
版本升级决策矩阵:
| 当前版本 | 目标版本 | 升级难度 | 关键变更 | 建议工期 |
|---|---|---|---|---|
| CAS 5.3.x | CAS 6.6.x | 中等 | spring.factories、@Configuration、@ConditionalOnMissingBean | 2-4 周 |
| CAS 6.6.x | CAS 7.3.x | 较高 | imports 文件、@AutoConfiguration、javax→jakarta、Java 21 | 4-8 周 |
| CAS 5.3.x | CAS 7.3.x | 高 | 包含上述所有变更,建议分两步升级 | 6-12 周 |
展望未来:
CAS 作为企业级 SSO 领域最成熟的开源方案,其架构演进始终紧跟 Spring Boot 生态的发展方向。展望未来,我们可以预见以下趋势:
更深入的云原生集成:CAS 将进一步与 Kubernetes、Service Mesh 等云原生基础设施深度集成,Bean 注册和配置管理将更加动态化。CAS 可能会提供原生的 Kubernetes Operator,支持通过 Custom Resource Definition(CRD)来管理 CAS 的配置和认证策略。
更细粒度的条件控制:Spring Boot 的
@ConditionalOn*注解体系将继续扩展,为 CAS 的模块化配置提供更精细的控制能力。CAS 可能会引入基于特征标志(Feature Flag)的条件控制机制,允许在运行时动态启用或禁用特定功能模块。更完善的可观测性:CAS 的 Bean 注册过程、认证链路执行过程将提供更丰富的可观测性指标(Micrometer 指标、OpenTelemetry 追踪),便于问题排查和性能优化。开发者可以通过 Grafana 仪表盘实时监控认证链路的健康状态。
GraalVM Native Image 支持:随着 Spring Boot 3.x 对 GraalVM Native Image 的支持日益成熟,CAS 未来可能提供原生镜像支持,进一步缩短启动时间(从数十秒缩短到亚秒级)和降低内存占用(从数 GB 降低到数百 MB),使得 CAS 更适合在 Serverless 和边缘计算场景中部署。
AI 驱动的自适应认证:CAS 可能会集成机器学习模型,实现基于用户行为分析的风险自适应认证。这将对认证处理器注册模式提出新的要求——认证处理器可能需要支持动态加载和卸载,以适应不同风险等级下的认证策略切换。
对于正在使用 CAS 或计划引入 CAS 的技术团队,建议持续关注 CAS 的版本发布说明和 Spring Boot 的升级指南,及时跟进技术演进,确保系统的安全性和可维护性。同时,建议建立完善的自动化测试体系(包括单元测试、集成测试和端到端测试),以降低版本升级的风险。
版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。
本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。
如需获取完整项目代码或技术支持,请访问 bima.cc。