Appearance
Apereo CAS 自定义认证处理器从入门到精通:构建企业级统一身份认证
作者: 必码 | bima.cc
前言
在企业级应用架构中,统一身份认证(Single Sign-On,SSO)是基础设施层面的核心组件之一。无论是内部办公系统、客户门户,还是面向第三方的开放平台,都需要一个可靠、安全、可扩展的认证中心来统一管理用户身份。Apereo CAS(Central Authentication Service)作为开源领域最成熟的SSO解决方案之一,凭借其高度可定制的认证架构、丰富的协议支持以及活跃的社区生态,在全球范围内被广泛应用于教育、金融、政务等多个行业。
然而,在实际项目中,绝大多数企业不会直接使用CAS的默认静态用户名密码认证机制。企业的用户数据存储在自有的数据库、LDAP目录或第三方用户中心中,认证逻辑可能涉及手机号登录、邮箱验证码登录、第三方OAuth绑定、邀请码注册等多种场景。这就要求开发者深入理解CAS的认证架构,并具备自定义认证处理器(Authentication Handler)的能力。
本文将基于CAS Overlay项目(覆盖5.3.x、6.6.x、7.3.x三个主要版本线),从认证架构的核心接口体系出发,逐步深入到自定义凭证类设计、认证处理器实现、配置注册、密码安全、属性传递以及审计日志等关键环节。文章不仅讲解"怎么做",更着重阐述"为什么这样做",帮助读者建立对CAS认证机制的系统性认知。
第一章 CAS认证架构概述
1.1 CAS认证流程全景图
在深入代码细节之前,我们需要先建立对CAS认证流程的宏观认知。当用户在浏览器中访问一个受CAS保护的应用时,整个认证流程大致如下:
- 用户访问受保护资源:浏览器向目标应用发起请求。
- 重定向至CAS登录页:目标应用检测到用户未携带有效的Service Ticket,将用户重定向至CAS Server的登录页面。
- 用户提交凭证:用户在CAS登录页面输入用户名、密码等凭证信息,提交表单。
- CAS Server执行认证:CAS Server接收到凭证后,通过Authentication Manager协调多个Authentication Handler完成认证。
- 认证成功与Ticket签发:认证通过后,CAS Server签发Ticket Granting Ticket(TGT),并重定向回目标应用,携带Service Ticket(ST)。
- 目标应用验证Ticket:目标应用通过后端接口向CAS Server验证ST的有效性,验证通过后建立本地会话。
在这个流程中,步骤4是整个认证链路的核心。CAS Server内部的认证引擎负责将用户提交的凭证分发给合适的Authentication Handler进行处理,最终汇总认证结果。理解这个分发机制,是掌握自定义认证的前提。
1.2 AuthenticationHandler接口体系
CAS的认证架构采用了经典的策略模式(Strategy Pattern)。AuthenticationHandler是所有认证处理器的顶层接口,定义了认证处理器必须具备的基本契约。让我们从源码层面分析这个接口体系。
AuthenticationHandler接口的核心方法包括:
java
// 教学示例 - AuthenticationHandler核心方法签名
public interface AuthenticationHandler {
// 处理器名称标识
String getName();
// 判断是否支持给定的凭证类型
boolean supports(Credential credential);
// 执行认证逻辑,返回认证结果
AuthenticationHandlerExecutionResult authenticate(Credential credential)
throws GeneralSecurityException, PreventedException;
// 处理器的执行优先级
int getOrder();
}1
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
这个接口的设计体现了几个重要的架构思想:
第一,凭证与处理器的解耦。 supports()方法建立了一个类型匹配机制,使得Authentication Manager能够在运行时动态选择合适的处理器。每个处理器只需要声明自己能够处理哪种类型的凭证,而不需要关心其他处理器的存在。这种设计使得系统可以同时注册多个处理器,分别处理不同类型的认证场景。
第二,统一的异常体系。 authenticate()方法声明抛出GeneralSecurityException和PreventedException两种异常。前者表示认证过程中的安全相关错误(如密码错误、账户锁定),后者表示由于外部条件导致的认证中断(如数据库不可用、网络超时)。这种异常分类为后续的审计和错误处理提供了清晰的边界。
第三,优先级排序。 getOrder()方法返回一个整数值,用于在多个处理器之间确定执行顺序。数值越小,优先级越高。CAS会按照优先级顺序依次尝试处理器,直到有一个处理器成功完成认证。
1.3 AbstractPreAndPostProcessingAuthenticationHandler抽象类
直接实现AuthenticationHandler接口需要处理大量的模板逻辑,如日志记录、前置校验、后置处理等。为了简化开发者的工作,CAS提供了AbstractPreAndPostProcessingAuthenticationHandler抽象类。这个类是实际开发中最常用的基类。
AbstractPreAndPostProcessingAuthenticationHandler的核心设计采用了模板方法模式(Template Method Pattern)。它将认证流程分解为以下几个阶段:
authenticate(Credential) // 模板方法入口
├── preAuthenticate(Credential) // 前置处理钩子
├── doAuthentication(Credential, Service) // 核心认证逻辑(子类实现)
├── postAuthenticate(Credential, result) // 后置处理钩子
└── createHandlerResult(...) // 构建认证结果1
2
3
4
5
2
3
4
5
前置处理阶段(preAuthenticate)通常用于执行通用的校验逻辑,例如检查凭证是否为空、检查账户是否被锁定等。后置处理阶段(postAuthenticate)则用于在认证成功后执行额外的操作,如记录审计日志、更新登录统计等。核心认证逻辑(doAuthentication)留给子类实现,使得每个具体的认证处理器可以专注于自己的业务逻辑。
在CAS 5.3.x版本中,还存在一个AbstractUsernamePasswordAuthenticationHandler抽象类,它继承自AbstractPreAndPostProcessingAuthenticationHandler,专门针对用户名密码认证场景进行了优化。这个类提供了authenticateUsernamePasswordInternal()方法,简化了用户名密码认证的开发。然而,从CAS 6.x开始,这个类的使用逐渐减少,官方更推荐直接继承AbstractPreAndPostProcessingAuthenticationHandler。
1.4 AuthenticationEventExecutionPlanConfigurer配置接口
有了自定义的认证处理器之后,还需要将其注册到CAS的认证引擎中。AuthenticationEventExecutionPlanConfigurer就是完成这个注册工作的关键接口。
java
// 教学示例 - AuthenticationEventExecutionPlanConfigurer接口
@FunctionalInterface
public interface AuthenticationEventExecutionPlanConfigurer {
void configureAuthenticationExecutionPlan(AuthenticationEventExecutionPlan plan);
}1
2
3
4
5
2
3
4
5
AuthenticationEventExecutionPlan是一个认证执行计划对象,维护了所有注册的认证处理器、凭证解析器(PrincipalResolver)等组件。通过configureAuthenticationExecutionPlan()方法,我们可以将自定义的认证处理器注册到这个执行计划中。
这个接口的设计非常灵活。它既可以由Spring @Configuration类直接实现,也可以通过@Bean方法返回一个Lambda表达式来实现。CAS在启动时会自动扫描所有实现了这个接口的Bean,并将它们收集起来统一执行配置。
1.5 PrincipalFactory委托工厂
在认证成功后,CAS需要创建一个Principal对象来代表已认证的用户。PrincipalFactory就是负责创建这个对象的工厂。
java
// 教学示例 - PrincipalFactory的使用
PrincipalFactory principalFactory = new DefaultPrincipalFactory();
Principal principal = principalFactory.createPrincipal("username", attributes);1
2
3
2
3
DefaultPrincipalFactory是CAS提供的默认实现,它创建的DefaultPrincipal对象包含了用户的唯一标识(通常是用户名)以及一组属性(attributes)。这些属性可以在后续的流程中被Service Registry、Attribute Release策略等组件使用。
PrincipalFactory的设计采用了工厂模式(Factory Pattern),使得开发者可以在需要时替换为自定义的实现。例如,在某些场景下,你可能需要创建包含额外信息的自定义Principal对象,这时就可以提供自己的PrincipalFactory实现。
1.6 Credential凭证体系
Credential是CAS认证架构中的另一个核心概念。它代表了用户提交的认证信息,是一个纯粹的数据载体。CAS内置了多种凭证类型:
UsernamePasswordCredential:最基础的凭证类型,包含用户名和密码。RememberMeUsernamePasswordCredential:扩展了用户名密码凭证,增加了"记住我"功能。TokenCredential:用于基于Token的认证场景。HttpBasedServiceCredential:用于基于HTTP回调的认证场景。OAuth20AuthorizationCodeCredential:用于OAuth2.0授权码认证。
凭证体系的设计遵循了"数据与行为分离"的原则。凭证对象只负责承载数据,不包含任何认证逻辑。认证逻辑完全由AuthenticationHandler负责。这种分离使得凭证对象可以安全地在网络中传输(例如通过表单提交),而不用担心泄露认证逻辑。
理解了以上六个核心概念,我们就建立了对CAS认证架构的系统性认知。接下来,我们将进入实战环节,逐步实现一个企业级的自定义认证处理器。
第二章 自定义凭证类设计
2.1 为什么需要自定义凭证类
CAS默认的UsernamePasswordCredential只包含用户名和密码两个字段。然而,在现代企业应用中,登录场景远比简单的用户名密码复杂得多。企业可能需要支持以下登录方式:
- 用户名密码登录:最传统的登录方式。
- 手机号+验证码登录:用户通过手机接收短信验证码完成登录。
- 邮箱+验证码登录:用户通过邮箱接收验证码完成登录。
- 邀请码登录:新用户通过邀请码完成首次登录和注册。
- 第三方用户绑定:已通过微信、钉钉等第三方平台认证的用户,绑定到本地账户。
- 加密密码传输:前端对密码进行加密后再传输到后端,提高传输安全性。
为了支持这些多样化的登录方式,我们需要对凭证类进行扩展,使其能够携带更多的认证信息。
2.2 继承UsernamePasswordCredential扩展
自定义凭证类的基本思路是继承CAS的UsernamePasswordCredential,然后添加业务所需的额外字段。下面是一个教学示例:
java
// 教学示例 - 自定义凭证类基本结构
public class CustomCredential extends UsernamePasswordCredential {
// 图形验证码
private String captcha;
// 手机号
private String mobile;
// 邮箱
private String email;
// 短信/邮箱验证码
private String authCode;
// 认证类型标识
private String authType;
// 第三方平台用户ID
private String thirdUserId;
// 登录类型:1-账户密码,2-验证码,3-邀请码
private Integer loginType;
// 加密后的密码
private String encryptPassword;
// 客户端标识
private String clientId;
// 省略getter/setter...
}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
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
这个设计有几个值得注意的要点:
第一,保持与父类的兼容性。 通过继承UsernamePasswordCredential,我们的自定义凭证类天然兼容CAS的现有基础设施。CAS的Webflow绑定机制、表单提交处理等组件都可以无缝地与自定义凭证类配合工作。
第二,字段命名要语义清晰。 每个字段都应该有明确的业务含义,避免使用缩写或模糊的命名。例如,authCode明确表示"认证验证码",而不是简单的code。
第三,合理使用数据类型。 loginType使用Integer类型而不是int,是因为它可能为null(当用户未选择登录方式时)。这种设计可以避免NPE(NullPointerException)。
2.3 多种登录方式的字段设计
在实际项目中,多种登录方式往往需要不同的字段组合。我们需要仔细设计字段之间的关系,确保每种登录方式都能获取到所需的全部信息。
用户名密码登录需要的字段:
username(继承自父类):用户名password(继承自父类):密码captcha:图形验证码(防暴力破解)encryptPassword:前端加密后的密码
手机号验证码登录需要的字段:
mobile:手机号authCode:短信验证码loginType:值为2,标识验证码登录
邮箱验证码登录需要的字段:
email:邮箱地址authCode:邮箱验证码loginType:值为2,标识验证码登录
邀请码登录需要的字段:
inviteCode:邀请码captcha:图形验证码loginType:值为3,标识邀请码登录
第三方用户绑定需要的字段:
thirdUserId:第三方平台用户IDauthType:第三方平台类型标识needBindAuthCode:绑定所需的验证码
这种字段设计方式虽然会导致凭证类包含较多的字段,但每个字段都有明确的用途,且不同登录方式之间不会产生冲突。这是一种典型的"宽接口"设计,在SSO场景下是合理的,因为登录表单本身就是一个聚合了多种认证信息的入口。
2.4 加密密码传输机制
在企业级应用中,密码在传输过程中的安全性至关重要。即使使用了HTTPS,额外的客户端加密也能提供纵深防御(Defense in Depth)的能力。基本思路是:
- 前端加密:用户输入密码后,前端JavaScript使用RSA或AES算法对密码进行加密。
- 传输加密密文:加密后的密码通过
encryptPassword字段传输到CAS Server。 - 后端解密验证:CAS Server在认证处理器中使用私钥解密密码,然后进行后续的密码比对。
java
// 教学示例 - 加密密码处理思路
public class PasswordEncryptionUtil {
// 使用RSA私钥解密前端传输的加密密码
public static String decryptPassword(String encryptedPassword, String privateKey) {
// 实际实现需要引入RSA解密逻辑
// 此处仅展示思路
return decryptedPassword;
}
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
需要注意的是,加密传输机制需要配合非对称加密算法使用。前端使用公钥加密,后端使用私钥解密。密钥对的管理(生成、分发、轮换)是整个加密方案的关键环节,需要纳入企业的密钥管理流程中。
2.5 自定义字段绑定与验证
自定义凭证类中的字段需要通过CAS的Webflow绑定机制与前端表单进行关联。CAS使用Spring Webflow来管理登录流程,我们需要通过自定义WebflowConfigurer来添加字段的绑定配置。
java
// 教学示例 - Webflow绑定配置思路
public class CustomWebflowConfigurer extends DefaultLoginWebflowConfigurer {
@Override
protected void createRememberMeAuthnWebflowConfig(Flow flow) {
// 将自定义凭证类注册为Webflow变量
createFlowVariable(flow,
CasWebflowConstants.VAR_ID_CREDENTIAL,
CustomCredential.class);
// 获取登录表单视图状态
ViewState loginState = getState(flow,
CasWebflowConstants.STATE_ID_VIEW_LOGIN_FORM,
ViewState.class);
// 获取绑定配置
BinderConfiguration binder = getViewStateBinderConfiguration(loginState);
// 添加自定义字段的绑定
binder.addBinding(new BinderConfiguration.Binding(
"captcha", null, true));
binder.addBinding(new BinderConfiguration.Binding(
"mobile", null, false));
binder.addBinding(new BinderConfiguration.Binding(
"authCode", null, 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
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
BinderConfiguration.Binding的第三个参数表示该字段是否为必填项。设置为true的字段如果为空,Spring Webflow会自动触发验证错误。这种声明式的验证方式简化了表单验证的开发工作。
此外,还需要重写equals()和hashCode()方法,确保自定义凭证类在CAS的缓存和比较机制中正确工作。这是因为CAS在认证流程中可能会对凭证对象进行缓存和比较,如果这两个方法实现不当,可能导致认证行为异常。
java
// 教学示例 - equals和hashCode的实现要点
@Override
public int hashCode() {
final int prime = 31;
int result = super.hashCode();
result = prime * result + Objects.hash(
captcha, mobile, email, authCode,
authType, thirdUserId, loginType,
encryptPassword, clientId);
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (!super.equals(obj)) return false;
if (getClass() != obj.getClass()) return false;
CustomCredential other = (CustomCredential) obj;
return Objects.equals(captcha, other.captcha)
&& Objects.equals(mobile, other.mobile)
&& Objects.equals(email, other.email)
// ... 比较所有自定义字段
;
}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
第三章 自定义认证处理器实现
3.1 选择合适的基类
在实现自定义认证处理器时,选择合适的基类是第一步。CAS提供了两个主要的抽象基类供开发者选择:
AbstractUsernamePasswordAuthenticationHandler(CAS 5.3.x推荐)AbstractPreAndPostProcessingAuthenticationHandler(CAS 6.x/7.x推荐)
在CAS 5.3.x版本中,AbstractUsernamePasswordAuthenticationHandler是最常用的基类。它专门针对用户名密码认证场景进行了封装,提供了authenticateUsernamePasswordInternal()方法,开发者只需要实现这个方法即可完成认证逻辑。
从CAS 6.x开始,官方更推荐使用AbstractPreAndPostProcessingAuthenticationHandler作为基类。这个类更加通用,不局限于用户名密码认证,可以支持任意类型的凭证。它的核心方法是doAuthentication(),接收Credential和Service两个参数,提供了更大的灵活性。
版本差异要点:
| 特性 | CAS 5.3.x | CAS 6.6.x | CAS 7.3.x |
|---|---|---|---|
| 推荐基类 | AbstractUsernamePasswordAuthenticationHandler | AbstractPreAndPostProcessingAuthenticationHandler | AbstractPreAndPostProcessingAuthenticationHandler |
| 构造函数参数 | name, servicesManager, principalFactory, order | name, principalFactory, order | name, principalFactory, order |
| ServicesManager | 必须传入 | 可选 | 不需要 |
| doAuthentication签名 | Credential | Credential, Service | Credential, Service |
| supports方法 | 自动处理 | 需要显式实现 | 需要显式实现 |
3.2 supports()方法:凭证类型匹配
supports()方法是认证分发机制的核心。CAS的Authentication Manager在收到凭证后,会遍历所有已注册的认证处理器,调用其supports()方法来判断该处理器是否能够处理当前凭证。
java
// 教学示例 - supports方法实现
@Override
public boolean supports(Credential credential) {
// 方式一:精确匹配自定义凭证类型
return credential instanceof CustomCredential;
// 方式二:兼容父类型(更宽松的匹配策略)
// return credential instanceof UsernamePasswordCredential;
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
匹配策略的选择:
精确匹配(instanceof CustomCredential)是一种更安全的方式。它确保只有你的自定义凭证才会被这个处理器处理,避免了与其他处理器的冲突。推荐在大多数场景下使用这种方式。
宽松匹配(instanceof UsernamePasswordCredential)则允许处理器同时处理标准凭证和自定义凭证。这在需要兼容REST API认证等场景时可能有用,但需要注意与其他处理器的优先级配合。
一个常见的陷阱: 如果多个处理器的supports()方法都返回true,CAS会按照优先级顺序依次尝试。如果第一个处理器抛出异常,CAS会继续尝试下一个处理器。这意味着如果你的自定义处理器和CAS内置的静态用户认证处理器同时支持UsernamePasswordCredential,可能会导致不可预期的行为。解决方案是确保自定义处理器的优先级更高,或者覆盖内置处理器的注册。
3.3 doAuthentication()方法:核心认证逻辑
doAuthentication()方法是认证处理器的核心,包含了所有的认证业务逻辑。下面我们从设计模式的角度来分析这个方法的实现。
第一步:凭证转换与校验
java
// 教学示例 - 凭证转换与校验
@Override
protected AuthenticationHandlerExecutionResult doAuthentication(
Credential credential, Service service)
throws GeneralSecurityException, PreventedException {
// 类型转换
CustomCredential customCredential = (CustomCredential) credential;
String username = customCredential.getUsername();
String password = customCredential.getPassword();
// 参数校验
if (StringUtils.isBlank(username)) {
throw new CustomAuthenticationException(
"error.username.required");
}
if (StringUtils.isBlank(password)) {
throw new CustomAuthenticationException(
"error.password.required");
}
// 后续认证逻辑...
}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
参数校验应该放在认证逻辑的最前面,遵循"快速失败"(Fail Fast)原则。尽早发现无效输入,避免不必要的数据库查询等资源消耗。
第二步:用户信息查询
java
// 教学示例 - 用户信息查询
UserInfoDTO userInfo = userInfoService.selectByName(username);
if (userInfo == null || userInfo.getId() == null) {
// 注意:不要明确告知用户"用户名不存在"
// 统一返回"用户名或密码错误",防止用户名枚举攻击
throw new CustomAuthenticationException(
"error.authentication.failed");
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
这里有一个重要的安全原则:不要泄露用户是否存在的信息。无论用户名不存在还是密码错误,都应该返回相同的错误提示。这是防止用户名枚举攻击(Username Enumeration Attack)的基本措施。
第三步:密码比对
java
// 教学示例 - 密码比对逻辑
boolean passwordMatch = passwordEncoder.matches(
password, userInfo.getPassword());
if (!passwordMatch) {
throw new CustomAuthenticationException(
"error.authentication.failed");
}1
2
3
4
5
6
7
2
3
4
5
6
7
密码比对应该使用常量时间比较算法,避免时序攻击(Timing Attack)。CAS内置的密码编码器通常已经处理了这个问题,但如果使用自定义的密码比对逻辑,需要特别注意。
第四步:账户状态检查
java
// 教学示例 - 账户状态检查
if (!userInfo.getIsUsed()) {
throw new CustomAuthenticationException(
"error.account.disabled");
}1
2
3
4
5
2
3
4
5
除了密码验证外,还需要检查账户的状态。常见的状态检查包括:账户是否被禁用、是否被锁定、是否已过期等。
第五步:构建认证结果
java
// 教学示例 - 构建认证结果
Map<String, List<Object>> attributes = new HashMap<>();
attributes.put("userId", Collections.singletonList(
(Object) userInfo.getId()));
attributes.put("email", Collections.singletonList(
(Object) userInfo.getEmail()));
attributes.put("phone", Collections.singletonList(
(Object) userInfo.getTelphone()));
return createHandlerResult(
credential,
this.principalFactory.createPrincipal(username, attributes),
new ArrayList<>()
);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
createHandlerResult()方法由基类提供,它将认证凭证、Principal对象和消息描述列表封装为AuthenticationHandlerExecutionResult。Principal中携带的属性信息将在后续的Attribute Release阶段被使用,决定哪些信息可以传递给下游的应用。
3.4 构造函数设计的版本差异
CAS不同版本的认证处理器构造函数存在显著差异,这是开发者在跨版本迁移时最容易遇到的问题之一。
CAS 5.3.x版本的构造函数:
java
// 教学示例 - CAS 5.3.x构造函数
public CustomAuthenticationHandler(
String name,
ServicesManager servicesManager,
PrincipalFactory principalFactory,
Integer order,
UserInfoService userInfoService) {
super(name, servicesManager, principalFactory, order);
this.userInfoService = userInfoService;
}1
2
3
4
5
6
7
8
9
10
2
3
4
5
6
7
8
9
10
在CAS 5.3.x中,构造函数必须传入ServicesManager参数。ServicesManager是CAS服务注册表的管理器,用于获取和管理已注册的Service(即受保护的应用)。认证处理器在执行过程中可能需要根据当前请求的Service来决定认证策略。
CAS 6.6.x版本的构造函数:
java
// 教学示例 - CAS 6.6.x构造函数
public CustomAuthenticationHandler(
String name,
PrincipalFactory principalFactory,
Integer order,
UserInfoService userInfoService) {
super(name, principalFactory, order);
this.userInfoService = userInfoService;
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
从CAS 6.x开始,ServicesManager不再是构造函数的必需参数。CAS框架内部通过依赖注入的方式在需要时提供ServicesManager。
CAS 7.3.x版本的构造函数:
CAS 7.3.x的构造函数签名与6.6.x基本一致,但框架内部进行了大量的重构。最显著的变化是引入了更多的函数式编程风格和响应式编程支持。不过,对于自定义认证处理器来说,构造函数的使用方式没有本质变化。
版本迁移建议: 如果你的项目需要同时支持多个CAS版本,建议使用工厂模式或构建者模式来封装构造函数的差异,提供一个统一的创建接口。
3.5 Principal创建与属性传递
Principal是认证成功后代表用户的对象,它是CAS整个认证体系中信息流转的核心载体。正确地创建Principal并传递属性,对于后续的授权、属性释放等环节至关重要。
java
// 教学示例 - Principal创建的两种方式
// 方式一:仅传递用户标识
Principal principal1 = principalFactory.createPrincipal(username);
// 方式二:传递用户标识和属性
Map<String, List<Object>> attributes = new HashMap<>();
attributes.put("userId", Collections.singletonList((Object) 12345L));
attributes.put("displayName", Collections.singletonList((Object) "张三"));
attributes.put("email", Collections.singletonList((Object) "zhangsan@example.com"));
attributes.put("roles", Arrays.asList((Object) "ADMIN", (Object) "USER"));
Principal principal2 = principalFactory.createPrincipal(username, attributes);1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
属性设计的最佳实践:
属性值使用List包装:CAS规范要求所有属性值都是
List<Object>类型。即使某个属性只有一个值,也应该用Collections.singletonList()包装。这是为了与LDAP等协议的属性模型保持一致。属性命名要规范:使用驼峰命名法或全小写加连字符的方式,保持与标准属性名的一致性。例如,
email、phoneNumber、memberOf等。敏感信息不要放入Principal:密码、密钥等敏感信息绝对不能放入Principal属性中。Principal中的属性可能会通过CAS的Attribute Release机制传递给下游应用,放入敏感信息会导致安全泄露。
考虑属性的大小:如果某些属性值很大(如用户头像的Base64编码),建议只传递URL引用而不是实际数据。
3.6 自定义异常体系设计
异常体系的设计直接影响用户在登录失败时的体验,以及系统的安全性和可审计性。CAS的认证异常需要继承自javax.security.auth.login.AccountException,这是JAAS(Java Authentication and Authorization Service)规范的要求。
java
// 教学示例 - 自定义异常体系
public class UsernameNullException extends AccountException {
public UsernameNullException(String msg) {
super(msg);
}
}
public class PasswordNullException extends AccountException {
public PasswordNullException(String msg) {
super(msg);
}
}
public class AuthenticationFailedException extends AccountException {
public AuthenticationFailedException(String msg) {
super(msg);
}
}
public class AccountDisabledException extends AccountException {
public AccountDisabledException(String msg) {
super(msg);
}
}
public class RequestLimitException extends AccountException {
public RequestLimitException(String msg) {
super(msg);
}
}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
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
异常设计的原则:
细粒度异常分类:不同类型的认证失败应该使用不同的异常类。这不仅是代码可读性的要求,更重要的是CAS框架可以根据异常类型来执行不同的处理逻辑。例如,
RequestLimitException可以触发频率限制机制,AccountDisabledException可以显示不同的错误提示。异常消息国际化:异常消息应该使用资源文件的key,而不是硬编码的中文字符串。CAS的异常消息解析机制会自动根据用户的Locale从
messages.properties文件中查找对应的消息。安全信息隐藏:面向用户的异常消息不应该包含系统内部信息。例如,不应该返回"数据库连接超时"这样的技术细节,而应该返回"系统繁忙,请稍后重试"。
异常层次结构:如果异常之间存在逻辑上的包含关系,可以设计异常层次结构。例如,
AccountLockedException和AccountExpiredException都可以继承自AccountStatusException。
第四章 认证配置注册
4.1 CustomAuthenticationConfiguration类实现
有了自定义的认证处理器之后,下一步是将其注册到CAS的认证引擎中。在CAS 5.3.x和6.6.x版本中,通常通过一个Spring @Configuration类来完成这个工作。
java
// 教学示例 - CAS 5.3.x/6.6.x配置类
@Configuration("CustomAuthenticationConfiguration")
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CustomAuthenticationConfiguration
implements AuthenticationEventExecutionPlanConfigurer {
@Autowired
@Qualifier("servicesManager")
private ServicesManager servicesManager;
@Autowired
private UserInfoService userInfoService;
@Bean
public AuthenticationHandler myAuthenticationHandler() {
return new CustomAuthenticationHandler(
CustomAuthenticationHandler.class.getName(),
servicesManager,
new DefaultPrincipalFactory(),
1,
userInfoService
);
}
@Override
public void configureAuthenticationExecutionPlan(
AuthenticationEventExecutionPlan plan) {
plan.registerAuthenticationHandler(myAuthenticationHandler());
}
}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
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
配置类的关键注解说明:
@Configuration("CustomAuthenticationConfiguration"):声明这是一个配置类,注解值指定了配置类的名称。CAS在自动装配时会使用这个名称来引用配置类。@EnableConfigurationProperties(CasConfigurationProperties.class):启用CAS的配置属性绑定,使得我们可以在配置文件中使用cas.*前缀的配置项。@Autowired+@Qualifier("servicesManager"):注入CAS的ServicesManagerBean。@Qualifier注解用于在有多个同类型Bean时指定具体注入哪一个。
4.2 AuthenticationEventExecutionPlanConfigurer接口
AuthenticationEventExecutionPlanConfigurer是CAS认证配置的核心接口。它定义了一个方法configureAuthenticationExecutionPlan(),允许开发者向认证执行计划中注册自定义组件。
java
// 教学示例 - AuthenticationEventExecutionPlanConfigurer的多种实现方式
// 方式一:配置类直接实现接口
@Configuration
public class MyAuthConfig implements AuthenticationEventExecutionPlanConfigurer {
@Override
public void configureAuthenticationExecutionPlan(
AuthenticationEventExecutionPlan plan) {
plan.registerAuthenticationHandler(myHandler());
}
}
// 方式二:通过@Bean返回Lambda表达式
@Configuration
public class MyAuthConfig {
@Bean
public AuthenticationEventExecutionPlanConfigurer myConfigurer() {
return plan -> {
plan.registerAuthenticationHandler(myHandler());
};
}
}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
两种方式在功能上是等价的。方式一更加传统,适合配置逻辑较复杂的场景;方式二更加简洁,利用了Java 8的Lambda表达式特性。
AuthenticationEventExecutionPlan提供的注册方法:
registerAuthenticationHandler(handler):注册一个认证处理器。registerAuthenticationHandlerWithPrincipalResolver(handler, resolver):注册一个认证处理器,并指定对应的Principal解析器。registerAuthenticationMetadataPopulator(populator):注册认证元数据填充器。
4.3 configureAuthenticationExecutionPlan()方法详解
configureAuthenticationExecutionPlan()方法在CAS启动时被调用,此时所有的Bean都已经初始化完毕。在这个方法中,我们可以执行以下操作:
java
// 教学示例 - configureAuthenticationExecutionPlan的完整用法
@Override
public void configureAuthenticationExecutionPlan(
AuthenticationEventExecutionPlan plan) {
// 1. 注册自定义认证处理器
plan.registerAuthenticationHandler(
customUsernamePasswordHandler());
// 2. 注册带Principal解析器的处理器
plan.registerAuthenticationHandlerWithPrincipalResolver(
customTokenHandler(),
defaultPrincipalResolver);
// 3. 注册认证元数据填充器
plan.registerAuthenticationMetadataPopulator(
customMetadataPopulator());
}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
执行顺序的注意事项:
CAS会按照配置类的加载顺序依次调用每个AuthenticationEventExecutionPlanConfigurer的configureAuthenticationExecutionPlan()方法。如果多个配置类都注册了处理器,这些处理器会按照各自的order值排序后执行。
需要注意的是,CAS内置的配置类也会注册默认的认证处理器(如静态用户认证处理器)。如果你的自定义处理器需要完全替代默认处理器,需要确保自定义处理器的优先级更高,或者显式地移除默认处理器(CAS 7.3.x提供了这种方式)。
4.4 CAS 7.3版本的配置变化
CAS 7.3.x在认证配置方面引入了一些重要的变化,其中最显著的是@RefreshScope注解的使用和acceptUsersAuthenticationEventExecutionPlanConfigurer的覆盖。
@RefreshScope注解:
java
// 教学示例 - CAS 7.3.x配置类
@Configuration
@EnableConfigurationProperties(CasConfigurationProperties.class)
public class CasInitializerConfig {
@Autowired
private PrincipalFactory principalFactory;
@Bean
@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
public AuthenticationHandler loginAuthenticationHandler() {
UserInfoService userInfoService = userInfoService();
return new Login(
"LoginAuthenticationHandler",
principalFactory,
1,
userInfoService
);
}
@Bean
@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
public AuthenticationEventExecutionPlanConfigurer
authenticationEventExecutionPlanConfigurer(
@Qualifier("loginAuthenticationHandler")
final AuthenticationHandler handler,
@Qualifier(PrincipalResolver.BEAN_NAME_PRINCIPAL_RESOLVER)
final PrincipalResolver resolver) {
return plan -> {
// 清除所有现有的认证处理器
plan.getAuthenticationHandlers().clear();
// 只注册自定义的认证处理器
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
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
@RefreshScope是Spring Cloud Context提供的注解,它使得Bean在配置变更时可以被重新创建。在CAS 7.3.x中,将认证处理器标记为@RefreshScope,可以实现运行时动态更新认证配置,而无需重启CAS Server。
覆盖acceptUsersAuthenticationEventExecutionPlanConfigurer:
CAS默认会注册一个acceptUsersAuthenticationEventExecutionPlanConfigurer,它基于cas.authn.accept.users配置项创建一个静态用户认证处理器。在企业环境中,我们通常不需要这个默认处理器,但它可能会干扰自定义处理器的执行。
java
// 教学示例 - 覆盖默认的静态用户认证配置
@Bean
@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
public AuthenticationEventExecutionPlanConfigurer
acceptUsersAuthenticationEventExecutionPlanConfigurer() {
return plan -> {
// 不注册任何认证处理器,覆盖默认行为
};
}1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
通过定义一个同名的Bean,我们可以覆盖CAS的默认配置。这是一种"空操作"(No-Op)模式,它使得CAS不再注册默认的静态用户认证处理器,从而避免与自定义处理器产生冲突。
同样地,CAS默认还会注册一个acceptUsersAuthenticationInitializingBean,它会在启动时打印静态凭证认证的警告信息。我们也可以覆盖这个Bean来消除不必要的日志输出:
java
// 教学示例 - 覆盖默认的InitializingBean
@Bean
@RefreshScope(proxyMode = ScopedProxyMode.DEFAULT)
public InitializingBean acceptUsersAuthenticationInitializingBean() {
return () -> {
// 不执行任何操作
};
}1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
CAS 7.3.x配置的核心优势:
- 完全控制认证处理器列表:通过
plan.getAuthenticationHandlers().clear()清除所有现有处理器,然后只注册自定义处理器,确保认证行为完全可控。 - 动态刷新能力:
@RefreshScope使得认证配置可以在运行时更新,对于需要频繁调整认证策略的场景非常有用。 - 简化构造函数:不再需要传入
ServicesManager,减少了配置的复杂度。
第五章 密码安全
5.1 密码安全的重要性
密码安全是认证系统中最关键的环节之一。一个设计不当的密码存储和验证机制,可能导致整个认证体系的安全性崩溃。历史上,由于密码存储不当导致的数据泄露事件屡见不鲜。作为认证系统的开发者,我们必须对密码安全有深入的理解。
密码安全涉及以下几个层面:
- 存储安全:密码在数据库中的存储方式。
- 传输安全:密码在网络传输过程中的保护。
- 验证安全:密码比对过程中的安全性。
- 策略安全:密码复杂度、过期等策略。
5.2 SHA-256加盐加密
在企业级应用中,密码永远不应该以明文形式存储在数据库中。即使数据库被攻破,攻击者也不应该能够直接获取用户的原始密码。SHA-256加盐加密是一种常用的密码存储方案。
加密原理:
SHA-256(Secure Hash Algorithm 256-bit)是一种单向哈希函数,它将任意长度的输入映射为固定长度(256位)的输出。单向意味着从哈希值无法反推出原始输入。加盐(Salting)是在原始密码中加入一个随机字符串(盐值),然后再进行哈希。盐值的作用是防止彩虹表攻击(Rainbow Table Attack)。
java
// 教学示例 - SHA-256加盐加密实现
public class Sha256Utils {
private static final String SHA_256 = "SHA-256";
/**
* 对输入数据进行SHA-256加密
* @param input 原始输入(如密码)
* @param salt 盐值
* @return 加密后的哈希字符串
*/
public static String encrypt(String input, String salt) {
input += salt;
try {
MessageDigest digest = MessageDigest.getInstance(SHA_256);
byte[] hash = digest.digest(
input.getBytes(StandardCharsets.UTF_8));
StringBuilder hexString = new StringBuilder();
for (byte b : hash) {
String hex = Integer.toHexString(0xff & b);
if (hex.length() == 1) {
hexString.append('0');
}
hexString.append(hex);
}
return "{SHA256}" + hexString.toString();
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(
"SHA-256 algorithm not available", e);
}
}
/**
* 校验输入数据与哈希值是否匹配
*/
public static boolean verify(String input, String salt,
String hashString) {
String encryptedInput = encrypt(input, salt);
return encryptedInput.equals(hashString);
}
}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
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
实现要点解析:
盐值拼接方式:示例中采用简单的字符串拼接(
input += salt)。在实际生产中,更推荐使用HMAC(Hash-based Message Authentication Code)方式,将盐值作为HMAC的密钥,这样更加安全。哈希值前缀标识:加密结果添加了
{SHA256}前缀。这是一种常见的做法,用于标识哈希算法类型。当系统需要支持多种哈希算法时,可以通过前缀来判断使用哪种算法进行验证。字符编码:使用
StandardCharsets.UTF_8确保跨平台的一致性。不同平台可能使用不同的默认字符编码,显式指定UTF-8可以避免编码不一致导致的问题。异常处理:
NoSuchAlgorithmException在正常情况下不应该发生(因为SHA-256是JDK内置的算法),但作为防御性编程的一部分,仍然需要处理。
5.3 数据库密码比对逻辑
密码比对的完整流程包括以下几个步骤:
java
// 教学示例 - 完整的密码比对流程
public class PasswordVerificationService {
/**
* 验证用户密码
* @param inputPassword 用户输入的密码
* @param storedHash 数据库中存储的哈希值
* @param storedSalt 数据库中存储的盐值
* @return 密码是否匹配
*/
public boolean verifyPassword(String inputPassword,
String storedHash, String storedSalt) {
// 1. 参数校验
if (inputPassword == null || storedHash == null
|| storedSalt == null) {
return false;
}
// 2. 使用相同的算法和盐值对输入密码进行加密
String computedHash = Sha256Utils.encrypt(
inputPassword, storedSalt);
// 3. 使用常量时间比较防止时序攻击
return MessageDigest.isEqual(
computedHash.getBytes(StandardCharsets.UTF_8),
storedHash.getBytes(StandardCharsets.UTF_8));
}
}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
关键安全要点:
使用
MessageDigest.isEqual()进行常量时间比较:普通的String.equals()方法在发现第一个不匹配的字符时就会返回false,这会导致比较时间与密码的匹配程度相关。攻击者可以通过测量比较时间来逐字符地猜测密码(时序攻击)。MessageDigest.isEqual()方法始终比较完整的字符串,不受匹配位置的影响。每个用户使用不同的盐值:虽然示例中的盐值是固定的,但在生产环境中,每个用户应该有唯一的盐值。这样即使两个用户使用相同的密码,他们的哈希值也会不同。盐值应该在用户注册时随机生成,并与哈希值一起存储在数据库中。
考虑使用专门的密码哈希算法:SHA-256虽然比MD5和SHA-1更安全,但它并不是专门为密码存储设计的算法。在生产环境中,更推荐使用BCrypt、SCrypt或Argon2等专门的密码哈希算法。这些算法内置了盐值管理和可调的计算成本,能够有效抵御暴力破解攻击。
5.4 密码策略设计
一个完善的密码策略应该涵盖密码的整个生命周期:
密码复杂度要求:
- 最小长度:建议至少8个字符,推荐12个以上。
- 字符多样性:要求包含大写字母、小写字母、数字和特殊字符中的至少三类。
- 禁止使用常见密码:维护一个常见密码黑名单,如"123456"、"password"等。
- 禁止与用户名相同:密码不能与用户名、邮箱等个人信息相同。
密码生命周期管理:
- 密码过期策略:定期要求用户修改密码(如每90天)。
- 密码历史记录:防止用户重复使用最近使用过的密码(如最近5次)。
- 初始密码强制修改:首次登录或密码被管理员重置后,强制用户修改密码。
密码传输安全:
- 强制使用HTTPS:所有涉及密码传输的请求必须通过HTTPS。
- 前端加密:可选的额外安全层,使用RSA等算法在前端对密码进行加密。
- 密码字段不记录日志:确保密码不会被记录到应用日志、访问日志或审计日志中。
java
// 教学示例 - 密码策略校验思路
public class PasswordPolicyValidator {
private static final int MIN_LENGTH = 8;
private static final int MAX_HISTORY = 5;
public ValidationResult validate(String newPassword,
String username, List<String> passwordHistory) {
List<String> errors = new ArrayList<>();
// 长度校验
if (newPassword.length() < MIN_LENGTH) {
errors.add("密码长度不能少于" + MIN_LENGTH + "个字符");
}
// 复杂度校验
if (!meetsComplexity(newPassword)) {
errors.add("密码必须包含大小写字母、数字和特殊字符");
}
// 与用户名相同校验
if (newPassword.equalsIgnoreCase(username)) {
errors.add("密码不能与用户名相同");
}
// 历史密码校验
if (passwordHistory != null) {
for (String oldPassword : passwordHistory) {
if (newPassword.equals(oldPassword)) {
errors.add("不能使用最近使用过的密码");
break;
}
}
}
return new ValidationResult(errors.isEmpty(), errors);
}
private boolean meetsComplexity(String password) {
boolean hasUpper = false, hasLower = false;
boolean hasDigit = false, hasSpecial = false;
for (char c : password.toCharArray()) {
if (Character.isUpperCase(c)) hasUpper = true;
else if (Character.isLowerCase(c)) hasLower = true;
else if (Character.isDigit(c)) hasDigit = true;
else hasSpecial = true;
}
int categoryCount = (hasUpper ? 1 : 0) + (hasLower ? 1 : 0)
+ (hasDigit ? 1 : 0) + (hasSpecial ? 1 : 0);
return categoryCount >= 3;
}
}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
第六章 认证属性传递
6.1 Principal属性注入
在认证成功后,CAS会将用户信息封装为Principal对象。Principal不仅包含用户的唯一标识,还可以携带一组属性(Attributes)。这些属性是连接认证与授权的桥梁,它们描述了"这个用户是谁"以及"这个用户有什么权限"。
java
// 教学示例 - Principal属性注入
Map<String, List<Object>> attributes = new HashMap<>();
// 基本用户信息
attributes.put("userId",
Collections.singletonList((Object) userInfo.getId()));
attributes.put("username",
Collections.singletonList((Object) userInfo.getName()));
attributes.put("displayName",
Collections.singletonList((Object) userInfo.getNickname()));
attributes.put("email",
Collections.singletonList((Object) userInfo.getEmail()));
attributes.put("phone",
Collections.singletonList((Object) userInfo.getTelphone()));
// 账户状态信息
attributes.put("accountEnabled",
Collections.singletonList((Object) userInfo.getIsUsed()));
// 组织架构信息
attributes.put("department",
Collections.singletonList((Object) "技术部"));
attributes.put("position",
Collections.singletonList((Object) "高级工程师"));
Principal principal = principalFactory.createPrincipal(
username, attributes);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内部使用了一些标准的属性名(如
cn、mail、memberOf等,来自LDAP/X.500标准),建议在自定义属性中也参考这些标准。类型一致性:所有属性值都应该使用
List<Object>类型。CAS的属性释放机制是基于这种类型设计的,使用其他类型可能导致序列化或反序列化问题。避免敏感信息:密码、密钥、身份证号等敏感信息不应该放入Principal属性中。如果下游应用确实需要这些信息,应该通过独立的、更安全的渠道获取。
6.2 AuthenticationAttributes
除了Principal属性外,CAS还支持AuthenticationAttributes。这两者的区别在于作用范围不同:
- Principal Attributes:描述用户的固有属性,如姓名、邮箱、部门等。这些属性与用户身份绑定,不随认证会话的变化而变化。
- Authentication Attributes:描述本次认证的上下文信息,如认证时间、认证方式、客户端IP地址、设备指纹等。这些属性与具体的认证事件绑定。
java
// 教学示例 - AuthenticationAttributes的使用
// 在认证处理器中,可以通过createHandlerResult设置认证属性
// AuthenticationAttributes通常由框架自动填充
// 包括:authenticationDate, successfulAuthenticationHandlers,
// clientIpAddress, userAgent等1
2
3
4
5
2
3
4
5
AuthenticationAttributes的主要用途包括:
- 审计追踪:记录每次认证的详细信息,用于事后审计。
- 风险评估:根据认证上下文信息评估本次认证的风险等级。例如,如果用户从一个从未使用过的IP地址登录,可以触发额外的验证步骤。
- 会话管理:根据认证属性决定会话的超时策略。例如,从可信网络登录的会话可以设置更长的超时时间。
6.3 多因素认证(MFA)集成点
多因素认证(Multi-Factor Authentication,MFA)是现代安全体系的重要组成部分。CAS提供了完善的MFA支持,而认证处理器是MFA集成的关键切入点。
MFA的触发方式:
基于属性的触发:在认证处理器中设置特定的属性,CAS的MFA配置可以根据这些属性来决定是否触发MFA。例如,如果用户的
riskLevel属性为"high",则强制要求MFA。基于认证方式的触发:不同的认证方式可以配置不同的MFA策略。例如,用户名密码登录后需要短信验证码确认,而证书登录则不需要。
基于Service的触发:不同的受保护应用可以配置不同的MFA要求。例如,财务系统需要MFA,而内部知识库不需要。
yaml
# 教学示例 - CAS MFA配置思路(application.yml)
cas:
authn:
mfa:
# 全局MFA配置
global:
enabled: true
# 强制认证的Provider ID
globalProviderId: mfa-duo
# 基于属性的MFA触发
triggers:
authentication-attributes:
- name: "requireMfa"
values:
- "true"
# 基于Service的MFA触发
service-specific:
- id: "https://finance.example.com/**"
mfaProviderId: "mfa-duo"1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在认证处理器中集成MFA提示:
java
// 教学示例 - 在认证处理器中设置MFA相关属性
Map<String, List<Object>> attributes = new HashMap<>();
// ... 其他属性 ...
// 设置MFA触发属性
if (userInfo.isRequireMfa()) {
attributes.put("requireMfa",
Collections.singletonList((Object) "true"));
}
// 设置用户已注册的MFA方式
List<Object> registeredMfaMethods = new ArrayList<>();
if (userInfo.isSmsEnabled()) {
registeredMfaMethods.add("SMS");
}
if (userInfo.isTotpEnabled()) {
registeredMfaMethods.add("TOTP");
}
attributes.put("registeredMfaMethods", registeredMfaMethods);1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
通过这种方式,认证处理器可以将MFA相关的决策信息传递给CAS的MFA引擎,由MFA引擎来协调具体的验证流程。这种设计实现了认证逻辑与MFA逻辑的解耦,使得两者可以独立演进。
第七章 认证事件与审计
7.1 AuthenticationHandler成功/失败事件
CAS内置了一套事件发布机制,在认证过程中会发布各种类型的事件。这些事件可以被监听器捕获,用于触发后续的业务逻辑。
CAS内置的认证事件类型:
CasAuthenticationTransactionSuccessfulEvent:认证事务成功事件。CasAuthenticationTransactionFailureEvent:认证事务失败事件。CasAuthenticationSuccessEvent:单个认证处理器认证成功事件。CasAuthenticationFailureEvent:单个认证处理器认证失败事件。
这些事件由CAS框架在认证流程的各个阶段自动发布。开发者可以通过实现Spring的ApplicationListener接口或使用@EventListener注解来监听这些事件。
java
// 教学示例 - 认证事件监听
@Component
public class AuthenticationEventListener {
private static final Logger logger =
LoggerFactory.getLogger(AuthenticationEventListener.class);
@EventListener
public void onAuthenticationSuccess(
CasAuthenticationSuccessEvent event) {
Authentication authentication = event.getAuthentication();
Principal principal = authentication.getPrincipal();
logger.info("User [{}] authenticated successfully "
+ "via handler [{}]",
principal.getId(),
authentication.getSuccesses().keySet());
}
@EventListener
public void onAuthenticationFailure(
CasAuthenticationFailureEvent event) {
Credential credential = event.getCredential();
logger.warn("Authentication failed for credential [{}]",
credential);
}
}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
自定义事件发布:
在某些场景下,你可能需要在认证处理器中发布自定义事件。例如,当检测到异常的登录行为时,可以发布一个安全告警事件。
java
// 教学示例 - 在认证处理器中发布自定义事件
public class SecurityAlertEvent extends ApplicationEvent {
private final String username;
private final String alertType;
private final String ipAddress;
public SecurityAlertEvent(Object source, String username,
String alertType, String ipAddress) {
super(source);
this.username = username;
this.alertType = alertType;
this.ipAddress = ipAddress;
}
// 省略getter...
}
// 在认证处理器中使用
@Autowired
private ApplicationEventPublisher eventPublisher;
// 检测到异常登录行为时
eventPublisher.publishEvent(new SecurityAlertEvent(
this, username, "SUSPICIOUS_LOGIN", clientIpAddress));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
7.2 审计日志记录
审计日志是企业级认证系统不可或缺的组成部分。它记录了所有认证相关操作的详细信息,为安全分析、合规审计和故障排查提供数据支撑。
CAS内置的审计机制:
CAS基于Apereo Inspektr框架实现了审计日志功能。审计日志记录了以下信息:
- WHO:谁执行了操作(用户标识)。
- WHAT:执行了什么操作(动作类型)。
- WHEN:什么时候执行的(时间戳)。
- WHERE:从哪里执行的(客户端IP、资源名称)。
- OUTCOME:操作结果(成功/失败)。
yaml
# 教学示例 - CAS审计日志配置(application.yml)
cas:
audit:
# 审计日志配置
slf4j:
enabled: true
# 审计日志格式
auditFormat:
json: true1
2
3
4
5
6
7
8
9
2
3
4
5
6
7
8
9
自定义审计日志:
除了使用CAS内置的审计机制外,开发者还可以在认证处理器中添加自定义的审计逻辑。例如,将认证日志写入独立的审计数据库,或者推送到外部的日志分析平台。
java
// 教学示例 - 自定义审计日志记录
public class AuthenticationAuditLogger {
private static final Logger auditLogger =
LoggerFactory.getLogger("AUDIT_LOG");
public void logAuthenticationAttempt(String username,
String authType, String clientIp,
boolean success, String failureReason) {
AuditLogEntry entry = new AuditLogEntry();
entry.setTimestamp(Instant.now());
entry.setUsername(username);
entry.setAuthType(authType);
entry.setClientIp(clientIp);
entry.setSuccess(success);
entry.setFailureReason(failureReason);
// 使用结构化日志格式
auditLogger.info("AUTH_AUDIT | username={} | authType={} "
+ "| clientIp={} | success={} | reason={}",
entry.getUsername(),
entry.getAuthType(),
entry.getClientIp(),
entry.isSuccess(),
entry.getFailureReason());
}
}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
审计日志的设计原则:
- 不可篡改性:审计日志一旦写入,就不应该被修改或删除。可以考虑使用追加写入(Append-Only)的存储方式。
- 完整性:审计日志应该记录足够的信息,使得事后可以完整地还原认证过程。
- 时效性:审计日志应该在认证完成后立即写入,避免因系统故障导致日志丢失。
- 合规性:审计日志的内容和保留策略应该符合相关的法律法规要求(如等保2.0、GDPR等)。
7.3 认证失败统计与频率限制
认证失败统计和频率限制是防御暴力破解攻击的重要手段。CAS内置了Throttle(节流)机制,但企业级应用通常需要更精细的控制策略。
CAS内置的Throttle机制:
CAS的Throttle机制基于失败计数和时间窗口来实现频率限制。当某个用户名或IP地址在指定时间窗口内的认证失败次数超过阈值时,CAS会暂时拒绝该用户名或IP地址的认证请求。
yaml
# 教学示例 - CAS Throttle配置(application.yml)
cas:
authn:
throttle:
# 启用频率限制
enabled: true
# 失败次数阈值
failure:
threshold: 5
# 时间窗口(秒)
range-seconds: 300
# 应用范围:USERNAME, IP_ADDRESS, BOTH
app-code: USERNAME1
2
3
4
5
6
7
8
9
10
11
12
13
2
3
4
5
6
7
8
9
10
11
12
13
自定义频率限制策略:
CAS内置的Throttle机制使用内存存储,在分布式部署场景下存在局限性。企业级应用通常需要基于Redis等分布式缓存来实现跨节点的频率限制。
java
// 教学示例 - 基于Redis的频率限制实现思路
public class DistributedRateLimiter {
private final RedisTemplate<String, String> redisTemplate;
private final int maxAttempts;
private final int windowSeconds;
/**
* 检查是否允许认证尝试
* @param key 限制键(用户名或IP地址)
* @return true-允许,false-被限制
*/
public boolean allowAttempt(String key) {
String redisKey = "auth:rate:" + key;
Long count = redisTemplate.opsForValue().increment(redisKey);
if (count != null && count == 1) {
redisTemplate.expire(redisKey, windowSeconds,
TimeUnit.SECONDS);
}
return count != null && count <= maxAttempts;
}
/**
* 认证成功后清除计数
*/
public void resetOnSuccess(String key) {
redisTemplate.delete("auth:rate:" + key);
}
/**
* 获取剩余尝试次数
*/
public int getRemainingAttempts(String key) {
String redisKey = "auth:rate:" + key;
String count = redisTemplate.opsForValue().get(redisKey);
int current = count != null ? Integer.parseInt(count) : 0;
return Math.max(0, maxAttempts - current);
}
}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
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
频率限制的分层策略:
一个完善的频率限制系统应该采用分层策略:
- 用户名维度:限制单个用户名的认证尝试次数。防止攻击者对特定账户进行暴力破解。
- IP地址维度:限制单个IP地址的认证尝试次数。防止攻击者使用分布式方式对多个账户进行暴力破解。
- 全局维度:限制整个系统的认证请求总量。防止DDoS攻击导致认证服务不可用。
- 渐进式限制:随着失败次数的增加,逐步收紧限制策略。例如,失败3次后要求验证码,失败5次后锁定15分钟,失败10次后锁定1小时。
java
// 教学示例 - 渐进式频率限制策略
public class ProgressiveRateLimitStrategy {
public LimitResult checkLimit(String username, String ipAddress) {
int userFailures = getFailureCount(username);
int ipFailures = getFailureCount(ipAddress);
if (userFailures >= 10 || ipFailures >= 20) {
// 严重限制:锁定较长时间
return LimitResult.locked(3600);
} else if (userFailures >= 5 || ipFailures >= 10) {
// 中等限制:要求验证码 + 短时间锁定
return LimitResult.captchaRequired(300);
} else if (userFailures >= 3 || ipFailures >= 5) {
// 轻度限制:要求验证码
return LimitResult.captchaRequired(0);
}
return LimitResult.allowed();
}
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
认证失败统计的价值:
除了频率限制外,认证失败统计数据还有其他重要的用途:
- 安全态势感知:通过分析失败认证的分布模式,可以发现潜在的安全威胁。例如,大量针对同一用户名的失败请求可能表示凭证填充攻击(Credential Stuffing Attack)。
- 用户体验优化:如果某个用户反复认证失败,可以主动提供密码重置链接或客服联系方式。
- 系统健康监控:认证失败率的突然上升可能表示认证后端(如数据库)出现了问题。
第八章 版本迁移与最佳实践
8.1 从CAS 5.3.x迁移到6.6.x
CAS从5.3.x到6.6.x的升级是一个较大的跨越,涉及多个方面的变化。以下是认证处理器相关的关键迁移点:
包名变化:
CAS 6.x对部分包名进行了重组。最显著的变化是credential子包的移动:
java
// CAS 5.3.x
import org.apereo.cas.authentication.UsernamePasswordCredential;
// CAS 6.6.x
import org.apereo.cas.authentication.credential.UsernamePasswordCredential;1
2
3
4
5
2
3
4
5
基类变化:
从AbstractUsernamePasswordAuthenticationHandler迁移到AbstractPreAndPostProcessingAuthenticationHandler:
java
// CAS 5.3.x
public class Login extends AbstractUsernamePasswordAuthenticationHandler {
@Override
protected AuthenticationHandlerExecutionResult
authenticateUsernamePasswordInternal(
UsernamePasswordCredential credential,
String originalPassword) { ... }
}
// CAS 6.6.x
public class Login extends AbstractPreAndPostProcessingAuthenticationHandler {
@Override
public boolean supports(Credential credential) { ... }
@Override
protected AuthenticationHandlerExecutionResult
doAuthentication(Credential credential,
Service service) { ... }
}1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
构造函数变化:
移除ServicesManager参数,简化构造函数。
配置方式变化:
CAS 6.x推荐使用@Bean方法返回Lambda表达式的方式来注册认证配置,而不是直接实现接口。
8.2 从CAS 6.6.x迁移到7.3.x
CAS 7.3.x的认证配置变化相对较小,主要集中在以下几个方面:
- 引入
@RefreshScope:支持运行时动态刷新认证配置。 - 覆盖默认配置:通过同名Bean覆盖
acceptUsersAuthenticationEventExecutionPlanConfigurer。 - 清除现有处理器:通过
plan.getAuthenticationHandlers().clear()完全控制处理器列表。 - Java版本要求:CAS 7.x要求Java 17或更高版本。
8.3 企业级最佳实践总结
基于多个CAS项目的实施经验,以下是自定义认证处理器开发的企业级最佳实践:
架构设计层面:
- 单一职责原则:每个认证处理器只负责一种认证方式。不要在一个处理器中混合处理多种认证场景。
- 依赖注入优于硬编码:将数据库访问、外部服务调用等依赖通过构造函数注入,而不是在处理器内部直接创建。
- 接口抽象:将用户数据访问抽象为接口,而不是直接依赖具体的DAO实现。这使得认证处理器可以在不同的数据源之间切换。
- 配置外部化:将可配置的参数(如密码策略、频率限制阈值等)外部化到配置文件中,而不是硬编码在代码中。
安全层面:
- 统一错误提示:不要通过错误提示泄露用户是否存在、账户状态等敏感信息。
- 防御性编程:对所有外部输入进行校验,包括空值检查、类型检查、长度限制等。
- 日志脱敏:确保密码等敏感信息不会被记录到日志中。
- 常量时间比较:密码比对使用常量时间算法,防止时序攻击。
- 最小权限原则:认证处理器使用的数据库账号应该只具有必要的最小权限。
运维层面:
- 健康检查:为认证处理器添加健康检查接口,监控数据库连接、外部服务可用性等。
- 指标监控:暴露认证成功/失败次数、平均认证耗时等指标,接入Prometheus等监控系统。
- 灰度发布:在升级认证处理器时,通过特性开关(Feature Toggle)实现灰度发布,降低升级风险。
- 回滚方案:准备好快速回滚方案,确保在出现问题时可以快速恢复服务。
第九章 实战案例:构建多方式登录认证系统
9.1 需求分析
假设我们需要为一个企业门户构建统一认证系统,支持以下登录方式:
- 用户名密码登录:标准的用户名密码认证。
- 手机号+验证码登录:用户通过手机号接收短信验证码登录。
- 邮箱+验证码登录:用户通过邮箱接收验证码登录。
- 图形验证码:所有登录方式都需要通过图形验证码校验(防机器人)。
9.2 整体架构设计
基于CAS的认证架构,我们可以设计如下方案:
┌─────────────────────────────────────────────────┐
│ CAS Login Page │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ 用户名密码 │ │ 手机验证码 │ │ 邮箱验证码 │ │
│ └────┬─────┘ └────┬─────┘ └────┬─────┘ │
│ └──────────────┼──────────────┘ │
│ ▼ │
│ ┌─────────────────┐ │
│ │ CustomCredential │ │
│ └────────┬────────┘ │
└─────────────────────┼───────────────────────────┘
▼
┌─────────────────────────────────────────────────┐
│ CAS Authentication Engine │
│ ┌───────────────────────────────────────┐ │
│ │ CustomAuthenticationHandler │ │
│ │ ├── supports() → CustomCredential │ │
│ │ └── doAuthentication() │ │
│ │ ├── 解析loginType │ │
│ │ ├── 校验图形验证码 │ │
│ │ ├── 分发到具体认证策略 │ │
│ │ │ ├── PasswordAuthStrategy │ │
│ │ │ ├── SmsAuthStrategy │ │
│ │ │ └── EmailAuthStrategy │ │
│ │ └── 创建Principal + 属性 │ │
│ └───────────────────────────────────────┘ │
└─────────────────────────────────────────────────┘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
9.3 认证策略模式实现
在认证处理器内部,我们可以使用策略模式来管理不同的认证方式:
java
// 教学示例 - 认证策略接口
public interface AuthStrategy {
/**
* 执行认证
* @param credential 自定义凭证
* @return 认证成功后的用户信息
*/
UserInfoDTO authenticate(CustomCredential credential)
throws GeneralSecurityException;
/**
* 是否支持该登录类型
*/
boolean supports(Integer loginType);
}
// 教学示例 - 密码认证策略
public class PasswordAuthStrategy implements AuthStrategy {
private final UserInfoService userInfoService;
@Override
public boolean supports(Integer loginType) {
return Integer.valueOf(1).equals(loginType);
}
@Override
public UserInfoDTO authenticate(CustomCredential credential)
throws GeneralSecurityException {
String username = credential.getUsername();
String password = credential.getPassword();
// 参数校验
// 数据库查询
// 密码比对
// 返回用户信息
return userInfo;
}
}
// 教学示例 - 短信验证码认证策略
public class SmsAuthStrategy implements AuthStrategy {
private final UserInfoService userInfoService;
private final SmsService smsService;
@Override
public boolean supports(Integer loginType) {
return Integer.valueOf(2).equals(loginType);
}
@Override
public UserInfoDTO authenticate(CustomCredential credential)
throws GeneralSecurityException {
String mobile = credential.getMobile();
String authCode = credential.getAuthCode();
// 验证短信验证码
// 根据手机号查询用户
// 返回用户信息
return userInfo;
}
}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
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
9.4 认证处理器整合
java
// 教学示例 - 整合多种认证策略的处理器
public class CustomAuthenticationHandler
extends AbstractPreAndPostProcessingAuthenticationHandler {
private final List<AuthStrategy> strategies;
private final CaptchaService captchaService;
@Override
protected AuthenticationHandlerExecutionResult doAuthentication(
Credential credential, Service service)
throws GeneralSecurityException, PreventedException {
CustomCredential customCred = (CustomCredential) credential;
// 1. 校验图形验证码
captchaService.verify(customCred.getCaptcha());
// 2. 根据loginType选择认证策略
AuthStrategy strategy = strategies.stream()
.filter(s -> s.supports(customCred.getLoginType()))
.findFirst()
.orElseThrow(() -> new AuthException(
"不支持的登录方式"));
// 3. 执行认证
UserInfoDTO userInfo = strategy.authenticate(customCred);
// 4. 构建Principal
Map<String, List<Object>> attributes = buildAttributes(userInfo);
return createHandlerResult(
credential,
this.principalFactory.createPrincipal(
userInfo.getName(), attributes),
new ArrayList<>()
);
}
}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
这种设计将不同的认证方式封装为独立的策略类,使得认证处理器本身保持简洁。当需要添加新的认证方式时,只需要实现新的AuthStrategy并注册到策略列表中,而不需要修改认证处理器的代码。这完美地体现了开闭原则(Open-Closed Principle)。
第十章 总结与展望
10.1 核心知识回顾
本文从CAS认证架构的核心接口体系出发,系统地讲解了自定义认证处理器的完整开发流程。让我们回顾一下关键的知识点:
- AuthenticationHandler接口是认证处理器的顶层契约,定义了
supports()和authenticate()两个核心方法。 - AbstractPreAndPostProcessingAuthenticationHandler提供了模板方法模式的支持,简化了认证处理器的开发。
- 自定义凭证类通过继承
UsernamePasswordCredential来扩展字段,支持多种登录方式。 - 认证配置注册通过
AuthenticationEventExecutionPlanConfigurer接口完成,不同版本有不同的配置方式。 - 密码安全需要关注存储安全(加盐哈希)、传输安全(HTTPS+前端加密)和验证安全(常量时间比较)。
- 属性传递通过Principal属性和AuthenticationAttributes实现,是连接认证与授权的桥梁。
- 审计与频率限制是认证系统安全防护的重要组成部分,需要采用分层策略。
10.2 CAS生态展望
Apereo CAS作为一个持续演进的开源项目,其发展方向值得关注:
- 云原生部署:CAS 7.x对容器化部署和Kubernetes的支持更加完善,包括配置热更新、水平扩展等。
- 密码less认证:随着FIDO2/WebAuthn标准的普及,CAS正在加强对无密码认证的支持。
- OIDC原生支持:CAS正在从以CAS协议为核心转向同时深度支持OpenID Connect协议。
- Delegated Authentication:委托认证(将认证委托给外部IdP)的能力不断增强,支持更复杂的联邦认证场景。
- 可观测性:对Micrometer、OpenTelemetry等可观测性标准的支持更加完善。
10.3 给开发者的建议
对于正在或即将基于CAS构建企业级统一身份认证的开发者,以下建议或许对你有所帮助:
深入理解CAS的架构设计思想,而不仅仅是照搬代码。CAS的认证架构是策略模式、模板方法模式、工厂模式等设计模式的经典应用,理解这些模式有助于你更好地定制和扩展CAS。
关注安全最佳实践。认证系统是安全防护的第一道防线,任何安全漏洞都可能导致严重的后果。始终保持对安全威胁的警惕,定期审查认证逻辑的安全性。
建立完善的测试体系。认证处理器应该有充分的单元测试和集成测试,覆盖各种正常和异常场景。特别是密码比对、异常处理等关键逻辑,需要重点测试。
做好版本规划和迁移准备。CAS的版本迭代较快,不同版本之间可能存在不兼容的变化。在项目初期就应该规划好版本升级路径,预留足够的迁移时间。
积极参与社区。Apereo CAS拥有活跃的社区和丰富的文档资源。遇到问题时,优先查阅官方文档和GitHub Issues,必要时可以向社区寻求帮助。
版权声明: 本文为必码(bima.cc)原创技术文章,仅供学习交流。
本文内容基于实际项目源码解析整理,代码示例均为教学简化版本,仅供学习参考。
文档内容提取自项目源码与配置文件,如需获取完整项目代码,请访问 bima.cc。