CAS单点登录完整案例
系统架构概览
┌─────────┐ ┌─────────┐ ┌─────────┐
│ 系统A │ │ 系统B │ │ 系统C │
│(应用1) │ │(应用2) │ │(应用3) │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└───────────────┼───────────────┘
│
┌──────▼──────┐
│ CAS服务器 │
│ 认证中心 │
└─────────────┘
环境搭建
1 CAS服务器部署
CAS版本选择: CAS 5.3.x (Java 8+)

<!-- Maven依赖配置 (使用CAS Overlay方式) -->
<dependencies>
<dependency>
<groupId>org.apereo.cas</groupId>
<artifactId>cas-server-webapp-tomcat</artifactId>
<version>5.3.14</version>
<type>war</type>
</dependency>
</dependencies>
CAS服务器配置 (application.properties):
# 服务器配置 server.port=8443 server.ssl.key-store=classpath:keystore.jks server.ssl.key-store-password=changeit server.ssl.key-password=changeit # CAS核心配置 cas.server.name=https://cas.example.com:8443 cas.server.prefix=https://cas.example.com:8443/cas # 认证方式 cas.authn.accept.users=casuser::Mellon # 服务注册 cas.service-registry.init-from-json=true cas.service-registry.json.location=classpath:/services # Ticket过期时间 cas.ticket.tgt.timeout.maxTimeout=7200 cas.ticket.st.timeout=300
服务注册配置 (services/HTTPSandIMAPS-10000001.json):
{
"@class": "org.apereo.cas.services.RegexRegisteredService",
"serviceId": "^(https|http)://(localhost|127\\.0\\.0\\.1):.*",
"name": "Local Development Services",
"id": 10000001,
"description": "本地开发环境服务",
"evaluationOrder": 1,
"accessStrategy": {
"@class": "org.apereo.cas.services.DefaultRegisteredServiceAccessStrategy",
"enabled": true,
"ssoEnabled": true
},
"authenticationPolicy": {
"@class": "org.apereo.cas.services.DefaultRegisteredServiceAuthenticationPolicy",
"requiredAuthenticationHandlers": ["accept"]
}
}
2 数据库认证配置
# 数据库认证配置 cas.authn.jdbc.query[0].url=jdbc:mysql://localhost:3306/cas_db cas.authn.jdbc.query[0].user=cas_user cas.authn.jdbc.query[0].password=cas_password cas.authn.jdbc.query[0].driverClass=com.mysql.jdbc.Driver cas.authn.jdbc.query[0].sql=SELECT * FROM users WHERE username = ? cas.authn.jdbc.query[0].field.principalId=username cas.authn.jdbc.query[0].field.password=password cas.authn.jdbc.query[0].passwordEncoder.type=BCRYPT
客户端集成示例
1 Java Web客户端示例
Maven依赖配置:
<dependency>
<groupId>org.jasig.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>3.6.3</version>
</dependency>
web.xml配置:
<!-- CAS认证过滤器 -->
<filter>
<filter-name>CAS Authentication Filter</filter-name>
<filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
<init-param>
<param-name>casServerLoginUrl</param-name>
<param-value>https://cas.example.com:8443/cas/login</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://app1.example.com:8080</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Authentication Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- CAS票据验证过滤器 -->
<filter>
<filter-name>CAS Validation Filter</filter-name>
<filter-class>org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter</filter-class>
<init-param>
<param-name>casServerUrlPrefix</param-name>
<param-value>https://cas.example.com:8443/cas</param-value>
</init-param>
<init-param>
<param-name>serverName</param-name>
<param-value>http://app1.example.com:8080</param-value>
</init-param>
<init-param>
<param-name>redirectAfterValidation</param-name>
<param-value>true</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>CAS Validation Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- CAS请求包装过滤器 -->
<filter>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<filter-class>org.jasig.cas.client.util.HttpServletRequestWrapperFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS HttpServletRequest Wrapper Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- CAS单点登出过滤器 -->
<filter>
<filter-name>CAS Single Sign Out Filter</filter-name>
<filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>CAS Single Sign Out Filter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<!-- CAS单点登出监听器 -->
<listener>
<listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>
2 Spring Boot客户端示例
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/", "/home").permitAll()
.anyRequest().authenticated()
.and()
.logout()
.logoutSuccessUrl("https://cas.example.com:8443/cas/logout")
.invalidateHttpSession(true)
.deleteCookies("JSESSIONID")
.and()
.addFilterBefore(casAuthenticationFilter(),
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(casSingleSignOutFilter(),
CasAuthenticationFilter.class);
}
@Bean
public CasAuthenticationFilter casAuthenticationFilter() throws Exception {
CasAuthenticationFilter filter = new CasAuthenticationFilter();
filter.setAuthenticationManager(authenticationManager());
filter.setServiceProperties(serviceProperties());
return filter;
}
@Bean
public ServiceProperties serviceProperties() {
ServiceProperties serviceProperties = new ServiceProperties();
serviceProperties.setService("http://app1.example.com:8080/login/cas");
serviceProperties.setSendRenew(false);
return serviceProperties;
}
@Bean
public CasAuthenticationProvider casAuthenticationProvider() {
CasAuthenticationProvider provider = new CasAuthenticationProvider();
provider.setServiceProperties(serviceProperties());
provider.setTicketValidator(casTicketValidator());
provider.setUserDetailsService(createUserDetailsService());
provider.setKey("CAS_PROVIDER_LOCALHOST_9000");
return provider;
}
@Bean
public Cas30ServiceTicketValidator casTicketValidator() {
return new Cas30ServiceTicketValidator("https://cas.example.com:8443/cas");
}
@Bean
public SingleSignOutFilter casSingleSignOutFilter() {
SingleSignOutFilter filter = new SingleSignOutFilter();
filter.setCasServerUrlPrefix("https://cas.example.com:8443/cas");
filter.setIgnoreInitConfiguration(true);
return filter;
}
}
完整登录流程演示
1 首次访问流程
// 假设用户首次访问系统A的受保护资源
// http://app1.example.com:8080/secure/page
// 1. CAS客户端过滤器检测到未认证状态
// 自动重定向到CAS服务器
// https://cas.example.com:8443/cas/login?service=http://app1.example.com:8080/login/cas
// 2. 用户输入用户名密码
POST https://cas.example.com:8443/cas/login
Content-Type: application/x-www-form-urlencoded
username=casuser&password=Mellon&service=http://app1.example.com:8080/login/cas
// 3. CAS服务器验证成功后,重定向回应用系统
HTTP/1.1 302 Found
Location: http://app1.example.com:8080/login/cas?ticket=ST-1-abc123xyz456
// 4. 客户端使用ticket向CAS服务器验证
GET https://cas.example.com:8443/cas/serviceValidate?service=http://app1.example.com:8080/login/cas&ticket=ST-1-abc123xyz456
// 5. CAS服务器返回XML
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
<cas:authenticationSuccess>
<cas:user>casuser</cas:user>
<cas:attributes>
<cas:email>user@example.com</cas:email>
<cas:role>ROLE_ADMIN</cas:role>
</cas:attributes>
</cas:authenticationSuccess>
</cas:serviceResponse>
// 6. 验证成功后建立本地会话
// 创建session,存储用户信息和认证状态
2 单点登录流程
// 用户已登录系统A,现在访问系统B // 1. 系统B检测到未认证 // 重定向到CAS服务器 // https://cas.example.com:8443/cas/login?service=http://app2.example.com:8080/login/cas // 2. CAS服务器检查到已有TGT(Ticket Granting Ticket) // 直接发放ST(Service Ticket)并重定向 HTTP/1.1 302 Found Location: http://app2.example.com:8080/login/cas?ticket=ST-2-xyz789abc123 // 3. 系统B验证service ticket成功 // 无需再次输入用户名密码
3 单点登出流程
// 用户在系统A发起登出 // 系统A登出地址:http://app1.example.com:8080/logout // 1. 系统A登出,同时重定向到CAS登出 // 并携带service参数跳回 // https://cas.example.com:8443/cas/logout?service=http://app1.example.com:8080/login // 2. CAS服务器销毁全局会话TGT // 3. CAS服务器向所有已认证的服务发送注销请求 // POST http://app2.example.com:8080/cas/logout (带有samlRequest) // POST http://app3.example.com:8080/cas/logout (带有samlRequest) // 4. 每个客户端收到请求后销毁本地会话
核心实现代码
1 用户信息获取
@Controller
public class UserController {
@GetMapping("/user/info")
public String getUserInfo(HttpServletRequest request) {
// 方式1:使用AssertionHolder获取用户
Assertion assertion = AssertionHolder.getAssertion();
String username = assertion.getPrincipal().getName();
// 获取CAS传递的属性
Map<String, Object> attributes = assertion.getPrincipal().getAttributes();
String email = (String) attributes.get("email");
String role = (String) attributes.get("role");
// 方式2:从session中获取
Object sessionUser = request.getSession()
.getAttribute(CasConstants.CAS_ASSERTION);
return "userInfo";
}
// 使用注解方式获取用户
@GetMapping("/user/current")
@ResponseBody
public String getCurrentUser(@AuthenticationPrincipal Principal principal) {
return principal.getName();
}
}
2 手动认证处理
@Component
public class CasAuthenticationService {
@Autowired
private ServiceProperties serviceProperties;
@Autowired
private TicketValidator ticketValidator;
// 手动处理票据验证
public User authenticateTicket(String ticket, String serviceUrl) {
try {
// 验证票据
Assertion assertion = ticketValidator.validate(ticket, serviceUrl);
// 获取用户信息
String username = assertion.getPrincipal().getName();
// 获取属性
Map<String, Object> attributes =
assertion.getPrincipal().getAttributes();
// 构建用户对象
User user = new User();
user.setUsername(username);
user.setEmail((String) attributes.get("email"));
user.setRoles((List<String>) attributes.get("roles"));
return user;
} catch (TicketValidationException e) {
log.error("票据验证失败", e);
throw new AuthenticationException("票据无效");
}
}
}
3 登出实现
@Controller
public class LogoutController {
@GetMapping("/logout")
public String logout(HttpServletRequest request,
HttpServletResponse response) {
// 定义登出后跳转地址
String serviceUrl = "http://app1.example.com:8080/login";
// 构造CAS登出URL
String casLogoutUrl = "https://cas.example.com:8443/cas/logout";
String redirectUrl = casLogoutUrl + "?service=" +
URLEncoder.encode(serviceUrl, "UTF-8");
// 清除本地session
request.getSession().invalidate();
// 重定向到CAS登出
return "redirect:" + redirectUrl;
}
// 处理CAS服务器发来的登出请求
@PostMapping("/cas/logout")
@ResponseBody
public String handleCasLogout(@RequestBody String logoutRequest) {
// 解析SAML登出请求
String sessionId = parseSessionIdFromLogoutRequest(logoutRequest);
// 根据sessionId找到对应的session并销毁
HttpSession session = sessionRegistry.getSession(sessionId);
if (session != null) {
session.invalidate();
}
return "success";
}
private String parseSessionIdFromLogoutRequest(String logoutRequest) {
// 实现SAML请求解析
// 从XML中提取SessionIndex
return "session-id-xxx";
}
}
CAS配置管理界面
@Configuration
@EnableWebSecurity
public class CasManagementConfig {
@Bean
public ManagementService managementService() {
// 管理CAS服务注册中心
return new DefaultManagementService();
}
@Bean
public Controller controller() {
// CAS管理界面控制器
return null;
}
}
测试用例
@SpringBootTest
@AutoConfigureMockMvc
public class CasAuthenticationTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testCasLoginFlow() throws Exception {
// 模拟访问受保护资源
MvcResult result = mockMvc.perform(MockMvcRequestBuilders
.get("/secure/resource"))
.andExpect(status().is3xxRedirection())
.andReturn();
// 验证重定向到CAS服务器
String redirectUrl = result.getResponse().getRedirectedUrl();
assertTrue(redirectUrl.startsWith("https://cas.example.com:8443/cas/login"));
}
@Test
public void testCasAuthentication() throws Exception {
// 模拟CAS服务器回调
mockMvc.perform(MockMvcRequestBuilders
.get("/login/cas")
.param("ticket", "ST-1-test-ticket"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrl("/secure/resource"));
}
@Test
public void testLogoutFlow() throws Exception {
// 测试登出
mockMvc.perform(MockMvcRequestBuilders
.get("/logout"))
.andExpect(status().is3xxRedirection())
.andExpect(redirectedUrlPattern("https://cas.example.com:8443/cas/logout*"));
}
}
常见问题与解决方案
1 会话不一致问题
// 解决方案:使用Redis存储session
@Configuration
@EnableRedisHttpSession
public class RedisSessionConfig {
@Bean
public LettuceConnectionFactory connectionFactory() {
return new LettuceConnectionFactory("localhost", 6379);
}
}
2 证书问题
# 生成客户端信任的证书
keytool -import -alias cas-server \
-keystore client-truststore.jks \
-file cas-server-cert.cer \
-storepass changeit -noprompt
# 或禁用证书验证(仅开发环境)
System.setProperty("javax.net.ssl.trustStore", "truststore.jks");
System.setProperty("javax.net.ssl.trustStorePassword", "changeit");
3 跨域配置(CORS)
@Configuration
public class CorsConfig implements WebMvcConfigurer {
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/**")
.allowedOrigins("*")
.allowedMethods("GET", "POST", "PUT", "DELETE")
.allowedHeaders("*")
.allowCredentials(true)
.maxAge(3600);
}
}
监控与日志
# 开启CAS监控
management.endpoints.web.exposure.include=health,info,metrics,loggers
management.endpoint.health.show-details=always
# 日志配置
logging.level.org.jasig.cas=DEBUG
logging.level.org.jasig.cas.client=DEBUG
logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} - %msg%n
监控点:
/actuator/health- 健康检查/actuator/metrics- 性能监控/cas/status- CAS服务器状态/cas/statistics- CAS统计信息
部署架构图
┌─────────────────────┐
│ 负载均衡器 │
│ (Nginx/HAProxy) │
└─────────┬───────────┘
│
┌───────────────┼───────────────┐
│ │ │
┌─────────▼─────┐ ┌──────▼────────┐ ┌─────▼─────────┐
│ CAS服务器1 │ │ CAS服务器2 │ │ CAS服务器3 │
│ (TGT存储) │ │ (TGT存储) │ │ (TGT存储) │
└─────────┬─────┘ └──────┬────────┘ └─────┬─────────┘
│ │ │
└───────────────┼───────────────┘
│
┌─────────▼─────────┐
│ Redis集群 │
│ (Session共享) │
└───────────────────┘
│
┌─────────▼─────────┐
│ 数据库(MySQL) │
│ (用户信息存储) │
└───────────────────┘
这个完整的CAS单点登录案例涵盖了从环境搭建、配置、开发到部署的全流程,适用于中小型企业的多系统统一认证需求。
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
足球直播
足球直播
足球直播
篮球直播
篮球直播
篮球直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
亚冠直播
亚冠直播
亚冠直播
欧冠直播
欧冠直播
欧冠直播
英超直播
英超直播
英超直播
西甲直播
西甲直播
西甲直播
中超直播
中超直播
中超直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
曼联直播
曼联直播
曼联直播
日职联直播
日职联直播
日职联直播
澳超直播
澳超直播
澳超直播
阿森纳直播
阿森纳直播
阿森纳直播
足球直播
足球直播
足球直播
NBA直播
NBA直播
NBA直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
欧冠直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
篮球直播
篮球直播
篮球直播
篮球直播
篮球直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播
中超直播