Skip to content

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.x

或者指定特定版本:

bash
git clone -b 7.0.0 https://github.com/apereo/cas-overlay-template.git cas-overlay-7.0.0

环境要求

  • 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.x

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 run

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=30s

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 -d

二次开发

自定义认证处理器

创建自定义认证处理器示例:

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; // 简化示例,实际应该检查凭证类型
    }
}

自定义属性解析器

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);
    }
}

自定义事件监听器

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());
    }
}

自定义控制器

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;
    }
}

自定义视图

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>

配置示例

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,employeeType

数据库认证配置

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=true

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_token

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=BASE64

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;
    }
}

自定义服务注册示例

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. 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=5000

2. 认证失败问题

检查日志文件以确定认证失败的原因:

bash
tail -f /path/to/catalina.out

3. 内存溢出问题

增加 JVM 内存设置:

bash
export JAVA_OPTS="-Xms2g -Xmx4g -XX:MetaspaceSize=512m -XX:MaxMetaspaceSize=1024m"

4. 服务注册问题

确保服务注册文件位于正确位置:

properties
cas.serviceRegistry.initFromJson=true
cas.serviceRegistry.location=classpath:/services
cas.serviceRegistry.schedule.repeatInterval=120
cas.serviceRegistry.managementType=DEFAULT

服务注册文件示例 (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
  }
}

性能优化

连接池配置

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=6000

缓存配置

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=7200

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"

升级指南

从 6.x 升级到 7.x:

  1. 备份现有配置
  2. 更新 pom.xml 中的 CAS 版本到 7.x
  3. 检查废弃的配置属性,更新为新的属性名
  4. 更新服务注册文件格式(如有必要)
  5. 测试认证流程
  6. 部署到生产环境

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 1

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:

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: LoadBalancer

总结

CAS Overlay Template 7.x 是一个功能强大且高度可定制的单点登录解决方案。它提供了现代化的架构、丰富的认证方式和灵活的配置选项,支持云原生部署。7.x 版本引入了对 Java 17+ 的全面支持、GraalVM 原生镜像支持、完全响应式架构以及更好的微服务集成能力,使其成为现代企业认证架构的理想选择。作为最新版本,它代表了 CAS 项目在现代化、性能和安全性方面的最高成就。

适用于 CAS 7.x 系列,基于 Spring Boot 3.x 构建。该版本强制要求 Java 17+,全面拥抱响应式架构和现代化云原生技术栈。


快速开始

要使用 CAS Overlay Template,请按照以下步骤:

  1. 选择合适的版本:根据您的需求选择对应的 CAS 版本
  2. 克隆模板:使用 Git 克隆相应的 Overlay 模板
  3. 自定义配置:修改配置文件以满足您的需求
  4. 添加自定义功能:根据需要添加自定义认证处理器或组件
  5. 构建和部署:构建项目并部署到您的环境中

最佳实践

  • 保持 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/oidc

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 -d

二次开发

自定义认证处理器

创建自定义响应式认证处理器示例:

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; // 简化示例,实际应该检查凭证类型
    }
}

自定义属性解析器(响应式)

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(); // 为了向后兼容
    }
}

自定义事件监听器(响应式)

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();
    }
}

自定义控制器(响应式)

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;
        });
    }
}

自定义视图

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">
            &copy; 2023 Secure Access Platform | Powered by <a href="https://apereo.github.io/cas" target="_blank">Apereo CAS 7.x</a>
        </small>
    </footer>
</body>
</html>

配置示例

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=true

数据库认证配置(响应式)

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=true

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=60

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=caspass

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;
        });
    }
}

自定义响应式服务注册示例

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. Java 版本兼容性问题

CAS 7.x 要求 Java 17+,确保使用正确的 Java 版本:

bash
java -version
# 应该显示 Java 17 或更高版本

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=5000

3. 认证失败问题

启用详细日志来调试:

properties
logging.level.org.apereo.cas.authentication=DEBUG
logging.level.org.apereo.cas.web.flow=DEBUG
logging.level.org.springframework.security=DEBUG

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"

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=caspass

性能优化

连接池配置

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=6000

缓存配置

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=false

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"

升级指南

从 6.x 升级到 7.x:

  1. Java 版本升级: 确保升级到 Java 17+
  2. Spring Boot 升级: 从 Spring Boot 2.x 升级到 3.x
  3. 配置更新: 更新配置属性名称(从驼峰命名到 kebab-case)
  4. 依赖检查: 检查自定义代码中的废弃 API
  5. 测试验证: 全面测试认证流程和自定义功能

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 1

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:

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: 443

监控和可观测性

指标配置

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.1

总结

CAS Overlay Template 7.x 是一个面向未来的现代化认证平台,完全拥抱响应式编程模型和云原生架构。它提供了企业级的安全性、可扩展性和可观测性,是现代企业认证架构的理想选择。7.x 版本的显著改进包括对 Java 17+ 的强制支持、全面的响应式架构、现代化的 UI/UX 以及对最新认证协议的全面支持。