CAS单点登录原理与实践
CAS(Central Authentication Service)是耶鲁大学开发的SSO协议实现,本文深入剖析CAS协议原理、票据机制、部署配置,以及与企业系统的集成实践。
一、SSO概述与CAS协议
1.1 单点登录概念
用户
应用A
应用B
应用C
SSO认证中心
单点登录(Single Sign-On,SSO)允许用户只需登录一次,即可访问所有相互信任的应用系统。
1.2 CAS协议核心角色
票据
CAS角色
存储
一次性
浏览器
Client 客户端
Browser 浏览器
CAS Server
Service 应用服务
TGT Ticket Granting Ticket
ST Service Ticket
TGC Ticket Granting Cookie
| 角色 | 说明 |
|---|---|
| Client/Browser | 用户使用的浏览器 |
| CAS Server | 认证中心服务器 |
| Service | 接入CAS的应用服务 |
| TGT | 票据授权票据,存储在Server端 |
| ST | 服务票据,一次性使用 |
| TGC | 票据授权Cookie,存储在浏览器 |
1.3 CAS协议版本
| 版本 | 说明 | 特点 |
|---|---|---|
| CAS 1.0 | 基础协议 | 简单认证流程 |
| CAS 2.0 | 代理支持 | 支持代理认证 |
| CAS 3.0 | 多协议 | 支持OAuth/SAML |
| CAS 4.0+ | 企业特性 | 支持分布式、HA |
二、CAS认证核心流程
2.1 标准认证流程
App2 CAS服务器 应用服务 用户浏览器 App2 CAS服务器 应用服务 用户浏览器 alt [首次登录] 后续访问其他应用 访问受保护资源 重定向到CAS登录页 显示登录表单 提交用户名密码 验证用户凭证 创建TGT 设置TGC Cookie 重定向到应用,携带ST 携带ST访问应用 验证ST有效性 验证成功,返回用户信息 返回受保护资源 访问应用2 重定向到CAS(携带TGC) 携带TGC 验证TGC有效 直接返回ST(无需登录) 携带ST访问应用2
2.2 协议详细步骤
无
有
失败
成功
无效
有效
无效
有效
- 用户访问App
有TGC Cookie? - 重定向到CAS登录
- 验证TGC
- 用户登录
凭证验证
显示错误 - 创建TGT
- 设置TGC Cookie
- 生成ST
- 重定向到App
TGC有效? - 生成ST
- App验证ST
ST有效? - 显示错误
- 返回用户信息
- 创建本地Session
2.3 服务票据ST特性
| 特性 | 说明 |
|---|---|
| 一次性 | 每个ST只能使用一次 |
| 时效性 | 通常5-10秒内必须使用 |
| 绑定服务 | ST与请求的服务URL绑定 |
| 单向认证 | 只能验证用户身份,不能反向 |
三、票据机制详解
3.1 三层票据体系
ST内容
TGT内容
CAS服务器层
浏览器层
对应
生成
关系
TGC Cookie
TGT Session
ST 票据
用户身份
认证时间
过期时间
服务列表
目标服务URL
用户名
创建时间
父TGT ID
3.2 票据存储策略
java
// CAS Server配置票据存储
@Configuration
public class TicketRegistryConfig {
@Bean
public TicketRegistry ticketRegistry() {
// 1. 内存存储(开发环境)
return new DefaultTicketRegistry();
// 2. Redis存储(生产环境推荐)
// return new RedisTicketRegistry();
// 3. JPA存储(数据库)
// return new JpaTicketRegistry();
}
}
3.3 票据生命周期
java
@Service
public class TicketLifecycleService {
@Value("${cas.ticket.tgt.max-time-to-live-seconds:28800}")
private int tgtMaxTimeToLive;
@Value("${cas.ticket.tgt.timeout-seconds:7200}")
private int tgtTimeOut;
@Value("${cas.ticket.st.time-to-live-seconds:10}")
private int stTimeToLive;
/**
* 创建TGT
*/
public TicketGrantingTicket createTGT(String username) {
// 1. 创建TGT
TicketGrantingTicketImpl tgt = new TicketGrantingTicketImpl(
TicketGrantingTicket.PREFIX,
new SimplePrincipal(username),
new EncryptedConcurrentHashMap<>(),
tgtMaxTimeToLive,
tgtTimeOut
);
// 2. 存储TGT
ticketRegistry.addTicket(tgt);
return tgt;
}
/**
* 创建ST
*/
public ServiceTicket createST(TicketGrantingTicket tgt, String serviceUrl) {
// 1. 验证TGT有效性
if (!tgt.isExpired()) {
throw new TicketException("TGT expired");
}
// 2. 创建ST
ServiceTicketImpl st = new ServiceTicketImpl(
ServiceTicket.PREFIX,
tgt,
serviceUrl,
stTimeToLive,
serviceManager.isSsoEnabled()
);
// 3. 存储ST
ticketRegistry.addTicket(st);
return st;
}
/**
* 验证ST
*/
public Authentication verifyST(ServiceTicket st, String serviceUrl) {
// 1. 检查ST是否已使用
if (st.isConsumed()) {
throw new TicketException("ST already consumed");
}
// 2. 检查ST是否过期
if (st.isExpired()) {
throw new TicketException("ST expired");
}
// 3. 检查服务URL匹配
if (!st.getService().equals(serviceUrl)) {
throw new TicketException("Service URL mismatch");
}
// 4. 标记ST已使用
st.markConsumed();
// 5. 返回认证信息
return st.getGrantingTicket().getAuthentication();
}
}
四、CAS Server部署配置
4.1 Docker部署
yaml
# docker-compose.yml
version: '3.8'
services:
cas:
image: apereo/cas:6.6.10
container_name: cas-server
ports:
- "8443:8443"
- "8080:8080"
environment:
- CAS_SERVER_NAME=https://cas.example.com
- CAS_SERVER_URL=https://cas.example.com:8443
- SERVER_SSL_KEY_STORE=file:/etc/cas/thekeystore
- SERVER_SSL_KEY_STORE_PASSWORD=changeit
- SERVER_SSL_KEY_ALIAS=cas
- DATABASE_HOST=postgres
- DATABASE_PORT=5432
- DATABASE_NAME=cas
- DATABASE_USER=cas
- DATABASE_PASSWORD=cas_password
- DATABASE_DRIVER_CLASS=org.postgresql.Driver
- DATABASE_DDL_AUTO=update
- DATABASE_POOL_AUTOCOMMIT=true
- DATABASE_POOL_MIN_SIZE=5
- DATABASE_POOL_MAX_SIZE=20
- DATABASE_POOL_TIMEOUT=30000
- SPRING_JDBC_DRIVER_CLASS_NAME=org.postgresql.Driver
volumes:
- ./etc/cas:/etc/cas
- ./keys:/etc/cas/keys
- ./logs:/etc/cas/logs
restart: unless-stopped
networks:
- cas-network
postgres:
image: postgres:15
container_name: cas-postgres
environment:
- POSTGRES_DB=cas
- POSTGRES_USER=cas
- POSTGRES_PASSWORD=cas_password
volumes:
- postgres-data:/var/lib/postgresql/data
restart: unless-stopped
networks:
- cas-network
volumes:
postgres-data:
networks:
cas-network:
driver: bridge
4.2 应用配置
properties
# application.properties
# 服务端点
cas.server.name=https://cas.example.com
cas.server.prefix=${cas.server.name}/cas
# 客户端回调
cas.client.host.url=http://app.example.com
# TGT配置
cas.ticket.tgt.max-time-to-live-seconds=28800
cas.ticket.tgt.timeout-seconds=7200
# ST配置
cas.ticket.st.time-to-live-seconds=10
cas.ticket.st.number-of-uses=1
# 数据库配置
spring.datasource.url=jdbc:postgresql://postgres:5432/cas
spring.datasource.username=cas
spring.datasource.password=cas_password
spring.datasource.driver-class-name=org.postgresql.Driver
# JPA配置
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.show-sql=false
# 日志配置
logging.config=file:/etc/cas/config/log4j2.xml
4.3 SSL证书配置
bash
# 生成 keystore
keytool -genkey -alias cas \
-keyalg RSA \
-keysize 2048 \
-keystore thekeystore \
-storepass changeit \
-keypass changeit \
-dname "CN=cas.example.com, OU=IT, O=Example, L=Beijing, ST=Beijing, C=CN"
# 导出证书
keytool -export \
-alias cas \
-keystore thekeystore \
-file cas.crt \
-storepass changeit
# 导入到信任库
keytool -import \
-alias cas \
-file cas.crt \
-keystore $JAVA_HOME/jre/lib/security/cacerts \
-storepass changeit \
-noprompt
4.4 高可用配置
yaml
# cas-high-availability.yml
---
spring:
profiles: ha
# Redis共享Session
session:
store-type: redis
redis:
namespace: cas:session
timeout: 3600s
# Redis连接
data:
redis:
host: redis-master
port: 6379
password: ${REDIS_PASSWORD:}
timeout: 5000ms
lettuce:
pool:
max-active: 20
max-idle: 10
min-idle: 5
# 多数据源
datasource:
primary:
url: jdbc:postgresql://postgres-primary:5432/cas
hikari:
maximum-pool-size: 20
minimum-idle: 5
replica:
url: jdbc:postgresql://postgres-replica:5432/cas
hikari:
maximum-pool-size: 20
minimum-idle: 5
五、CAS Client集成
5.1 Maven依赖
xml
<!-- CAS Client -->
<dependency>
<groupId>org.apereo.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.6.4</version>
</dependency>
<!-- Spring Security CAS集成 -->
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-cas</artifactId>
</dependency>
5.2 Spring Security CAS配置
java
@Configuration
@EnableWebSecurity
public class CasSecurityConfig {
@Value("${cas.server-url-prefix}")
private String casServerUrlPrefix;
@Value("${cas.server-login-url}")
private String casServerLoginUrl;
@Value("${cas.server-logout-url}")
private String casServerLogoutUrl;
@Value("${app.server-url}")
private String appServerUrl;
@Value("${cas.validation-url}")
private String casValidationUrl;
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties properties = new ServiceProperties();
properties.setService(appServerUrl + "/login/cas");
properties.setSendRenew(false); // 不强制每次都登录
properties.setAuthenticateAllArtifacts(false);
return properties;
}
@Bean
public TicketValidator ticketValidator() {
// CAS 3.x 协议验证器
return new Cas30ServiceTicketValidator(casServerUrlPrefix);
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setKey("CasAuthenticationProvider");
provider.setTicketValidator(ticketValidator());
provider.setServiceProperties(serviceProperties());
provider.setAuthenticationUserDetailsService(
new CustomAuthenticationUserDetailsService());
return provider;
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter() {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
filter.setServiceProperties(serviceProperties());
filter.setFilterProcessesUrl("/login/cas");
return filter;
}
@Bean
public SecurityFilterChain casFilterChain(HttpSecurity http) throws Exception {
http
.addFilterBefore(casAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.authorizeHttpRequests(auth -> auth
.requestMatchers("/public/**", "/login", "/error").permitAll()
.requestMatchers("/admin/**").hasRole("ADMIN")
.anyRequest().authenticated())
.exceptionHandling(ex -> ex
.authenticationEntryPoint(new CasAuthenticationEntryPoint()))
.logout(logout -> logout
.logoutUrl("/logout")
.logoutSuccessHandler(casLogoutSuccessHandler())
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID"));
return http.build();
}
@Bean
public CasAuthenticationEntryPoint casAuthenticationEntryPoint() {
CasAuthenticationEntryPoint entryPoint =
new CasAuthenticationEntryPoint();
entryPoint.setLoginUrl(casServerLoginUrl);
entryPoint.setServiceProperties(serviceProperties());
return entryPoint;
}
@Bean
public LogoutSuccessHandler casLogoutSuccessHandler() {
SimpleUrlLogoutSuccessHandler handler =
new SimpleUrlLogoutSuccessHandler();
handler.setDefaultTargetUrl("/");
handler.setAlwaysUseDefaultTargetUrl(false);
// CAS单点登出
CasLogoutSuccessHandler casHandler = new CasLogoutSuccessHandler();
casHandler.setCasServerUrlPrefix(casServerUrlPrefix);
casHandler.setDefaultTargetUrl("/");
return casHandler;
}
}
5.3 自定义用户详情服务
java
public class CustomAuthenticationUserDetailsService
implements AuthenticationUserDetailsService<CasAssertionAuthenticationToken> {
@Autowired
private UserService userService;
@Override
public UserDetails loadUserDetails(
CasAssertionAuthenticationToken token) throws UsernameNotFoundException {
// 1. 从CAS获取用户信息
String username = token.getName();
Assertion assertion = token.getAssertion();
// 2. 获取CAS返回的附加属性
Map<String, Object> attributes = assertion.getAttributes();
String email = (String) attributes.get("email");
String displayName = (String) attributes.get("displayName");
List<String> roles = (List<String>) attributes.get("roles");
// 3. 查询本地用户或创建新用户
User user = userService.findByUsername(username)
.orElseGet(() -> createUserFromCas(username, email, displayName));
// 4. 更新用户信息
user.setLastLoginTime(new Date());
userService.save(user);
// 5. 返回UserDetails
return User.builder()
.username(user.getUsername())
.password("") // CAS认证不需要密码
.authorities(roles.stream()
.map(role -> new SimpleGrantedAuthority("ROLE_" + role))
.toArray(GrantedAuthority[]::new))
.accountExpired(false)
.accountLocked(!user.isEnabled())
.credentialsExpired(false)
.disabled(!user.isEnabled())
.build();
}
private User createUserFromCas(String username, String email,
String displayName) {
User user = new User();
user.setUsername(username);
user.setEmail(email);
user.setDisplayName(displayName);
user.setEnabled(true);
user.setCreateTime(new Date());
return userService.save(user);
}
}
5.4 票据验证服务
java
@Service
public class CasTicketValidationService {
private final TicketValidator ticketValidator;
private final ServiceProperties serviceProperties;
/**
* 验证CAS票据
*/
public Authentication validateTicket(String ticket, String service) {
try {
// 1. 构建服务URL
Assertion assertion = ticketValidator.validate(ticket, service);
// 2. 提取用户信息
Principal principal = assertion.getPrincipal();
String username = principal.getName();
Map<String, Object> attributes = principal.getAttributes();
// 3. 构建认证信息
return new UsernamePasswordAuthenticationToken(
username, null,
extractAuthorities(attributes)
);
} catch (TicketValidationException e) {
throw new AuthenticationException("CAS ticket validation failed", e);
}
}
/**
* 从CAS属性提取权限
*/
private Collection<GrantedAuthority> extractAuthorities(
Map<String, Object> attributes) {
List<GrantedAuthority> authorities = new ArrayList<>();
// 从roles属性提取
List<String> roles = (List<String>) attributes.get("roles");
if (roles != null) {
roles.forEach(role ->
authorities.add(new SimpleGrantedAuthority("ROLE_" + role)));
}
// 从authorities属性提取
List<String> auths = (List<String>) attributes.get("authorities");
if (auths != null) {
auths.forEach(auth ->
authorities.add(new SimpleGrantedAuthority(auth)));
}
return authorities;
}
}
六、跨域SSO方案
6.1 跨域Cookie方案
java
@Configuration
public class CrossDomainCasConfig {
@Value("${cas.server-domain}")
private String casServerDomain;
@Bean
public CookieGenerator ticketGrantingTicketCookieGenerator() {
CookieGenerator generator = new CookieGenerator();
generator.setCookieName("TGC");
generator.setCookieDomain(casServerDomain); // 跨域Cookie
generator.setCookieMaxAge(-1); // Session级别
generator.setCookiePath("/");
generator.setuseHttpOnly(true);
generator.setsameSiteValue("Strict");
return generator;
}
}
6.2 TicketRelay跨域方案
应用B CAS服务器 应用A 用户代理 应用B CAS服务器 应用A 用户代理 登录 重定向 登录 设置TGC + 重定向 携带ST 验证ST 登录成功 访问 重定向(ticketRelayService参数) 重定向回App2 携带ST 验证ST 登录成功
java
@Configuration
public class TicketRelayConfig {
/**
* 跨域票据转发
*/
@Bean
public TicketRelayService ticketRelayService() {
return new TicketRelayServiceImpl();
}
}
@Service
public class TicketRelayServiceImpl implements TicketRelayService {
@Override
public String getTargetService(HttpServletRequest request) {
String service = request.getParameter("targetService");
if (service != null) {
return URLDecoder.decode(service, StandardCharsets.UTF_8);
}
// 从请求属性获取(过滤器设置)
return (String) request.getAttribute("targetService");
}
@Override
public String buildRedirectUrl(String service, String ticket) {
String delimiter = service.contains("?") ? "&" : "?";
return service + delimiter + "ticket=" + ticket;
}
}
七、CAS安全加固
7.1 协议安全配置
properties
# 协议安全配置
cas.security.cas.tgc.enabled=true
cas.security.cas.tgc.remember-me.enabled=false
cas.security.cas.tgc.crypto.enabled=true
cas.security.cas.tgc.crypto.alg=AES
cas.security.cas.tgc.crypto.key.size=256
# 票据加密
cas.security.ticket.st.crypto.enabled=true
cas.security.ticket.tgt.crypto.enabled=true
cas.security.ticket.core.encryption.key=your-encryption-key
cas.security.ticket.core.signing.key=your-signing-key
# 代理票据安全
cas.proxy.authn.max.number.of.proxies=10
cas.proxy.callback.url.allowed=.*
7.2 登录限流
java
@Configuration
public class LoginRateLimitConfig {
@Value("${cas.authn.ip.rate.limit.enabled:true}")
private boolean rateLimitEnabled;
@Value("${cas.authn.ip.rate.limit.max-attempts:10}")
private int maxAttempts;
@Value("${cas.authn.ip.rate.limit.window-seconds:60}")
private int windowSeconds;
@Bean
public IPAddressAuthenticationHandler ipAuthenticationHandler() {
return new IPAddressAuthenticationHandler(
rateLimitEnabled,
maxAttempts,
windowSeconds
);
}
}
public class IPAddressAuthenticationHandler
implements AuthenticationHandler {
private final RateLimiter rateLimiter;
@Override
public Authentication authenticate(AuthenticationTransaction transaction)
throws AuthenticationException {
String ipAddress = transaction.getProperties()
.get("clientIpAddress");
// 检查IP是否被限制
if (rateLimiter.isLimited(ipAddress)) {
throw new FailedLoginException("Too many authentication attempts");
}
// 执行认证逻辑
Authentication result = doAuthenticate(transaction);
// 认证失败,记录
if (result == null) {
rateLimiter.recordFailure(ipAddress);
} else {
rateLimiter.recordSuccess(ipAddress);
}
return result;
}
}
7.3 审计日志
java
@Configuration
public class AuditConfig {
@Bean
public AuditTrailManager auditTrailManager() {
return new JpaAuditTrailManager();
}
}
@Service
public class CasAuditService {
@Autowired
private AuditTrailManager auditTrailManager;
public void audit(String who, String what, String action,
String resource, boolean success) {
AuditAction auditAction = AuditAction.create(who, what, action);
Map<String, Object> data = new HashMap<>();
data.put("resource", resource);
data.put("success", success);
data.put("timestamp", Instant.now());
data.put("ipAddress", getClientIp());
data.put("userAgent", getUserAgent());
auditAction.setData(data);
auditTrailManager.record(auditAction);
}
}
八、CAS与OAuth2/OIDC集成
8.1 CAS as OAuth2 Provider
properties
# 启用OAuth2
cas.authn.oauth.enabled=true
cas.authn.oauth.refreshToken.timeToKillInSeconds=2592000
cas.authn.oauth.code.timeToKillInSeconds=60
# OAuth2客户端注册
cas.oauth.provider.name=CAS
cas.oauth.provider.display-name=CAS OAuth Server
8.2 CAS OIDC配置
properties
# 启用OIDC
cas.authn.oidc.enabled=true
cas.authn.oidc.discovery.discoveryUri=https://cas.example.com/.well-known/openid-configuration
# 签名密钥
cas.authn.oidc.core.signingJwksUri=file:/etc/cas/oidc-signing.jwks
cas.authn.oidc.core.encryptionJwksUri=file:/etc/cas/oidc-encryption.jwks
# 作用域
cas.authn.oidc.core.defaultScopes=openid,profile,email
cas.authn.oidc.core.availableScopes=openid,profile,email,address,phone
8.3 第三方应用集成
java
@Configuration
public class OidcClientConfig {
@Bean
public OAuth2AuthorizedClientService authorizedClientService(
ClientRegistrationRepository registrations) {
return new InMemoryOAuth2AuthorizedClientService(registrations);
}
@Bean
public OAuth2LoginAuthenticationProvider oidcProvider(
OAuth2UserService<OIDCUserRequest, OIDCUser> userService) {
return new OAuth2LoginAuthenticationProvider(
new DefaultOAuth2AuthorizationClientProvider(),
userService
);
}
}
九、常见问题处理
9.1 票据相关问题
| 问题 | 原因 | 解决方案 |
|---|---|---|
| ST无效 | ST已使用或过期 | 检查ST有效期内使用 |
| TGT过期 | 会话超时 | 重新登录 |
| 票据不匹配 | Service URL不一致 | 检查URL格式 |
| 票据无法验证 | CAS Server时钟不同步 | 同步NTP |
9.2 性能问题
java
// 票据缓存配置
@Configuration
public class TicketCachingConfig {
@Bean
public ConcurrentMapCacheManager ticketCacheManager() {
ConcurrentMapCacheManager manager = new ConcurrentMapCacheManager();
manager.setCacheNames(Arrays.asList(
"ticketValidationCache",
"proxyGrantingTicketCache"
));
return manager;
}
@Cacheable(value = "ticketValidationCache",
key = "#ticket + #service")
public Assertion validateAndCache(String ticket, String service) {
return ticketValidator.validate(ticket, service);
}
}
9.3 集群会话同步
java
@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
@Bean
public ConfigBeanWrapper<RedisIndexedSessionRepository> sessionRepository(
RedisConnectionFactory factory) {
RedisIndexedSessionRepository repository =
new RedisIndexedSessionRepository(factory);
repository.setDefaultSerializer(new JdkSerializationRedisSerializer());
repository.setFlushMode(RedisFlushMode.ON_SAVE);
repository.setSaveMode(SaveMode.ON_SET_ATTRIBUTE);
return new ConfigBeanWrapper<>(repository);
}
}
十、总结
10.1 CAS部署架构
应用层
数据层
CAS集群
负载均衡层
负载均衡器
CAS Server 1
CAS Server 2
CAS Server 3
PostgreSQL
Redis
应用A
应用B
应用C
10.2 关键配置检查表
| 配置项 | 说明 | 状态 |
|---|---|---|
| HTTPS配置 | SSL证书配置 | ☐ |
| TGT过期时间 | 默认2小时 | ☐ |
| ST过期时间 | 默认10秒 | ☐ |
| 票据加密 | AES-256加密 | ☐ |
| 登录限流 | 防暴力破解 | ☐ |
| 审计日志 | 记录认证事件 | ☐ |
| 单点登出 | SLO配置 | ☐ |
| 高可用 | 集群部署 | ☐ |
10.3 与其他方案对比
| 特性 | CAS | OAuth2 | JWT |
|---|---|---|---|
| 协议复杂度 | 中 | 高 | 低 |
| 单点登录 | ✅ 原生支持 | ⚠️ 需扩展 | ❌ |
| 单点登出 | ✅ 原生支持 | ⚠️ 需扩展 | ❌ |
| 第三方登录 | ⚠️ 需配置 | ✅ 原生支持 | ❌ |
| 移动端支持 | ⚠️ | ✅ | ✅ |
| 微服务认证 | ⚠️ | ✅ | ✅ |
| 学习成本 | 中 | 高 | 低 |
CAS作为成熟的企业级SSO解决方案,在单点登录场景下具有明显优势。掌握其协议原理和配置方法,能够帮助企业快速构建统一认证体系。