前言
在企业级应用开发中,随着系统数量的不断增加,用户需要频繁登录不同系统,这不仅降低了工作效率,也增加了管理成本。单点登录(Single Sign-On,SSO)技术应运而生,而中央认证服务(CAS,Central Authentication Service)作为一种成熟的开源单点登录解决方案,在 Java 生态中得到广泛应用。本文将深入探讨 Spring Boot 基于 CAS 实现单点登录的原理、实现方式、优缺点以及优化策略,并通过具体代码示例帮助读者快速掌握这一技术。
一、CAS 实现单点登录原理
1.1 CAS 核心组件
CAS 系统主要由两部分组成:CAS Server和CAS Client。
- CAS Server:作为认证中心,负责处理用户的登录请求,验证用户身份,并发放票据。它独立于应用系统,可进行集群部署,以保证高可用性。
- CAS Client:集成在各个应用系统中,负责拦截用户的请求,判断用户是否已经通过认证。如果用户未认证,则将用户重定向到 CAS Server 进行登录;如果已认证,则允许用户访问资源。
1.2 认证流程
- 用户请求资源:用户访问集成了 CAS Client 的应用系统(如app1.example.com)的受保护资源。
- 未认证重定向:CAS Client 检测到用户未认证,生成一个 Service Ticket Request,并将用户重定向到 CAS Server 的登录页面(如cas.example.com/login)。
- 用户登录:用户在 CAS Server 登录页面输入用户名和密码,CAS Server 验证用户凭据。如果验证通过,CAS Server 会生成一个 Ticket Granting Ticket(TGT),并将其存储在服务器端的会话中,同时返回一个 Ticket Granting Cookie(TGC)到用户的浏览器。
- 获取 Service Ticket:用户再次访问app1.example.com时,CAS Client 携带 TGC 向 CAS Server 请求 Service Ticket(ST)。CAS Server 验证 TGC 的有效性后,生成 ST 并返回给 CAS Client。
- 验证 Service Ticket:CAS Client 将 ST 发送给 CAS Server 进行验证。如果验证成功,CAS Server 返回验证结果,表明用户已通过认证,CAS Client 则允许用户访问受保护资源。
1.3 票据机制
- Ticket Granting Ticket(TGT):相当于用户在 CAS Server 的长期会话凭证,用于获取 ST。TGT 包含用户的身份信息和过期时间等。
- Service Ticket(ST):用于访问特定应用系统的临时凭证,具有一次性和时效性,每次访问资源时都需要验证。
二、Spring Boot 基于 CAS 的实现方式
2.1 项目搭建与依赖添加
创建一个 Spring Boot 项目,在pom.xml
文件中添加以下依赖:
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.6.2</version>
</dependency>
</dependencies>
2.2 配置 CAS Server 和 Client 信息
在application.yml
文件中配置 CAS Server 的地址和应用系统的相关信息:
cas:
server-url-prefix: https://cas.example.com # CAS Server地址
client-host-url: https://app1.example.com # 应用系统地址
login-path: /login # CAS Server登录路径
logout-path: /logout # CAS Server登出路径
2.3 配置安全过滤器
创建SecurityConfig
类,配置 CAS 认证相关的过滤器:
import org.jasig.cas.client.authentication.AuthenticationFilter;
import org.jasig.cas.client.session.SingleSignOutFilter;
import org.jasig.cas.client.session.SingleSignOutHttpSessionListener;
import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.boot.web.servlet.ServletListenerRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.web.filter.DelegatingFilterProxy;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Value("${cas.server-url-prefix}")
private String casServerUrlPrefix;
@Value("${cas.client-host-url}")
private String casClientHostUrl;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.csrf().disable()
.authorizeRequests()
.antMatchers("/", "/public/**").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutUrl(casServerUrlPrefix + "/logout")
.logoutSuccessUrl(casClientHostUrl);
}
@Bean
public FilterRegistrationBean<DelegatingFilterProxy> casAuthenticationFilter() {
FilterRegistrationBean<DelegatingFilterProxy> registration = new FilterRegistrationBean<>();
DelegatingFilterProxy filter = new DelegatingFilterProxy("authenticationFilter");
filter.setTargetFilterLifecycle(true);
registration.setFilter(filter);
registration.addUrlPatterns("/*");
return registration;
}
@Bean
public AuthenticationFilter authenticationFilter() {
AuthenticationFilter authenticationFilter = new AuthenticationFilter();
authenticationFilter.setCasServerLoginUrl(casServerUrlPrefix + "/login");
authenticationFilter.setServerName(casClientHostUrl);
return authenticationFilter;
}
@Bean
public FilterRegistrationBean<SingleSignOutFilter> singleSignOutFilter() {
FilterRegistrationBean<SingleSignOutFilter> registration = new FilterRegistrationBean<>();
registration.setFilter(new SingleSignOutFilter());
registration.addUrlPatterns("/*");
return registration;
}
@Bean
public ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> singleSignOutHttpSessionListener() {
ServletListenerRegistrationBean<SingleSignOutHttpSessionListener> registration = new ServletListenerRegistrationBean<>();
registration.setListener(new SingleSignOutHttpSessionListener());
return registration;
}
}
上述配置中,AuthenticationFilter
用于拦截未认证的请求并将用户重定向到 CAS Server 进行登录;SingleSignOutFilter
和SingleSignOutHttpSessionListener
用于实现单点登出功能,当用户在 CAS Server 登出时,自动注销所有应用系统的会话。
2.4 创建受保护资源示例
创建一个简单的控制器,模拟受保护资源:
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class ProtectedResourceController {
@GetMapping("/protected")
public String protectedResource() {
return "This is a protected resource accessed via CAS SSO.";
}
}
三、优缺点分析
3.1 优点
- 成熟稳定:CAS 作为开源项目,经过多年发展,拥有庞大的用户群体和成熟的社区支持,稳定性和可靠性高。
- 跨语言支持:不仅适用于 Java 项目,还支持多种编程语言和框架,方便企业整合异构系统。
- 安全性高:采用票据机制,避免了用户密码在多个系统间传递,降低了密码泄露风险。同时支持 SSL/TLS 加密传输,进一步保障数据安全。
- 易于扩展:提供丰富的插件和扩展点,可根据需求添加多因素认证、审计日志等功能。
3.2 缺点
- 部署复杂:需要独立部署 CAS Server,涉及服务器配置、端口映射、证书管理等操作,对运维人员要求较高。
- 性能开销:每次请求都需要与 CAS Server 进行票据验证,在高并发场景下可能会成为性能瓶颈。
- 用户体验问题:用户在首次登录时需要跳转到 CAS Server 登录页面,可能会影响用户体验。
四、需要注意的问题和难点
4.1 配置问题
- CAS Server 与 Client 的地址配置:确保cas.server-url-prefix和cas.client-host-url配置正确,否则会导致认证失败。
- 过滤器顺序:过滤器的注册顺序很重要,需要保证AuthenticationFilter在其他过滤器之前执行,以确保请求先经过认证处理。
4.2 性能问题
- 票据验证效率:在高并发情况下,频繁的票据验证会对 CAS Server 造成较大压力。可以通过缓存票据、优化数据库查询等方式提高验证效率。
- 网络延迟:CAS Client 与 CAS Server 之间的网络通信可能会带来延迟,影响用户体验。可以通过优化网络架构、使用负载均衡等方式减少延迟。
4.3 安全问题
- 票据泄露:如果 Service Ticket 或 Ticket Granting Ticket 泄露,攻击者可能会冒充用户访问资源。需要采取措施保护票据安全,如设置合理的票据过期时间、使用加密传输等。
- 跨站请求伪造(CSRF):虽然 CAS 在一定程度上可以防止 CSRF 攻击,但仍需在应用系统中采取额外的防护措施,如添加 CSRF 令牌。
五、优化策略
5.1 集群部署
将 CAS Server 进行集群部署,通过负载均衡器(如 Nginx、Apache)将请求分发到不同的 CAS Server 节点,提高系统的可用性和性能。同时,集群部署还可以实现会话共享,确保用户在不同节点间切换时无需重新登录。
通过 Nginx 负载均衡实现 CAS Server 集群,提升系统可用性。配置 Nginx 反向代理:
upstream cas_servers {
server cas1.example.com:8443;
server cas2.example.com:8443;
}
server {
listen 443 ssl;
server_name cas.example.com;
ssl_certificate /path/to/cas.crt;
ssl_certificate_key /path/to/cas.key;
location / {
proxy_pass https://cas_servers;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
5.2 票据缓存
引入缓存机制(如 Redis、Ehcache)对 Service Ticket 和 Ticket Granting Ticket 进行缓存,减少对数据库或文件系统的访问,提高票据验证的效率。在 Spring Boot 中,可以使用@Cacheable注解方便地实现缓存功能。
引入 Redis 缓存 Service Ticket,减少数据库或文件系统的 I/O 操作。使用 Spring Cache 集成 Redis:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Configuration;
@Configuration
@EnableCaching
public class CacheConfig {
// 配置Redis缓存管理器等相关bean
}
在票据验证逻辑中添加缓存注解:
import org.springframework.cache.annotation.Cacheable;
@Cacheable("serviceTickets")
public boolean validateServiceTicket(String serviceTicket) {
// 实际验证逻辑
}
5.3 优化用户体验
- 自定义登录页面:可以根据企业风格自定义 CAS Server 的登录页面,提升用户体验。
- 静默登录:利用 TGC 实现静默登录,当用户已经在 CAS Server 认证过,再次访问其他集成 CAS Client 的应用系统时,无需再次跳转到登录页面,直接完成认证。
结语
通过本文的介绍,我们详细了解了 Spring Boot 基于 CAS 实现单点登录的原理、实现方式、优缺点以及优化策略,并通过具体代码示例进行了实践。CAS 作为一种成熟的单点登录解决方案,能够有效解决企业多系统间的统一认证问题。在实际应用中,需要根据项目需求和特点,合理配置和优化 CAS 系统,以实现安全、高效、用户友好的单点登录功能。