Skip to content

CAS Overlay Template 5.x 版本指南

官方介绍

CAS Overlay Template 5.x 是 Apereo CAS 项目的官方推荐部署方式,基于 Spring Boot 1.5.x 构建。5.x 版本是 CAS 项目的重要版本系列,引入了许多现代化特性和改进,包括对 Java 8+ 的全面支持、更好的云原生支持以及改进的模块化架构。这个版本标志着 CAS 向现代化微服务架构的演进。

CAS (Central Authentication Service) 5.x 是一个企业级的单点登录系统,支持多种认证协议,包括 SAML、OAuth2、OpenID Connect、WS-Federation、SCIM 等。


版本特性

核心特性

  • Spring Boot 1.5+ 集成: 完全基于 Spring Boot 1.5.x,提供现代化部署和配置
  • Java 8+ 支持: 全面支持 Java 8 及更高版本
  • 模块化架构: 更好的插件化扩展机制
  • 协议支持: 支持 CAS、SAML、OAuth2、OpenID Connect、WS-Federation、SCIM
  • 多认证源: 支持 LDAP、数据库、REST、JWT、Radius、OAuth、SAML 等多种认证方式
  • 安全性: 内置高级安全机制,支持 MFA(多因素认证)
  • 配置管理: 基于 Spring Cloud Config 的集中配置管理
  • 云原生支持: 原生支持 Kubernetes、Docker 等云原生环境

5.x 版本演进

  • 5.0.x: 初始版本,从传统架构向 Spring Boot 迁移
  • 5.1.x: 增强了配置管理,改进了安全性
  • 5.2.x: 支持更多认证协议,改进了性能和监控
  • 5.3.x: 增强了 Docker 支持,改进了开发体验
  • 5.4.x: 最终版本,增加了对 JWT、OAuth2 的更好支持
  • 5.5.x: 预计未来版本,将进一步增强云原生支持

官方获取地址

bash
# 通过 Git Clone 获取 CAS Overlay Template 5.x
git clone -b 5.3.x https://github.com/apereo/cas-overlay-template.git cas-overlay-5.x

或者指定特定版本:

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

环境要求

  • Java: JDK 11 或更高版本(推荐 JDK 11/17)
  • Maven: 3.6.3 或更高版本
  • Git: 用于版本控制
  • 操作系统: 跨平台支持(Linux、Windows、macOS)

部署方法

1. 克隆项目

bash
git clone -b 5.3.x https://github.com/apereo/cas-overlay-template.git cas-overlay-5.x
cd cas-overlay-5.x

2. 构建项目

bash
# 清理并构建项目
./mvnw clean compile

# 构建 WAR 包
./mvnw clean package

# 直接运行(开发模式)
./mvnw clean spring-boot:run

# 或者使用包装器脚本
bash build.sh package
bash build.sh run

3. 配置文件说明

CAS 6.x 使用以下配置文件:

application.properties

properties
# 服务器配置
server.port=8443
server.servlet.context-path=/cas

# 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:log4j2.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

4. 使用 Docker 部署

bash
# 构建 Docker 镜像
docker build -t cas-overlay:5.x .

# 运行容器
docker run -p 8443:8443 -v /path/to/config:/etc/cas/config cas-overlay:5.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.authentication.adaptive.AdaptiveAuthenticationPrincipalResolver;
import org.apereo.cas.event.CasEventRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.autoconfigure.RefreshAutoConfiguration;
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", "5.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", "5.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", "5.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="-Xms1g -Xmx2g -XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m"

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

# EhCache 配置
cas.cache.core.tickets.ehcache.maxElementsOnDisk=1000
cas.cache.core.tickets.ehcache.maxElementsInMemory=100
cas.cache.core.tickets.ehcache.eternal=false
cas.cache.core.tickets.ehcache.timeToLiveSeconds=7200
cas.cache.core.tickets.ehcache.timeToIdleSeconds=1800

JVM 优化

bash
# 推荐的 JVM 参数
JAVA_OPTS="-Xms1g -Xmx2g \
-XX:MetaspaceSize=256m -XX:MaxMetaspaceSize=512m \
-XX:+UseG1GC -XX:MaxGCPauseMillis=200 \
-XX:+UseStringDeduplication \
-XX:+OptimizeStringConcat \
-Djava.awt.headless=true \
-Dfile.encoding=UTF-8"

升级指南

从 6.2.x 升级到 6.3.x:

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

Docker 部署示例

Dockerfile

dockerfile
FROM eclipse-temurin:11-jre-focal

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:6-alpine
    ports:
      - "6379:6379"
    volumes:
      - redis_data:/data

  ldap:
    image: osixia/openldap:1.4.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:5.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: "1Gi"
            cpu: "500m"
          limits:
            memory: "2Gi"
            cpu: "1000m"
        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 5.x 是一个功能强大且高度可定制的单点登录解决方案。它提供了现代化的架构、丰富的认证方式和灵活的配置选项,支持云原生部署。5.x 版本引入了对 Spring Boot 1.5+ 的全面支持、改进的安全机制以及更好的微服务集成能力,使其成为现代企业认证架构的理想选择。