Appearance
CAS Overlay Template 7.x 版本指南
官方介绍
CAS Overlay Template 7.x 是 Apereo CAS 项目的官方推荐部署方式,基于 Spring Boot 3.x 构建。7.x 版本是 CAS 项目的最新重大版本,引入了许多现代化特性和改进,包括对 Java 17+ 的全面支持、原生 GraalVM 支持、响应式编程模型的进一步完善、以及更好的云原生支持。这个版本标志着 CAS 向现代化微服务和云原生架构的全面演进,采用了最新的技术栈和架构模式。
CAS (Central Authentication Service) 7.x 是一个企业级的单点登录系统,支持多种认证协议,包括 SAML、OAuth2、OpenID Connect、WS-Federation、SCIM、SAML2 等。
版本特性
核心特性
- Spring Boot 3.x 集成: 完全基于 Spring Boot 3.x,提供现代化部署和配置
- Java 17+ 支持: 全面支持 Java 17 及更高版本,利用最新的语言特性和性能改进
- GraalVM 原生镜像支持: 支持编译为原生镜像,提供更快的启动速度和更低的内存占用
- 响应式架构: 基于 Project Reactor 的完全响应式架构,提升性能和可扩展性
- 模块化架构: 更好的插件化扩展机制,支持微服务架构
- 协议支持: 支持 CAS、SAML、OAuth2、OpenID Connect、WS-Federation、SCIM
- 多认证源: 支持 LDAP、数据库、REST、JWT、Radius、OAuth、SAML 等多种认证方式
- 安全性: 内置高级安全机制,支持 MFA(多因素认证)、风险自适应认证
- 配置管理: 基于 Spring Cloud Config 和外部配置的集中配置管理
- 云原生支持: 原生支持 Kubernetes、Docker、Serverless 等云原生环境
- 可观测性: 集成 Micrometer、Prometheus、OpenTelemetry 等观测性工具
7.x 版本演进
- 7.0.x: 初始版本,全面迁移到 Spring Boot 3.x 和 Java 17+
- 7.1.x: 增强了 GraalVM 原生镜像支持,改进了安全性
- 7.2.x: 支持更多认证协议,改进了性能和监控
- 7.3.x: 增强了云原生支持,改进了开发体验
- 7.4.x: 最新稳定版本,增加了对 JWT、OAuth2.1、FAPI 的更好支持
官方获取地址
bash
# 通过 Git Clone 获取 CAS Overlay Template 7.x
git clone -b 7.0.x https://github.com/apereo/cas-overlay-template.git cas-overlay-7.x1
2
2
或者指定特定版本:
bash
git clone -b 7.0.0 https://github.com/apereo/cas-overlay-template.git cas-overlay-7.0.01
环境要求
- Java: JDK 17 或更高版本(推荐 JDK 17/21)
- Maven: 3.9.0 或更高版本
- Git: 用于版本控制
- 操作系统: 跨平台支持(Linux、Windows、macOS)
- GraalVM: 如需构建原生镜像(可选)
部署方法
1. 克隆项目
bash
git clone -b 7.0.x https://github.com/apereo/cas-overlay-template.git cas-overlay-7.x
cd cas-overlay-7.x1
2
2
2. 构建项目
bash
# 清理并构建项目
./mvnw clean compile
# 构建 JAR 包(Spring Boot 可执行 JAR)
./mvnw clean package
# 直接运行(开发模式)
./mvnw clean spring-boot:run
# 构建原生镜像(需要 GraalVM)
./mvnw clean native:compile -Pnative
# 或者使用包装器脚本
bash build.sh package
bash build.sh run1
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
3. 配置文件说明
CAS 7.x 使用以下配置文件:
application.properties
properties
# 服务器配置
server.port=8443
server.servlet.context-path=/cas
server.ssl.enabled=true
server.ssl.key-store=file:/etc/cas/thekeystore
server.ssl.key-store-password=changeit
server.ssl.key-store-type=JKS
server.ssl.key-alias=localhost
# CAS 服务器配置
cas.server.name=https://cas.example.org:8443
cas.server.prefix=https://cas.example.org:8443/cas
# 服务注册配置
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.location=classpath:/services
# 认证配置
cas.authn.accept.users=casuser::Mellon
# 日志配置
logging.config=classpath:logback-spring.xml
# Ticket 配置
cas.ticket.st.timeToKillInSeconds=10
cas.ticket.tgt.timeToKillInSeconds=7200
# Redis 配置
cas.ticket.registry.redis.host=localhost
cas.ticket.registry.redis.port=6379
# 集群配置
cas.cluster.machineName=localhost
cas.cluster.hostname=localhost
# 监控配置
management.endpoints.web.exposure.include=health,info,metrics,cas
# 安全配置
cas.authn.pm.enabled=true
cas.authn.pm.reset.mail.subject=Password Reset Request
# GraalVM 原生镜像配置
cas.native.enabled=false
cas.native.wait-for-session-replication=false
cas.native.refresh-delay=30s1
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
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
4. 使用 Docker 部署
bash
# 构建 Docker 镜像
docker build -t cas-overlay:7.x .
# 运行容器
docker run -p 8443:8443 -v /path/to/config:/etc/cas/config cas-overlay:7.x
# 使用 Docker Compose
docker-compose -f docker-compose.yml up -d1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
二次开发
自定义认证处理器
创建自定义认证处理器示例:
src/main/java/com/example/CustomAuthenticationHandler.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.exceptions.AccountDisabledException;
import org.apereo.cas.authentication.handler.support.AbstractPreAndPostProcessingAuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import javax.security.auth.login.AccountLockedException;
import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;
@Slf4j
@Component("customAuthenticationHandler")
public class CustomAuthenticationHandler extends AbstractPreAndPostProcessingAuthenticationHandler {
public CustomAuthenticationHandler() {
super();
}
@Autowired
public CustomAuthenticationHandler(@Qualifier("servicesManager") final ServicesManager servicesManager,
@Qualifier("defaultPrincipalFactory") final PrincipalFactory principalFactory) {
super(null, servicesManager, principalFactory, null);
}
@Override
protected AuthenticationHandlerExecutionResult doAuthentication(final Credential credential, final String originalPassword) throws GeneralSecurityException {
String username = credential.getId();
String password = originalPassword;
log.info("Attempting authentication for user: {}", username);
// 自定义认证逻辑
if (authenticateUser(username, password)) {
log.info("Authentication successful for user: {}", username);
return createHandlerResult(credential, this.principalFactory.createPrincipal(username), null);
} else {
log.warn("Authentication failed for user: {}", username);
throw new FailedLoginException("Authentication failed");
}
}
private boolean authenticateUser(String username, String password) {
// 实现您的认证逻辑
return "admin".equals(username) && "password".equals(password);
}
@Override
public boolean supports(Credential credential) {
return true; // 简化示例,实际应该检查凭证类型
}
}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
自定义属性解析器
src/main/java/com/example/CustomPersonAttributeDao.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.apereo.services.persondir.IPersonAttributes;
import org.apereo.services.persondir.support.NamedStubPersonAttributeDao;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Component("customPersonAttributeDao")
public class CustomPersonAttributeDao extends NamedStubPersonAttributeDao {
@Override
public IPersonAttributes getPerson(String uid) {
log.info("Retrieving attributes for user: {}", uid);
Map<String, List<Object>> attributes = new HashMap<>();
// 添加自定义属性
attributes.put("mail", new ArrayList<>(List.of(uid + "@example.org")));
attributes.put("displayName", new ArrayList<>(List.of("User " + uid)));
attributes.put("memberOf", new ArrayList<>(List.of("GROUP1", "GROUP2")));
attributes.put("department", new ArrayList<>(List.of("IT")));
attributes.put("employeeType", new ArrayList<>(List.of("Full-time")));
attributes.put("telephoneNumber", new ArrayList<>(List.of("+1-555-123-4567")));
log.debug("Returning attributes for user: {} - {}", uid, attributes.keySet());
return new org.apereo.services.persondir.support.StubPersonAttributeTO(uid, 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
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
自定义事件监听器
src/main/java/com/example/CustomCasEventListener.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.event.CasEventRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Component;
import java.util.concurrent.CompletableFuture;
@Slf4j
@Component("customCasEventListener")
public class CustomCasEventListener {
@Autowired(required = false)
private CasEventRepository casEventRepository;
@EventListener
@Async
public CompletableFuture<Void> handleCasAuthenticationSuccessEvent(
final org.apereo.cas.support.events.authentication.CasAuthenticationSuccessEvent event) {
log.info("Authentication success for: {}", event.getCredential().getId());
// 记录认证成功事件
if (casEventRepository != null) {
casEventRepository.save(event);
}
// 异步处理认证成功逻辑
return CompletableFuture.runAsync(() -> {
try {
Thread.sleep(100); // 模拟异步处理
log.debug("Processed authentication success for: {}", event.getCredential().getId());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Interrupted while processing authentication success", e);
}
});
}
@EventListener
public void handleCasAuthenticationFailureEvent(
final org.apereo.cas.support.events.authentication.CasAuthenticationFailureEvent event) {
log.warn("Authentication failure for: {}, reason: {}",
event.getCredential().getId(), event.getException().getMessage());
}
}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
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
自定义控制器
src/main/java/com/example/CustomController.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.CasProtocolConstants;
import org.apereo.cas.authentication.principal.Principal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
public class CustomController {
@GetMapping("/api/userinfo")
public Map<String, Object> getUserInfo(HttpServletRequest request) {
Map<String, Object> response = new HashMap<>();
// 从请求中获取认证的主体信息
Principal principal = (Principal) request.getAttribute(CasProtocolConstants.CONST_PARAM_PRINCIPAL);
if (principal != null) {
response.put("username", principal.getId());
response.put("attributes", principal.getAttributes());
response.put("message", "Successfully authenticated user");
log.info("User {} accessed userinfo endpoint", principal.getId());
} else {
response.put("error", "User not authenticated");
log.warn("Unauthenticated access attempt to userinfo endpoint");
}
return response;
}
@GetMapping("/api/health")
public Map<String, Object> getHealthInfo() {
Map<String, Object> response = new HashMap<>();
response.put("status", "UP");
response.put("version", "7.x");
response.put("timestamp", System.currentTimeMillis());
response.put("component", "Custom Health Endpoint");
log.debug("Health check performed");
return response;
}
}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
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
自定义视图
在 src/main/resources/templates 目录下可以自定义 CAS 的视图模板。例如,创建自定义登录页面:
src/main/resources/templates/casLoginView.html
html
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{layout}">
<head>
<meta charset="UTF-8"/>
<title>CAS Login</title>
<link rel="stylesheet" th:href="@{/css/cas.css}" />
<style>
.login-card {
max-width: 400px;
margin: 2rem auto;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
border-radius: 8px;
}
.form-control:focus {
border-color: #007bff;
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.btn-primary {
background-color: #007bff;
border-color: #007bff;
}
</style>
</head>
<body>
<main role="main" class="container mt-3 mb-3">
<div layout:fragment="content">
<div class="card login-card">
<div class="card-header bg-primary text-white text-center">
<h4><i class="fas fa-lock"></i> Secure Login</h4>
</div>
<div class="card-body">
<form method="post" th:object="${credential}" id="fm1" class="fm-v clearfix">
<div class="form-group row">
<label for="username" class="col-sm-12 col-form-label">Username:</label>
<div class="col-sm-12">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-user"></i></span>
</div>
<input class="form-control required" id="username" size="25" tabindex="1"
type="text" th:field="*{username}"
placeholder="Enter your username"
autocomplete="off" />
</div>
</div>
</div>
<div class="form-group row">
<label for="password" class="col-sm-12 col-form-label">Password:</label>
<div class="col-sm-12">
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
</div>
<input class="form-control required" id="password" size="25" tabindex="2"
type="password" th:field="*{password}"
placeholder="Enter your password"
autocomplete="off" />
</div>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12">
<input type="hidden" name="execution" th:value="${flowExecutionKey}" />
<input type="hidden" name="_eventId" value="submit" />
<input type="hidden" name="geolocation" />
<button class="btn btn-primary btn-block" name="submit" accesskey="l"
th:text="#{screen.welcome.button.login}"
tabindex="6" type="submit">
Sign In
</button>
</div>
</div>
<div class="form-group row">
<div class="col-sm-12 text-center">
<small>
<a href="#" th:text="#{screen.button.forgotpwd}" th:href="@{/forgotPassword}">Forgot Password?</a> |
<a href="#" th:text="#{screen.button.register}" th:href="@{/register}">Register</a>
</small>
</div>
</div>
</form>
</div>
</div>
<div class="text-center mt-3">
<small class="text-muted">
Powered by <a href="https://apereo.github.io/cas" target="_blank">Apereo CAS</a>
</small>
</div>
</div>
</main>
</body>
</html>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
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
配置示例
LDAP 认证配置
application.properties
properties
# LDAP 配置
cas.authn.ldap[0].type=AUTHENTICATED
cas.authn.ldap[0].ldapUrl=ldaps://ldap.example.org:636
cas.authn.ldap[0].baseDn=ou=people,dc=example,dc=org
cas.authn.ldap[0].userFilter=uid={user}
cas.authn.ldap[0].bindDn=cn=admin,dc=example,dc=org
cas.authn.ldap[0].bindCredential=password
cas.authn.ldap[0].principalAttributeList=uid,sn,givenName,mail,memberOf,employeeType
cas.authn.ldap[0].principalIdAttribute=uid
cas.authn.ldap[0].providerClass=org.ldaptive.provider.unboundid.UnboundIDProvider
cas.authn.ldap[0].useSsl=true
cas.authn.ldap[0].enhanceWithEntryResolver=true
# 连接池配置
cas.authn.ldap[0].minPoolSize=3
cas.authn.ldap[0].maxPoolSize=10
cas.authn.ldap[0].validateOnCheckout=true
cas.authn.ldap[0].validatePeriodically=true
cas.authn.ldap[0].blockWaitTime=3000
cas.authn.ldap[0].idleTime=6000
cas.authn.ldap[0].prunePeriod=6000
cas.authn.ldap[0].validatePeriod=6000
# 属性配置
cas.authn.ldap[0].returnAttributes=uid,cn,sn,givenName,mail,telephoneNumber,memberOf,employeeType1
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
数据库认证配置
application.properties
properties
# 数据库认证配置
cas.authn.jdbc.query[0].name=EXAMPLE_QUERY
cas.authn.jdbc.query[0].sql=SELECT password FROM users WHERE username=?
cas.authn.jdbc.query[0].healthQuery=SELECT 1
cas.authn.jdbc.query[0].isolateInternalQueries=false
cas.authn.jdbc.query[0].url=jdbc:mysql://localhost:3306/cas
cas.authn.jdbc.query[0].failFast=true
cas.authn.jdbc.query[0].isolationLevelName=ISOLATION_READ_COMMITTED
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.MySQL8Dialect
cas.authn.jdbc.query[0].leakThreshold=10
cas.authn.jdbc.query[0].propagationBehaviorName=PROPAGATION_REQUIRED
cas.authn.jdbc.query[0].batchSize=1
cas.authn.jdbc.query[0].user=database_user
cas.authn.jdbc.query[0].password=database_password
cas.authn.jdbc.query[0].driverClass=com.mysql.cj.jdbc.Driver
cas.authn.jdbc.query[0].idleTimeout=5000
# 连接池配置
cas.authn.jdbc.query[0].pool.suspension=false
cas.authn.jdbc.query[0].pool.minSize=6
cas.authn.jdbc.query[0].pool.maxSize=18
cas.authn.jdbc.query[0].pool.maxWait=2000
# 属性查询配置
cas.authn.jdbc.query[1].name=ATTRIBUTES_QUERY
cas.authn.jdbc.query[1].sql=SELECT uid, mail, department, telephoneNumber FROM users WHERE username=?
cas.authn.jdbc.query[1].fieldAttributeMappings.uid=uid
cas.authn.jdbc.query[1].fieldAttributeMappings.mail=mail
cas.authn.jdbc.query[1].fieldAttributeMappings.department=department
cas.authn.jdbc.query[1].fieldAttributeMappings.telephoneNumber=telephoneNumber
cas.authn.jdbc.query[1].singleRow=true1
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
OAuth2 客户端配置
application.properties
properties
# OAuth2 客户端配置
cas.authn.oauth.google.clientId=GOOGLE_CLIENT_ID
cas.authn.oauth.google.clientSecret=GOOGLE_CLIENT_SECRET
cas.authn.oauth.facebook.clientId=FACEBOOK_CLIENT_ID
cas.authn.oauth.facebook.clientSecret=FACEBOOK_CLIENT_SECRET
cas.authn.oauth.github.clientId=GITHUB_CLIENT_ID
cas.authn.oauth.github.clientSecret=GITHUB_CLIENT_SECRET
# OAuth2 服务配置
cas.authn.oauth.userProfileViewType=NESTED
cas.authn.oauth.callbackUrl=https://cas.example.org:8443/cas/oauth2/callbackAuthorize
# OAuth2 客户端注册
cas.authn.oauth.clients[0].clientId=web-client
cas.authn.oauth.clients[0].clientSecret=web-client-secret
cas.authn.oauth.clients[0].name=Web Client Application
cas.authn.oauth.clients[0].redirectUri=https://client.example.org/callback
cas.authn.oauth.clients[0].scopes=openid,profile,email
cas.authn.oauth.clients[0].grantTypes=authorization_code,refresh_token,password,client_credentials
cas.authn.oauth.clients[0].responseTypes=code,token,id_token1
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
OpenID Connect 配置
application.properties
properties
# OpenID Connect 配置
cas.authn.oidc.issuer=https://cas.example.org:8443/cas/oidc
cas.authn.oidc.jwksFile=file:/etc/cas/config/keystore.jwks
# OIDC 客户端注册
cas.authn.oidc.clients[0].clientId=oidc-client
cas.authn.oidc.clients[0].clientSecret=oidc-client-secret
cas.authn.oidc.clients[0].name=OIDC Client Application
cas.authn.oidc.clients[0].redirectUri=https://client.example.org/oidc-callback
cas.authn.oidc.clients[0].scopes=openid,profile,email,address,phone,offline_access
cas.authn.oidc.clients[0].grantTypes=authorization_code,refresh_token,password,client_credentials
cas.authn.oidc.clients[0].responseTypes=code,token,id_token
# JWT 配置
cas.authn.oidc.jwksEncryptionAlg=A256GCM
cas.authn.oidc.jwksEncryptionEncoding=BASE641
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
Demo 示例
简单的健康检查端点示例
创建一个简单的健康检查端点来演示 CAS 集成:
src/main/java/com/example/HealthCheckController.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.HealthIndicator;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
public class HealthCheckController implements HealthIndicator {
private boolean externalServiceAvailable = true;
@Override
public Health health() {
if (externalServiceAvailable) {
Map<String, String> details = new HashMap<>();
details.put("status", "UP");
details.put("component", "External Service Mock");
details.put("response_time", "50ms");
return Health.up().withDetails(details).build();
} else {
return Health.down().withDetail("reason", "External service unavailable").build();
}
}
@GetMapping("/api/custom-health")
public Map<String, Object> customHealthCheck() {
Map<String, Object> response = new HashMap<>();
response.put("status", "UP");
response.put("timestamp", System.currentTimeMillis());
response.put("version", "7.x");
response.put("uptime", "Running for several hours");
response.put("custom_component", "Healthy");
log.info("Custom health check performed");
return response;
}
@GetMapping("/api/system-info")
public Map<String, Object> systemInfo() {
Map<String, Object> response = new HashMap<>();
Runtime runtime = Runtime.getRuntime();
response.put("total_memory", runtime.totalMemory());
response.put("free_memory", runtime.freeMemory());
response.put("max_memory", runtime.maxMemory());
response.put("available_processors", runtime.availableProcessors());
response.put("java_version", System.getProperty("java.version"));
response.put("os_name", System.getProperty("os.name"));
response.put("cas_version", "7.x");
log.debug("System info retrieved");
return response;
}
}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
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
自定义服务注册示例
src/main/java/com/example/CustomServiceRegistry.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.services.RegisteredService;
import org.apereo.cas.services.ServiceRegistry;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
@Slf4j
@Component("customServiceRegistry")
public class CustomServiceRegistry implements ServiceRegistry {
private final List<RegisteredService> registeredServices = new ArrayList<>();
@Override
public RegisteredService save(RegisteredService registeredService) {
log.info("Saving registered service: {}", registeredService.getName());
// 检查是否已存在相同 ID 的服务
RegisteredService existing = findServiceById(registeredService.getId());
if (existing != null) {
registeredServices.remove(existing);
}
registeredServices.add(registeredService);
log.info("Saved service: {} with ID: {}", registeredService.getName(), registeredService.getId());
return registeredService;
}
@Override
public boolean delete(RegisteredService registeredService) {
log.info("Deleting registered service: {}", registeredService.getName());
return registeredServices.remove(registeredService);
}
@Override
public Collection<RegisteredService> load() {
log.info("Loading {} registered services", registeredServices.size());
return new ArrayList<>(registeredServices);
}
@Override
public RegisteredService findServiceById(long id) {
return registeredServices.stream()
.filter(service -> service.getId() == id)
.findFirst()
.orElse(null);
}
@Override
public RegisteredService findServiceById(String id) {
return registeredServices.stream()
.filter(service -> service.getServiceId().equals(id))
.findFirst()
.orElse(null);
}
@Override
public long size() {
return registeredServices.size();
}
public List<RegisteredService> findByServiceName(String serviceName) {
return registeredServices.stream()
.filter(service -> service.getName().contains(serviceName))
.collect(Collectors.toList());
}
}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
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
常见问题和解决方案
1. SSL 证书问题
如果遇到 SSL 证书问题,可以配置信任证书:
properties
# SSL 配置
cas.httpClient.trustStore.file=file:/etc/cas/truststore.jks
cas.httpClient.trustStore.psw=changeit
cas.httpClient.followRedirects=true
cas.httpClient.readTimeout=5000
cas.httpClient.connectionTimeout=50001
2
3
4
5
6
2
3
4
5
6
2. 认证失败问题
检查日志文件以确定认证失败的原因:
bash
tail -f /path/to/catalina.out1
3. 内存溢出问题
增加 JVM 内存设置:
bash
export JAVA_OPTS="-Xms2g -Xmx4g -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1024m"1
4. 服务注册问题
确保服务注册文件位于正确位置:
properties
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.location=classpath:/services
cas.serviceRegistry.schedule.repeatInterval=120
cas.serviceRegistry.managementType=DEFAULT1
2
3
4
2
3
4
服务注册文件示例 (src/main/resources/services/HTTPSandIMAPS-10000001.json):
json
{
"@class" : "org.apereo.cas.services.RegexRegisteredService",
"serviceId" : "^(https|imaps)://.*",
"name" : "HTTPS and IMAPS",
"id" : 10000001,
"evaluationOrder" : 10000,
"description" : "This service definition authorizes all application urls that support HTTPS and IMAPS protocols.",
"attributeReleasePolicy" : {
"@class" : "org.apereo.cas.services.ReturnAllAttributeReleasePolicy",
"authorizedToReleaseCredentialPassword" : false,
"authorizedToReleaseProxyGrantingTicket" : false
},
"accessStrategy" : {
"@class" : "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy",
"enabled" : true,
"ssoEnabled" : true,
"requireAllAttributes" : false
}
}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
性能优化
连接池配置
properties
# 数据库连接池
cas.jdbc.pool.platform=mysql
cas.jdbc.pool.minSize=2
cas.jdbc.pool.maxSize=10
cas.jdbc.pool.maxWait=2000
cas.jdbc.pool.acquireIncrement=2
cas.jdbc.pool.idleConnectionTestPeriod=30
cas.jdbc.pool.maxIdleTime=300
# LDAP 连接池
cas.authn.ldap[0].minPoolSize=3
cas.authn.ldap[0].maxPoolSize=10
cas.authn.ldap[0].validateOnCheckout=true
cas.authn.ldap[0].validatePeriodically=true
cas.authn.ldap[0].blockWaitTime=3000
cas.authn.ldap[0].idleTime=6000
cas.authn.ldap[0].prunePeriod=6000
cas.authn.ldap[0].validatePeriod=60001
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
缓存配置
properties
# Redis 缓存配置
cas.ticket.registry.redis.host=localhost
cas.ticket.registry.redis.port=6379
cas.ticket.registry.redis.password=
cas.ticket.registry.redis.database=0
cas.ticket.registry.redis.timeout=2000
cas.ticket.registry.redis.useSsl=false
cas.ticket.registry.redis.usePool=true
# Redis 连接池配置
cas.ticket.registry.redis.pool.maxTotal=20
cas.ticket.registry.redis.pool.maxIdle=8
cas.ticket.registry.redis.pool.minIdle=0
cas.ticket.registry.redis.pool.maxWaitMillis=3000
cas.ticket.registry.redis.pool.testOnCreate=false
cas.ticket.registry.redis.pool.testOnBorrow=false
cas.ticket.registry.redis.pool.testOnReturn=false
cas.ticket.registry.redis.pool.testWhileIdle=true
# Caffeine 缓存配置
cas.cache.core.tickets.caffeine.maxSize=1000
cas.cache.core.tickets.caffeine.initialCapacity=100
cas.cache.core.tickets.caffeine.expireAfterAccessSeconds=7200
cas.cache.core.tickets.caffeine.expireAfterWriteSeconds=72001
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
JVM 优化
bash
# 推荐的 JVM 参数
JAVA_OPTS="-Xms2g -Xmx4g \
-XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1024m \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:+UseStringDeduplication \
-XX:+OptimizeStringConcat \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8"1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
升级指南
从 6.x 升级到 7.x:
- 备份现有配置
- 更新 pom.xml 中的 CAS 版本到 7.x
- 检查废弃的配置属性,更新为新的属性名
- 更新服务注册文件格式(如有必要)
- 测试认证流程
- 部署到生产环境
Docker 部署示例
Dockerfile
dockerfile
FROM eclipse-temurin:17-jre-jammy
LABEL maintainer="Your Name <your.email@example.com>"
RUN apt-get update && apt-get install -y \
ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /etc/cas/config
COPY . /etc/cas/
EXPOSE 8443
CMD ["bash", "-c", "java -Djava.security.egd=file:/dev/./urandom -jar /etc/cas/webapp/cas.war"]
HEALTHCHECK --interval=30s --timeout=3s --start-period=60s --retries=3 \
CMD curl -f https://localhost:8443/cas/actuator/health || exit 11
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
Docker Compose 配置
yaml
version: '3.8'
services:
cas:
build: .
ports:
- "8443:8443"
volumes:
- ./config:/etc/cas/config
environment:
- CAS_SERVER_NAME=https://localhost:8443
- CAS_SERVER_PREFIX=https://localhost:8443/cas
- CAS_TICKET_REGISTRY_REDIS_HOST=redis
- SPRING_PROFILES_ACTIVE=redis
depends_on:
- redis
- ldap
- mysql
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
ldap:
image: osixia/openldap:1.5.0
ports:
- "389:389"
- "636:636"
environment:
- LDAP_ORGANISATION=Example Inc.
- LDAP_DOMAIN=example.org
- LDAP_ADMIN_PASSWORD=adminpassword
volumes:
- ldap_data:/var/lib/ldap
- ldap_config:/etc/ldap/slapd.d
mysql:
image: mysql:8.0
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=rootpassword
- MYSQL_DATABASE=cas
- MYSQL_USER=casuser
- MYSQL_PASSWORD=caspass
volumes:
- mysql_data:/var/lib/mysql
volumes:
redis_data:
ldap_data:
ldap_config:
mysql_data: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
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
Kubernetes 部署示例
Deployment 配置
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cas-server
labels:
app: cas-server
spec:
replicas: 2
selector:
matchLabels:
app: cas-server
template:
metadata:
labels:
app: cas-server
spec:
containers:
- name: cas
image: cas-overlay:7.x
ports:
- containerPort: 8443
env:
- name: CAS_SERVER_NAME
value: "https://cas.example.org:8443"
- name: CAS_SERVER_PREFIX
value: "https://cas.example.org:8443/cas"
- name: CAS_TICKET_REGISTRY_REDIS_HOST
value: "redis-service"
- name: SPRING_PROFILES_ACTIVE
value: "redis,kubernetes"
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /cas/actuator/health/liveness
port: 8443
scheme: HTTPS
initialDelaySeconds: 120
periodSeconds: 30
readinessProbe:
httpGet:
path: /cas/actuator/health/readiness
port: 8443
scheme: HTTPS
initialDelaySeconds: 30
periodSeconds: 10
---
apiVersion: v1
kind: Service
metadata:
name: cas-service
spec:
selector:
app: cas-server
ports:
- protocol: TCP
port: 443
targetPort: 8443
type: LoadBalancer1
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
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
总结
CAS Overlay Template 7.x 是一个功能强大且高度可定制的单点登录解决方案。它提供了现代化的架构、丰富的认证方式和灵活的配置选项,支持云原生部署。7.x 版本引入了对 Java 17+ 的全面支持、GraalVM 原生镜像支持、完全响应式架构以及更好的微服务集成能力,使其成为现代企业认证架构的理想选择。作为最新版本,它代表了 CAS 项目在现代化、性能和安全性方面的最高成就。
适用于 CAS 7.x 系列,基于 Spring Boot 3.x 构建。该版本强制要求 Java 17+,全面拥抱响应式架构和现代化云原生技术栈。
快速开始
要使用 CAS Overlay Template,请按照以下步骤:
- 选择合适的版本:根据您的需求选择对应的 CAS 版本
- 克隆模板:使用 Git 克隆相应的 Overlay 模板
- 自定义配置:修改配置文件以满足您的需求
- 添加自定义功能:根据需要添加自定义认证处理器或组件
- 构建和部署:构建项目并部署到您的环境中
最佳实践
- 保持 Overlay 项目与官方模板同步更新
- 将敏感配置外部化,不要硬编码在项目中
- 使用版本控制系统管理自定义代码
- 定期测试升级路径,确保能够平滑升级
支持资源
properties
# 服务器配置
server.port=8443
server.servlet.context-path=/cas
server.error.include-message=always
server.error.include-binding-errors=always
# CAS 服务器配置
cas.server.name=https://cas.example.org:8443
cas.server.prefix=https://cas.example.org:8443/cas
# 服务注册配置
cas.service-registry.jpa.enabled=true
cas.service-registry.jpa.ddl-auto=create-drop
cas.service-registry.jpa.driver-class=org.postgresql.Driver
cas.service-registry.jpa.url=jdbc:postgresql://localhost:5432/cas
cas.service-registry.jpa.username=casuser
cas.service-registry.jpa.password=caspass
# 认证配置
cas.authn.accept.users=casuser::Mellon
# 日志配置
logging.config=classpath:log4j2.xml
logging.level.org.apereo.cas=DEBUG
logging.level.org.springframework=INFO
# Ticket 配置
cas.ticket.st.time-to-kill-in-seconds=10
cas.ticket.tgt.time-to-kill-in-seconds=7200
# Redis 配置
cas.ticket.registry.redis.host=localhost
cas.ticket.registry.redis.port=6379
cas.ticket.registry.redis.password=
# 集群配置
cas.cluster.machine-name=localhost
cas.cluster.hostname=localhost
# 监控配置
management.endpoints.web.exposure.include=health,info,metrics,cas,logfile
management.endpoint.health.show-details=always
management.info.build.enabled=true
management.info.git.mode=full
# 安全配置
cas.authn.pm.enabled=true
cas.authn.pm.reset.mail.subject=Password Reset Request
cas.authn.mfa.groovy.location=classpath:MfaTrigger.groovy
# OAuth2/OpenID Connect 配置
cas.authn.oauth.core.user-profile-view-type=NESTED
cas.authn.oidc.jwks-file=file:/etc/cas/config/keystore.jwks
cas.authn.oidc.issuer=https://cas.example.org:8443/cas/oidc1
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
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
4. 使用 Docker 部署
bash
# 构建 Docker 镜像
docker build -t cas-overlay:7.x .
# 运行容器
docker run -p 8443:8443 -v /path/to/config:/etc/cas/config cas-overlay:7.x
# 使用 Docker Compose
docker-compose -f docker-compose.yml up -d1
2
3
4
5
6
7
8
2
3
4
5
6
7
8
二次开发
自定义认证处理器
创建自定义响应式认证处理器示例:
src/main/java/com/example/CustomReactiveAuthenticationHandler.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult;
import org.apereo.cas.authentication.Credential;
import org.apereo.cas.authentication.exceptions.AccountDisabledException;
import org.apereo.cas.authentication.handler.support.AbstractPreAndPostProcessingAuthenticationHandler;
import org.apereo.cas.authentication.principal.PrincipalFactory;
import org.apereo.cas.services.ServicesManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import javax.security.auth.login.AccountLockedException;
import javax.security.auth.login.AccountNotFoundException;
import javax.security.auth.login.FailedLoginException;
import java.security.GeneralSecurityException;
@Slf4j
@Component("customReactiveAuthenticationHandler")
public class CustomReactiveAuthenticationHandler extends AbstractPreAndPostProcessingAuthenticationHandler {
public CustomReactiveAuthenticationHandler() {
super();
}
@Autowired
public CustomReactiveAuthenticationHandler(@Qualifier("servicesManager") final ServicesManager servicesManager,
@Qualifier("defaultPrincipalFactory") final PrincipalFactory principalFactory) {
super(null, servicesManager, principalFactory, null);
}
@Override
protected Mono<AuthenticationHandlerExecutionResult> doAuthentication(final Credential credential) {
String username = credential.getId();
String password = getPassword(credential);
log.info("Attempting reactive authentication for user: {}", username);
// 响应式认证逻辑
return Mono.fromCallable(() -> {
if (authenticateUser(username, password)) {
log.info("Reactive authentication successful for user: {}", username);
return createHandlerResult(credential, this.principalFactory.createPrincipal(username), null);
} else {
log.warn("Reactive authentication failed for user: {}", username);
throw new FailedLoginException("Authentication failed");
}
}).onErrorMap(Exception::class.java, ex -> new FailedLoginException(ex.getMessage()));
}
private String getPassword(Credential credential) {
// 提取密码逻辑
if (credential instanceof org.apereo.cas.authentication.UsernamePasswordCredential) {
return ((org.apereo.cas.authentication.UsernamePasswordCredential) credential).getPassword();
}
return null;
}
private boolean authenticateUser(String username, String password) {
// 实现您的认证逻辑
return "admin".equals(username) && "password".equals(password);
}
@Override
public boolean supports(final Credential credential) {
return true; // 简化示例,实际应该检查凭证类型
}
}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
自定义属性解析器(响应式)
src/main/java/com/example/CustomReactivePersonAttributeDao.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.apereo.services.persondir.IPersonAttributes;
import org.apereo.services.persondir.support.NamedStubPersonAttributeDao;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@Component("customReactivePersonAttributeDao")
public class CustomReactivePersonAttributeDao extends NamedStubPersonAttributeDao {
public Mono<IPersonAttributes> getPersonAttributes(final String uid) {
log.info("Reactive retrieving attributes for user: {}", uid);
return Mono.fromCallable(() -> {
Map<String, List<Object>> attributes = new HashMap<>();
// 添加自定义属性
attributes.put("mail", new ArrayList<>(List.of(uid + "@example.org")));
attributes.put("displayName", new ArrayList<>(List.of("User " + uid)));
attributes.put("memberOf", new ArrayList<>(List.of("GROUP1", "GROUP2")));
attributes.put("department", new ArrayList<>(List.of("IT")));
attributes.put("employeeType", new ArrayList<>(List.of("Full-time")));
attributes.put("telephoneNumber", new ArrayList<>(List.of("+1-555-123-4567")));
attributes.put("lastLoginTime", new ArrayList<>(List.of(System.currentTimeMillis())));
log.debug("Returning reactive attributes for user: {} - {}", uid, attributes.keySet());
return new org.apereo.services.persondir.support.StubPersonAttributeTO(uid, attributes);
});
}
@Override
public IPersonAttributes getPerson(String uid) {
return getPersonAttributes(uid).block(); // 为了向后兼容
}
}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
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
自定义事件监听器(响应式)
src/main/java/com/example/CustomReactiveCasEventListener.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.event.CasEventRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.event.EventListener;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Mono;
@Component("customReactiveCasEventListener")
@Slf4j
public class CustomReactiveCasEventListener {
@Autowired(required = false)
private CasEventRepository casEventRepository;
@EventListener
public Mono<Void> handleCasAuthenticationSuccessEvent(
final org.apereo.cas.support.events.authentication.CasAuthenticationSuccessEvent event) {
return Mono.fromRunnable(() -> {
log.info("Reactive authentication success for: {}", event.getCredential().getId());
// 记录认证成功事件
if (casEventRepository != null) {
casEventRepository.save(event);
}
}).then();
}
@EventListener
public Mono<Void> handleCasAuthenticationFailureEvent(
final org.apereo.cas.support.events.authentication.CasAuthenticationFailureEvent event) {
return Mono.fromRunnable(() -> {
log.warn("Reactive authentication failure for: {}, reason: {}",
event.getCredential().getId(), event.getException().getMessage());
}).then();
}
}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
自定义控制器(响应式)
src/main/java/com/example/CustomReactiveController.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.CasProtocolConstants;
import org.apereo.cas.authentication.principal.Principal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
public class CustomReactiveController {
@GetMapping("/api/reactive/userinfo")
public Mono<Map<String, Object>> getReactiveUserInfo(org.springframework.web.server.ServerWebExchange exchange) {
return Mono.fromCallable(() -> {
Map<String, Object> response = new HashMap<>();
// 从请求中获取认证的主体信息
Principal principal = exchange.getAttribute(CasProtocolConstants.CONST_PARAM_PRINCIPAL);
if (principal != null) {
response.put("username", principal.getId());
response.put("attributes", principal.getAttributes());
response.put("message", "Successfully authenticated user");
log.info("User {} accessed reactive userinfo endpoint", principal.getId());
} else {
response.put("error", "User not authenticated");
log.warn("Unauthenticated access attempt to reactive userinfo endpoint");
}
return response;
});
}
@GetMapping("/api/reactive/health")
public Mono<Map<String, Object>> getReactiveHealthInfo() {
return Mono.fromCallable(() -> {
Map<String, Object> response = new HashMap<>();
response.put("status", "UP");
response.put("version", "7.x");
response.put("timestamp", System.currentTimeMillis());
response.put("component", "Custom Reactive Health Endpoint");
response.put("reactive", true);
log.debug("Reactive health check performed");
return response;
});
}
}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
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
自定义视图
在 src/main/resources/templates 目录下可以自定义 CAS 的视图模板。例如,创建现代化的自定义登录页面:
src/main/resources/templates/casLoginView.html
html
<!DOCTYPE html>
<html xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout"
xmlns:th="http://www.thymeleaf.org"
layout:decorate="~{layout}">
<head>
<meta charset="UTF-8"/>
<title>CAS Login</title>
<link rel="stylesheet" th:href="@{/css/cas.css}" />
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" />
<style>
:root {
--primary-color: #007bff;
--secondary-color: #6c757d;
--success-color: #28a745;
--danger-color: #dc3545;
--warning-color: #ffc107;
--light-color: #f8f9fa;
--dark-color: #343a40;
}
.login-container {
display: flex;
min-height: 100vh;
}
.login-left-panel {
flex: 1;
background: linear-gradient(135deg, var(--primary-color), #0056b3);
color: white;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 2rem;
}
.login-right-panel {
flex: 1;
display: flex;
justify-content: center;
align-items: center;
padding: 2rem;
background-color: #f8f9fa;
}
.login-card {
width: 100%;
max-width: 400px;
background: white;
border-radius: 10px;
box-shadow: 0 10px 30px rgba(0, 0, 0, 0.1);
overflow: hidden;
}
.card-header {
background-color: var(--primary-color);
color: white;
padding: 1.5rem;
text-align: center;
}
.card-body {
padding: 2rem;
}
.form-control {
height: calc(1.5em + .75rem + 2px);
padding: .375rem .75rem;
font-size: 1rem;
border: 1px solid #ced4da;
border-radius: .25rem;
transition: border-color .15s ease-in-out,box-shadow .15s ease-in-out;
}
.form-control:focus {
border-color: var(--primary-color);
box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);
}
.btn-primary {
background-color: var(--primary-color);
border-color: var(--primary-color);
padding: .375rem .75rem;
font-size: 1rem;
border-radius: .25rem;
cursor: pointer;
width: 100%;
}
.btn-primary:hover {
background-color: #0056b3;
border-color: #004085;
}
.input-group {
position: relative;
display: flex;
flex-wrap: wrap;
align-items: stretch;
width: 100%;
}
.input-group-prepend {
margin-right: -1px;
}
.input-group-text {
display: flex;
align-items: center;
padding: .375rem .75rem;
margin-bottom: 0;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
text-align: center;
white-space: nowrap;
background-color: #e9ecef;
border: 1px solid #ced4da;
border-radius: .25rem 0 0 .25rem;
}
.form-control {
position: relative;
flex: 1 1 auto;
width: 1%;
min-width: 0;
margin-bottom: 0;
border-radius: 0 .25rem .25rem 0;
}
.divider {
text-align: center;
position: relative;
margin: 1.5rem 0;
}
.divider::before {
content: '';
position: absolute;
top: 50%;
left: 0;
right: 0;
height: 1px;
background-color: #dee2e6;
z-index: 1;
}
.divider span {
position: relative;
z-index: 2;
display: inline-block;
padding: 0 .5rem;
background-color: #f8f9fa;
color: #6c757d;
}
.social-login {
display: flex;
gap: 1rem;
margin-top: 1rem;
}
.social-btn {
flex: 1;
padding: .5rem;
border: 1px solid #ddd;
border-radius: .25rem;
text-align: center;
cursor: pointer;
}
.social-btn i {
margin-right: .5rem;
}
@media (max-width: 768px) {
.login-container {
flex-direction: column;
}
.login-left-panel {
display: none;
}
.login-right-panel {
padding: 1rem;
}
}
</style>
</head>
<body>
<div class="login-container">
<div class="login-left-panel d-none d-md-flex">
<div class="text-center">
<h2><i class="fas fa-shield-alt fa-3x mb-3"></i></h2>
<h3>Secure Access Platform</h3>
<p class="mt-3">Enterprise-grade authentication and authorization services</p>
<ul class="list-unstyled mt-4 text-left">
<li><i class="fas fa-check-circle text-success mr-2"></i> Single Sign-On</li>
<li><i class="fas fa-check-circle text-success mr-2"></i> Multi-Factor Authentication</li>
<li><i class="fas fa-check-circle text-success mr-2"></i> Social Login Integration</li>
<li><i class="fas fa-check-circle text-success mr-2"></i> Enterprise Security</li>
</ul>
</div>
</div>
<div class="login-right-panel">
<div class="login-card">
<div class="card-header">
<h4><i class="fas fa-lock"></i> Secure Login</h4>
<p class="mb-0">Please sign in to continue</p>
</div>
<div class="card-body">
<form method="post" th:object="${credential}" id="fm1" class="fm-v clearfix">
<div class="form-group mb-3">
<label for="username" class="form-label">Username</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-user"></i></span>
</div>
<input class="form-control required" id="username" size="25" tabindex="1"
type="text" th:field="*{username}"
placeholder="Enter your username"
autocomplete="off" />
</div>
</div>
<div class="form-group mb-3">
<label for="password" class="form-label">Password</label>
<div class="input-group">
<div class="input-group-prepend">
<span class="input-group-text"><i class="fas fa-lock"></i></span>
</div>
<input class="form-control required" id="password" size="25" tabindex="2"
type="password" th:field="*{password}"
placeholder="Enter your password"
autocomplete="off" />
</div>
</div>
<div class="form-group form-check mb-3">
<input type="checkbox" class="form-check-input" id="rememberMe" name="rememberMe" />
<label class="form-check-label" for="rememberMe">Remember me</label>
</div>
<div class="form-group mb-3">
<input type="hidden" name="execution" th:value="${flowExecutionKey}" />
<input type="hidden" name="_eventId" value="submit" />
<input type="hidden" name="geolocation" />
<button class="btn btn-primary" name="submit" accesskey="l"
th:text="#{screen.welcome.button.login}"
tabindex="6" type="submit">
Sign In
</button>
</div>
<div class="divider">
<span>Or continue with</span>
</div>
<div class="social-login">
<div class="social-btn">
<i class="fab fa-google"></i> Google
</div>
<div class="social-btn">
<i class="fab fa-facebook-f"></i> Facebook
</div>
<div class="social-btn">
<i class="fab fa-github"></i> GitHub
</div>
</div>
<div class="text-center mt-3">
<small>
<a href="#" th:text="#{screen.button.forgotpwd}" th:href="@{/pwdreset}" class="text-decoration-none">Forgot Password?</a> |
<a href="#" th:text="#{screen.button.register}" th:href="@{/register}" class="text-decoration-none">Register</a>
</small>
</div>
</form>
</div>
</div>
</div>
</div>
<footer class="bg-light text-center py-3 mt-5">
<small class="text-muted">
© 2023 Secure Access Platform | Powered by <a href="https://apereo.github.io/cas" target="_blank">Apereo CAS 7.x</a>
</small>
</footer>
</body>
</html>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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
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
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
配置示例
LDAP 认证配置(响应式)
application.properties
properties
# LDAP 配置
cas.authn.ldap[0].type=AUTHENTICATED
cas.authn.ldap[0].ldap-url=ldaps://ldap.example.org:636
cas.authn.ldap[0].base-dn=ou=people,dc=example,dc=org
cas.authn.ldap[0].user-filter=uid={user}
cas.authn.ldap[0].bind-dn=cn=admin,dc=example,dc=org
cas.authn.ldap[0].bind-credential=password
cas.authn.ldap[0].principal-attribute-list=uid,sn,givenName,mail,memberOf,employeeType,telephoneNumber
cas.authn.ldap[0].principal-id-attribute=uid
cas.authn.ldap[0].provider-class=org.ldaptive.provider.unboundid.UnboundIDProvider
cas.authn.ldap[0].use-ssl=true
cas.authn.ldap[0].enhance-with-entry-resolver=true
cas.authn.ldap[0].validate-credentials-on-authn=true
cas.authn.ldap[0].allow-missing-handler=false
# 连接池配置
cas.authn.ldap[0].min-pool-size=5
cas.authn.ldap[0].max-pool-size=15
cas.authn.ldap[0].validate-on-checkout=true
cas.authn.ldap[0].validate-periodically=true
cas.authn.ldap[0].block-wait-time=3000
cas.authn.ldap[0].idle-time=6000
cas.authn.ldap[0].prune-period=6000
cas.authn.ldap[0].validate-period=6000
# 属性配置
cas.authn.ldap[0].return-attributes=uid,cn,sn,givenName,mail,telephoneNumber,memberOf,employeeType,createTimestamp,modifyTimestamp
cas.authn.ldap[0].subtree-search=true
cas.authn.ldap[0].paged-results-enabled=true
cas.authn.ldap[0].page-size=100
# 响应式配置
cas.authn.ldap[0].reactive=true
cas.authn.ldap[0].async-operations-enabled=true1
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
数据库认证配置(响应式)
application.properties
properties
# 数据库认证配置
cas.authn.jdbc.query[0].name=EXAMPLE_QUERY
cas.authn.jdbc.query[0].sql=SELECT password FROM users WHERE username=?
cas.authn.jdbc.query[0].health-query=SELECT 1
cas.authn.jdbc.query[0].isolate-internal-queries=false
cas.authn.jdbc.query[0].url=jdbc:postgresql://localhost:5432/cas
cas.authn.jdbc.query[0].fail-fast=true
cas.authn.jdbc.query[0].isolation-level-name=ISOLATION_READ_COMMITTED
cas.authn.jdbc.query[0].dialect=org.hibernate.dialect.PostgreSQL10Dialect
cas.authn.jdbc.query[0].leak-threshold=10
cas.authn.jdbc.query[0].propagation-behavior-name=PROPAGATION_REQUIRED
cas.authn.jdbc.query[0].batch-size=1
cas.authn.jdbc.query[0].user=casuser
cas.authn.jdbc.query[0].password=caspass
cas.authn.jdbc.query[0].driver-class=org.postgresql.Driver
cas.authn.jdbc.query[0].idle-timeout=5000
# 连接池配置
cas.authn.jdbc.query[0].pool.suspension=false
cas.authn.jdbc.query[0].pool.min-size=6
cas.authn.jdbc.query[0].pool.max-size=18
cas.authn.jdbc.query[0].pool.max-wait=2000
# 属性查询配置
cas.authn.jdbc.query[1].name=ATTRIBUTES_QUERY
cas.authn.jdbc.query[1].sql=SELECT username, email, department, phone FROM users WHERE username=?
cas.authn.jdbc.query[1].field-attribute-mappings.username=uid
cas.authn.jdbc.query[1].field-attribute-mappings.email=mail
cas.authn.jdbc.query[1].field-attribute-mappings.department=department
cas.authn.jdbc.query[1].field-attribute-mappings.phone=telephoneNumber
cas.authn.jdbc.query[1].single-row=true
# 响应式配置
cas.authn.jdbc.reactive=true
cas.authn.jdbc.async-operations-enabled=true1
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
OAuth2 客户端配置(响应式)
application.properties
properties
# OAuth2 客户端配置
cas.authn.oauth.google.client-id=GOOGLE_CLIENT_ID
cas.authn.oauth.google.client-secret=GOOGLE_CLIENT_SECRET
cas.authn.oauth.facebook.client-id=FACEBOOK_CLIENT_ID
cas.authn.oauth.facebook.client-secret=FACEBOOK_CLIENT_SECRET
cas.authn.oauth.github.client-id=GITHUB_CLIENT_ID
cas.authn.oauth.github.client-secret=GITHUB_CLIENT_SECRET
# OAuth2 服务配置
cas.authn.oauth.core.user-profile-view-type=NESTED
cas.authn.oauth.callback-url=https://cas.example.org:8443/cas/oauth2/callbackAuthorize
# OAuth2 客户端注册
cas.authn.oauth.clients[0].client-id=web-client
cas.authn.oauth.clients[0].client-secret=web-client-secret
cas.authn.oauth.clients[0].name=Web Client Application
cas.authn.oauth.clients[0].redirect-uri=https://client.example.org/callback
cas.authn.oauth.clients[0].scopes=openid,profile,email
cas.authn.oauth.clients[0].grant-types=authorization_code,refresh_token,password,client_credentials
cas.authn.oauth.clients[0].response-types=code,token,id_token
# OIDC 配置
cas.authn.oidc.issuer=https://cas.example.org:8443/cas/oidc
cas.authn.oidc.jwks-file=file:/etc/cas/config/keystore.jwks
cas.authn.oidc.scopes=openid,profile,email,address,phone,offline_access,groups
# OIDC 客户端注册
cas.authn.oidc.clients[0].client-id=oidc-client
cas.authn.oidc.clients[0].client-secret=oidc-client-secret
cas.authn.oidc.clients[0].name=OIDC Client Application
cas.authn.oidc.clients[0].redirect-uri=https://client.example.org/oidc-callback
cas.authn.oidc.clients[0].scopes=openid,profile,email,address,phone,offline_access
cas.authn.oidc.clients[0].grant-types=authorization_code,refresh_token,password,client_credentials
cas.authn.oidc.clients[0].response-types=code,token,id_token
# JWT 配置
cas.authn.oidc.jwks-encryption-alg=A256GCM
cas.authn.oidc.jwks-encryption-encoding=BASE64
cas.authn.oidc.jwks-cache-in-minutes=601
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
WebAuthn 配置
application.properties
properties
# WebAuthn 配置
cas.authn.mfa.webauthn.core.allowed-origins=https://cas.example.org:8443
cas.authn.mfa.webauthn.core.application-origin=https://cas.example.org:8443
cas.authn.mfa.webauthn.core.rp-entity-name=CAS WebAuthn Server
cas.authn.mfa.webauthn.core.rp-id=cas.example.org
cas.authn.mfa.webauthn.core.timeout=60000
cas.authn.mfa.webauthn.core.attestation-conveyance-mode=none
cas.authn.mfa.webauthn.core.user-verification=preferred
cas.authn.mfa.webauthn.core.authenticator.attachment=any
cas.authn.mfa.webauthn.core.resident-key-required=false
cas.authn.mfa.webauthn.core.exclude-credential-storage=false
cas.authn.mfa.webauthn.core.allow-passkeys=true
# WebAuthn 存储配置
cas.authn.mfa.webauthn.jpa.enabled=true
cas.authn.mfa.webauthn.jpa.ddl-auto=update
cas.authn.mfa.webauthn.jpa.driver-class=org.postgresql.Driver
cas.authn.mfa.webauthn.jpa.url=jdbc:postgresql://localhost:5432/cas
cas.authn.mfa.webauthn.jpa.username=casuser
cas.authn.mfa.webauthn.jpa.password=caspass1
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
Demo 示例
响应式健康检查端点示例
创建一个响应式健康检查端点来演示 CAS 7.x 的新特性:
src/main/java/com/example/ReactiveHealthCheckController.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.actuate.health.Health;
import org.springframework.boot.actuate.health.ReactiveHealthIndicator;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Mono;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@RestController
public class ReactiveHealthCheckController implements ReactiveHealthIndicator {
private boolean externalServiceAvailable = true;
@Override
public Mono<Health> health() {
return Mono.fromCallable(() -> {
if (externalServiceAvailable) {
Map<String, String> details = new HashMap<>();
details.put("status", "UP");
details.put("component", "External Service Mock");
details.put("response_time", "50ms");
details.put("version", "7.x");
return Health.up().withDetails(details).build();
} else {
return Health.down().withDetail("reason", "External service unavailable").build();
}
});
}
@GetMapping("/api/reactive/custom-health")
public Mono<Map<String, Object>> customReactiveHealthCheck() {
return Mono.fromCallable(() -> {
Map<String, Object> response = new HashMap<>();
response.put("status", "UP");
response.put("timestamp", System.currentTimeMillis());
response.put("version", "7.x");
response.put("uptime", "Running for several hours");
response.put("custom_component", "Healthy");
response.put("reactive", true);
response.put("java_version", System.getProperty("java.version"));
log.info("Custom reactive health check performed");
return response;
});
}
@GetMapping("/api/reactive/system-info")
public Mono<Map<String, Object>> reactiveSystemInfo() {
return Mono.fromCallable(() -> {
Map<String, Object> response = new HashMap<>();
Runtime runtime = Runtime.getRuntime();
response.put("total_memory", runtime.totalMemory());
response.put("free_memory", runtime.freeMemory());
response.put("max_memory", runtime.maxMemory());
response.put("available_processors", runtime.availableProcessors());
response.put("java_version", System.getProperty("java.version"));
response.put("java_vendor", System.getProperty("java.vendor"));
response.put("os_name", System.getProperty("os.name"));
response.put("os_version", System.getProperty("os.version"));
response.put("cas_version", "7.x");
response.put("reactive_support", true);
log.debug("Reactive system info retrieved");
return response;
});
}
}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
自定义响应式服务注册示例
src/main/java/com/example/CustomReactiveServiceRegistry.java
java
package com.example;
import lombok.extern.slf4j.Slf4j;
import org.apereo.cas.services.RegisteredService;
import org.apereo.cas.services.ReactiveServiceRegistry;
import org.springframework.stereotype.Component;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
@Slf4j
@Component("customReactiveServiceRegistry")
public class CustomReactiveServiceRegistry implements ReactiveServiceRegistry {
private final ConcurrentMap<Long, RegisteredService> registeredServices = new ConcurrentHashMap<>();
@Override
public Mono<RegisteredService> save(final RegisteredService registeredService) {
return Mono.fromCallable(() -> {
log.info("Saving registered service: {}", registeredService.getName());
registeredServices.put(registeredService.getId(), registeredService);
log.info("Saved service: {} with ID: {}", registeredService.getName(), registeredService.getId());
return registeredService;
});
}
@Override
public Mono<Boolean> delete(final RegisteredService registeredService) {
return Mono.fromCallable(() -> {
log.info("Deleting registered service: {}", registeredService.getName());
return registeredServices.remove(registeredService.getId()) != null;
});
}
@Override
public Flux<RegisteredService> load() {
return Flux.fromIterable(registeredServices.values())
.doOnSubscribe(subscription -> log.info("Loading {} registered services", registeredServices.size()));
}
@Override
public Mono<RegisteredService> findServiceById(final long id) {
return Mono.fromCallable(() -> registeredServices.get(id))
.doOnNext(service -> {
if (service != null) {
log.debug("Found service by ID {}: {}", id, service.getName());
} else {
log.debug("Service not found by ID: {}", id);
}
});
}
@Override
public Mono<RegisteredService> findServiceById(final String id) {
return load()
.filter(service -> service.getServiceId().equals(id))
.next()
.doOnNext(service -> {
if (service != null) {
log.debug("Found service by service ID {}: {}", id, service.getName());
} else {
log.debug("Service not found by service ID: {}", id);
}
});
}
@Override
public Mono<Long> size() {
return Mono.fromCallable(() -> (long) registeredServices.size())
.doOnNext(size -> log.info("Current service registry size: {}", size));
}
public Flux<RegisteredService> findByServiceName(final String serviceName) {
return load()
.filter(service -> service.getName().toLowerCase().contains(serviceName.toLowerCase()));
}
}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
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
常见问题和解决方案
1. Java 版本兼容性问题
CAS 7.x 要求 Java 17+,确保使用正确的 Java 版本:
bash
java -version
# 应该显示 Java 17 或更高版本1
2
2
2. SSL 证书问题
配置信任证书:
properties
# SSL 配置
cas.http-client.truststore.file=file:/etc/cas/truststore.jks
cas.http-client.truststore.psw=changeit
cas.http-client.follow-redirects=true
cas.http-client.read-timeout=5000
cas.http-client.connect-timeout=50001
2
3
4
5
6
2
3
4
5
6
3. 认证失败问题
启用详细日志来调试:
properties
logging.level.org.apereo.cas.authentication=DEBUG
logging.level.org.apereo.cas.web.flow=DEBUG
logging.level.org.springframework.security=DEBUG1
2
3
2
3
4. 内存配置问题
由于 CAS 7.x 是完全响应式的,需要适当配置内存:
bash
export JAVA_OPTS="-Xms1g -Xmx2g \
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:+UseStringDeduplication \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8"1
2
3
4
5
6
2
3
4
5
6
5. 服务注册问题
CAS 7.x 推荐使用数据库作为服务注册存储:
properties
cas.service-registry.jpa.enabled=true
cas.service-registry.jpa.ddl-auto=update
cas.service-registry.jpa.driver-class=org.postgresql.Driver
cas.service-registry.jpa.url=jdbc:postgresql://localhost:5432/cas
cas.service-registry.jpa.username=casuser
cas.service-registry.jpa.password=caspass1
2
3
4
5
6
2
3
4
5
6
性能优化
连接池配置
properties
# 数据库连接池
cas.jdbc.pool.platform=postgresql
cas.jdbc.pool.min-size=5
cas.jdbc.pool.max-size=20
cas.jdbc.pool.max-wait=3000
cas.jdbc.pool.acquire-increment=2
cas.jdbc.pool.idle-connection-test-period=30
cas.jdbc.pool.max-idle-time=300
# LDAP 连接池
cas.authn.ldap[0].min-pool-size=5
cas.authn.ldap[0].max-pool-size=15
cas.authn.ldap[0].validate-on-checkout=true
cas.authn.ldap[0].validate-periodically=true
cas.authn.ldap[0].block-wait-time=3000
cas.authn.ldap[0].idle-time=6000
cas.authn.ldap[0].prune-period=6000
cas.authn.ldap[0].validate-period=60001
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
缓存配置
properties
# Redis 缓存配置
cas.ticket.registry.redis.host=localhost
cas.ticket.registry.redis.port=6379
cas.ticket.registry.redis.password=
cas.ticket.registry.redis.database=0
cas.ticket.registry.redis.timeout=2000
cas.ticket.registry.redis.use-ssl=false
cas.ticket.registry.redis.use-pool=true
# Redis 连接池配置
cas.ticket.registry.redis.pool.max-total=20
cas.ticket.registry.redis.pool.max-idle=8
cas.ticket.registry.redis.pool.min-idle=0
cas.ticket.registry.redis.pool.max-wait-millis=3000
cas.ticket.registry.redis.pool.test-on-create=false
cas.ticket.registry.redis.pool.test-on-borrow=false
cas.ticket.registry.redis.pool.test-on-return=false
cas.ticket.registry.redis.pool.test-while-idle=true
# 响应式缓存配置
cas.cache.tickets.redis.reactive=true
cas.cache.tickets.redis.cluster-enabled=false
cas.cache.tickets.redis.sentinel-enabled=false1
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
JVM 优化(针对响应式)
bash
# 针对响应式应用的 JVM 参数
JAVA_OPTS="-Xms2g -Xmx4g \
-XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1024m \
-XX:+UseG1GC -XX:MaxGCPauseMillis=100 \
-XX:+UseStringDeduplication \
-XX:G1HeapRegionSize=16m \
-XX:G1ReservePercent=20 \
-XX:G1HeapWastePercent=5 \
-XX:G1MixedGCCountTarget=8 \
-XX:G1MixedGCLiveThresholdPercent=85 \
-XX:G1OldCSetRegionThresholdPercent=20 \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8 \
-Dreactor.schedulers.defaultPoolSize=50"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
升级指南
从 6.x 升级到 7.x:
- Java 版本升级: 确保升级到 Java 17+
- Spring Boot 升级: 从 Spring Boot 2.x 升级到 3.x
- 配置更新: 更新配置属性名称(从驼峰命名到 kebab-case)
- 依赖检查: 检查自定义代码中的废弃 API
- 测试验证: 全面测试认证流程和自定义功能
Docker 部署示例
Dockerfile
dockerfile
FROM eclipse-temurin:17-jre-alpine
LABEL maintainer="Your Name <your.email@example.com>"
# 安装必要的工具
RUN apk add --no-cache \
curl \
bash
WORKDIR /etc/cas/config
COPY . /etc/cas/
EXPOSE 8443
ENTRYPOINT ["sh", "-c", "java -Djava.security.egd=file:/dev/./urandom -jar /etc/cas/webapp/cas.war"]
HEALTHCHECK --interval=30s --timeout=5s --start-period=120s --retries=3 \
CMD curl -f https://localhost:8443/cas/actuator/health || exit 11
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
Docker Compose 配置
yaml
version: '3.8'
services:
cas:
build: .
ports:
- "8443:8443"
volumes:
- ./config:/etc/cas/config
environment:
- CAS_SERVER_NAME=https://localhost:8443
- CAS_SERVER_PREFIX=https://localhost:8443/cas
- CAS_TICKET_REGISTRY_REDIS_HOST=redis
- SPRING_PROFILES_ACTIVE=redis,jpa
- JAVA_OPTS=-Xms1g -Xmx2g -XX:+UseG1GC
depends_on:
- redis
- postgres
- ldap
networks:
- cas-network
redis:
image: redis:7-alpine
ports:
- "6379:6379"
volumes:
- redis_data:/data
networks:
- cas-network
postgres:
image: postgres:15-alpine
ports:
- "5432:5432"
environment:
- POSTGRES_DB=cas
- POSTGRES_USER=casuser
- POSTGRES_PASSWORD=caspass
volumes:
- postgres_data:/var/lib/postgresql/data
networks:
- cas-network
ldap:
image: osixia/openldap:1.5.0
ports:
- "389:389"
- "636:636"
environment:
- LDAP_ORGANISATION=Example Inc.
- LDAP_DOMAIN=example.org
- LDAP_ADMIN_PASSWORD=adminpassword
volumes:
- ldap_data:/var/lib/ldap
- ldap_config:/etc/ldap/slapd.d
networks:
- cas-network
networks:
cas-network:
driver: bridge
volumes:
redis_data:
postgres_data:
ldap_data:
ldap_config: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
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
Kubernetes 部署示例
Deployment 配置
yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: cas-server
namespace: auth
labels:
app: cas-server
spec:
replicas: 3
selector:
matchLabels:
app: cas-server
template:
metadata:
labels:
app: cas-server
spec:
containers:
- name: cas
image: cas-overlay:7.x
ports:
- containerPort: 8443
env:
- name: CAS_SERVER_NAME
value: "https://cas.example.org:8443"
- name: CAS_SERVER_PREFIX
value: "https://cas.example.org:8443/cas"
- name: CAS_TICKET_REGISTRY_REDIS_HOST
value: "redis-service.auth.svc.cluster.local"
- name: SPRING_PROFILES_ACTIVE
value: "redis,jpa,kubernetes"
- name: JAVA_OPTS
value: "-Xms1g -Xmx2g -XX:+UseG1GC"
resources:
requests:
memory: "2Gi"
cpu: "1000m"
limits:
memory: "4Gi"
cpu: "2000m"
livenessProbe:
httpGet:
path: /cas/actuator/health/liveness
port: 8443
scheme: HTTPS
initialDelaySeconds: 180
periodSeconds: 30
timeoutSeconds: 10
readinessProbe:
httpGet:
path: /cas/actuator/health/readiness
port: 8443
scheme: HTTPS
initialDelaySeconds: 60
periodSeconds: 10
timeoutSeconds: 5
volumeMounts:
- name: cas-config
mountPath: /etc/cas/config
volumes:
- name: cas-config
configMap:
name: cas-config
---
apiVersion: v1
kind: Service
metadata:
name: cas-service
namespace: auth
spec:
selector:
app: cas-server
ports:
- protocol: TCP
port: 443
targetPort: 8443
type: LoadBalancer
---
apiVersion: networking.k8s.io/v1
kind: Ingress
metadata:
name: cas-ingress
namespace: auth
annotations:
nginx.ingress.kubernetes.io/ssl-redirect: "true"
nginx.ingress.kubernetes.io/backend-protocol: "HTTPS"
spec:
tls:
- hosts:
- cas.example.org
secretName: cas-tls
rules:
- host: cas.example.org
http:
paths:
- path: /
pathType: Prefix
backend:
service:
name: cas-service
port:
number: 4431
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
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
监控和可观测性
指标配置
properties
# Micrometer 指标配置
management.metrics.export.prometheus.enabled=true
management.metrics.export.prometheus.step=30s
management.metrics.tags.application=cas-server
management.metrics.distribution.percentiles-histogram.http.server.requests=true
management.metrics.distribution.sla.http.server.requests=100ms,500ms,1000ms
# 分布式追踪配置
management.tracing.enabled=true
management.zipkin.tracing.endpoint=http://zipkin:9411/api/v2/spans
management.tracing.sampling.probability=0.11
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
总结
CAS Overlay Template 7.x 是一个面向未来的现代化认证平台,完全拥抱响应式编程模型和云原生架构。它提供了企业级的安全性、可扩展性和可观测性,是现代企业认证架构的理想选择。7.x 版本的显著改进包括对 Java 17+ 的强制支持、全面的响应式架构、现代化的 UI/UX 以及对最新认证协议的全面支持。