CAS单点登录案例

wen java案例 3

CAS单点登录完整案例

系统架构概览

┌─────────┐     ┌─────────┐     ┌─────────┐
│ 系统A    │     │ 系统B    │     │ 系统C    │
│(应用1)   │     │(应用2)   │     │(应用3)   │
└────┬────┘     └────┬────┘     └────┬────┘
     │               │               │
     └───────────────┼───────────────┘
                     │
              ┌──────▼──────┐
              │   CAS服务器  │
              │ 认证中心     │
              └─────────────┘

环境搭建

1 CAS服务器部署

CAS版本选择: CAS 5.3.x (Java 8+)

CAS单点登录案例

<!-- 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单点登录案例涵盖了从环境搭建、配置、开发到部署的全流程,适用于中小型企业的多系统统一认证需求。

抱歉,评论功能暂时关闭!