# CAS 单点登录集成指南 ## 📋 目录 1. [概述](#概述) 2. [架构设计](#架构设计) 3. [配置说明](#配置说明) 4. [集成流程](#集成流程) 5. [API 接口](#api-接口) 6. [问题排查](#问题排查) 7. [测试验证](#测试验证) --- ## 概述 ### 功能说明 本系统集成了 Apereo CAS 单点登录功能,支持: - ✅ CAS SSO 单点登录 - ✅ 本地账号密码登录(与 CAS 共存) - ✅ 自动票据验证 - ✅ 单点登出 ### 核心组件 | 组件 | 说明 | 路径 | |------|------|------| | CasPathFilter | CAS 路径过滤器,排除特定路径 | `cn.start.tz.module.system.framework.cas.filter.CasPathFilter` | | CasAuthenticationFilter | CAS 认证过滤器(已禁用) | `cn.start.tz.module.system.framework.cas.filter.CasAuthenticationFilter` | | CasController | CAS 登录控制器 | `cn.start.tz.module.system.controller.admin.cas.CasController` | | CasProperties | CAS 配置属性 | `cn.start.tz.module.system.framework.cas.config.CasProperties` | | CasConfiguration | CAS 配置类 | `cn.start.tz.module.system.framework.cas.config.CasConfiguration` | --- ## 架构设计 ### 过滤器链 ``` 请求 → CasPathFilter (Order 0) ↓ SingleSignOutFilter (Order 10) ↓ CasAuthenticationFilter (Order 20) ❌ 已禁用 ↓ Cas20ProxyReceivingTicketValidationFilter (Order 30) ❌ 已禁用 ↓ HttpServletRequestWrapperFilter (Order 40) ↓ AssertionThreadLocalFilter (Order 50) ↓ Controller ``` ### 认证流程 #### CAS SSO 登录流程 ```mermaid sequenceDiagram participant User as 用户 participant App as 本应用 participant CAS as CAS服务器 User->>App: 访问受保护资源 App->>CAS: 重定向到 CAS 登录页 User->>CAS: 输入用户名密码 CAS->>App: 回调并携带 ticket App->>CAS: 验证 ticket CAS->>App: 返回用户 Assertion App->>App: 查询本地用户 App->>App: 创建本地 Token App->>User: 返回 accessToken ``` #### 本地登录流程 ```mermaid sequenceDiagram participant User as 用户 participant App as 本应用 User->>App: POST /system/auth/login App->>App: 验证用户名密码 App->>App: 创建 Token App->>User: 返回 accessToken ``` --- ## 配置说明 ### application-local.yaml ```yaml --- #################### CAS 单点登录相关配置 #################### cas: # 是否启用 CAS 单点登录 enabled: true # CAS 服务器地址前缀 server-url-prefix: http://192.168.19.219:6080/dev-api/protocols/cas20 # 忽略的地址(逗号分隔,支持 Ant 风格通配符) ignore-url-list: /admin-api/system/auth/**,/system/auth/**,/admin-api/system/cas/**,/system/cas/** # 认证成功后前端回调地址 front-url: http://localhost:8086 # CAS 客户端地址 client-host-url: http://127.0.0.1:48080 # CAS 服务端地址 client-service-url: http://127.0.0.1:48080/admin-api/system/auth/cas/login ``` ### 配置项说明 | 配置项 | 必填 | 说明 | 示例 | |--------|------|------|------| | `cas.enabled` | 是 | 是否启用 CAS | `true` / `false` | | `cas.server-url-prefix` | 是 | CAS 服务器地址 | `http://cas.example.com/cas` | | `cas.ignore-url-list` | 否 | 忽略的路径列表 | `/system/auth/**` | | `cas.front-url` | 是 | 前端回调地址 | `http://localhost:3000` | | `cas.client-host-url` | 是 | 客户端服务地址 | `http://localhost:8080` | ### 路径排除策略 **CasPathFilter 排除的路径:** ```java /admin-api/system/auth/** // 管理后台登录接口(完整路径) /system/auth/** // 原有登录接口 /admin-api/system/cas/** // CAS 相关接口(完整路径) /system/cas/** // CAS 相关接口 /file/** // 静态资源 /actuator/** // 监控端点 /swagger-ui/** // Swagger UI ``` --- ## 集成流程 ### 1. 添加依赖(已完成) ```xml org.apereo.cas.client cas-client-core 4.0.0 ``` ### 2. 创建配置类(已完成) #### CasProperties.java ```java @Data @Component @ConfigurationProperties(prefix = "cas") public class CasProperties { private String serverUrlPrefix; // CAS 服务器地址 private String ignoreUrlList; // 忽略路径列表 private String frontUrl; // 前端回调地址 private String clientHostUrl; // 客户端地址 private String clientServiceUrl; // 客户端服务地址 private Boolean enabled = true; // 是否启用 } ``` #### CasConfiguration.java ```java @Configuration public class CasConfiguration { @Bean public FilterRegistrationBean casPathFilterRegistration() { // Order = 0,优先级最高 // 负责排除不需要 CAS 认证的路径 } @Bean public FilterRegistrationBean singleSignOutFilter() { // Order = 10 // 处理 CAS 单点登出 } // CAS AuthenticationFilter 和 ValidationFilter 已禁用 // 因为 Apereo CAS 客户端的 ignoreUrlList 配置不生效 } ``` ### 3. 创建过滤器(已完成) #### CasPathFilter.java ```java public class CasPathFilter implements Filter { private static final List EXCLUDE_PATHS = Arrays.asList( "/admin-api/system/auth/**", "/system/auth/**", "/admin-api/system/cas/**", "/system/cas/**", // ... 其他排除路径 ); @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) { String requestUri = httpRequest.getRequestURI(); boolean exclude = EXCLUDE_PATHS.stream() .anyMatch(pattern -> pathMatcher.match(pattern, requestUri)); if (exclude) { log.info("[CAS Path Filter] 跳过CAS认证: {}", requestUri); chain.doFilter(request, response); return; } chain.doFilter(request, response); } } ``` ### 4. 创建控制器(已完成) #### CasController.java ```java @RestController @RequestMapping("/system/auth/cas") public class CasController { /** * CAS 登录回调接口 * GET /system/auth/cas/login */ @GetMapping("/login") public CommonResult casLogin(HttpServletRequest request) { // 1. 从 Session 获取 CAS Assertion Assertion assertion = (Assertion) request.getSession().getAttribute("_const_cas_assertion_"); // 2. 获取用户名 String username = assertion.getPrincipal().getName(); // 3. 查询本地用户 AdminUserDO user = adminUserService.getUserByUsername(username); // 4. 检查用户状态 // 5. 创建本地 Token AuthLoginRespVO loginResp = adminAuthService.createTokenAfterLoginSuccess( user.getId(), username, LoginLogTypeEnum.LOGIN_SOCIAL ); return success(loginResp); } /** * 获取当前用户信息 * GET /system/auth/cas/mineInfo */ @GetMapping("/mineInfo") public CommonResult mineInfo(HttpServletRequest request) { // 1. 检查 CAS Session Assertion assertion = (Assertion) request.getSession().getAttribute("_const_cas_assertion_"); // 2. 获取用户名 String username = assertion.getPrincipal().getName(); // 3. 查询本地用户 AdminUserDO user = adminUserService.getUserByUsername(username); // 4. 构建用户信息 CasUserInfoRespVO userInfo = new CasUserInfoRespVO(); userInfo.setUsername(user.getUsername()); userInfo.setDisplayName(user.getNickname()); userInfo.setEmail(user.getEmail()); userInfo.setMobile(user.getMobile()); return success(userInfo); } /** * CAS 登出接口 * GET /system/auth/cas/logout */ @GetMapping("/logout") public CommonResult logout(HttpServletRequest request) { // 1. 清除本地 Session request.getSession().invalidate(); // 2. 构建 CAS 登出 URL String casLogoutUrl = casProperties.getServerUrlPrefix() + "/logout?url=" + redirectUrl; return success(casLogoutUrl); } } ``` --- ## API 接口 ### 1. CAS 登录回调 **接口:** `GET /admin-api/system/auth/cas/login` **说明:** CAS 服务器认证成功后回调此接口 **请求参数:** 无(从 Session 获取 Assertion) **响应示例:** ```json { "code": 0, "data": { "userId": "1", "accessToken": "084f8f0ef03b471e...", "refreshToken": "b3e9a1f2d7c8...", "expiresTime": 1735228800000 }, "msg": "" } ``` **错误响应:** | 错误码 | 说明 | |--------|------| | 401 | 未获取到 CAS 认证信息 | | 404 | 用户不存在,请联系管理员创建账号 | | 403 | 用户已被禁用 | | 500 | CAS 登录异常 | ### 2. 获取当前用户信息 **接口:** `GET /admin-api/system/auth/cas/mineInfo` **说明:** 前端用于检测用户登录状态 **请求参数:** 无 **响应示例:** ```json { "code": 0, "data": { "username": "admin", "displayName": "管理员", "email": "admin@example.com", "mobile": "13800138000" }, "msg": "" } ``` **错误响应:** | 错误码 | 说明 | |--------|------| | 401 | 未登录,请先登录 | | 403 | 用户已被禁用 | ### 3. CAS 登出 **接口:** `GET /admin-api/system/auth/cas/logout` **说明:** 清除本地 Session 并返回 CAS 登出 URL **请求参数:** 无 **响应示例:** ```json { "code": 0, "data": "http://192.168.19.219:6080/dev-api/protocols/cas20/logout?url=http://localhost:8086", "msg": "" } ``` ### 4. 本地登录(与 CAS 共存) **接口:** `POST /admin-api/system/auth/login` **说明:** 使用用户名+密码登录(不受 CAS 影响) **请求示例:** ```json { "username": "admin", "password": "admin123" } ``` **响应示例:** ```json { "code": 0, "data": { "userId": "1", "accessToken": "084f8f0ef03b471e...", "refreshToken": "b3e9a1f2d7c8...", "expiresTime": 1735228800000 }, "msg": "" } ``` --- ## 问题排查 ### 问题 1:登录接口返回 404 **症状:** 访问 `/admin-api/system/auth/login` 返回 404 **原因:** CAS AuthenticationFilter 拦截了请求 **解决方案:** 1. 检查 `CasPathFilter` 日志:应该看到 `[CAS Path Filter] 路径在排除列表中,跳过CAS认证` 2. 如果没有看到此日志,说明 CasPathFilter 没有正确工作 3. 临时解决方案:禁用 CAS AuthenticationFilter 和 ValidationFilter ```java // CasConfiguration.java registration.setEnabled(false); // 禁用 CAS AuthenticationFilter ``` ### 问题 2:登录接口返回空响应 **症状:** 后置脚本错误:提取变量【accessToken】出错: No data, empty input **原因:** CAS 重定向导致响应为空 **解决方案:** 1. 检查是否被 CAS 重定向:查看响应状态码是否为 302 2. 确认 `ignore-url-list` 配置正确 3. 查看日志确认请求是否到达 Controller ### 问题 3:CAS 认证成功但 token 为 null **症状:** 日志显示 "创建访问令牌完成" 但返回的数据为 null **原因:** MapStruct 转换问题(已修复) **解决方案:** 已使用 Builder 模式手动构建响应对象 ```java AuthLoginRespVO respVO = AuthLoginRespVO.builder() .userId(accessTokenDO.getUserId()) .accessToken(accessTokenDO.getAccessToken()) .refreshToken(accessTokenDO.getRefreshToken()) .expiresTime(accessTokenDO.getExpiresTime()) .build(); ``` ### 问题 4:CAS AuthenticationFilter 的 ignoreUrlList 不生效 **症状:** 配置了 `ignore-url-list` 但请求仍被拦截 **原因:** Apereo CAS 客户端的 `AuthenticationFilter.doFilter()` 是 final 方法,无法覆盖 **解决方案:** 使用 `CasPathFilter` 在 CAS 过滤器链之前进行路径排除(当前方案) 或者使用 CAS 客户端的 `ignorePattern` 参数(需要进一步研究) --- ## 测试验证 ### 测试环境 - CAS 服务器:`http://192.168.19.219:6080/dev-api/protocols/cas20` - 应用地址:`http://127.0.0.1:48080` - 前端地址:`http://localhost:8086` ### 测试步骤 #### 1. 测试本地登录(不受 CAS 影响) ```bash curl -X POST http://127.0.0.1:48080/admin-api/system/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"admin123"}' ``` **预期结果:** ```json { "code": 0, "data": { "userId": "1", "accessToken": "xxx", "refreshToken": "xxx", "expiresTime": 1735228800000 } } ``` **检查日志:** ``` [CAS Path Filter] 路径在排除列表中,跳过CAS认证: /admin-api/system/auth/login [login] 收到登录请求, username=admin [login] 开始处理登录请求 [authenticate] 开始认证用户 [authenticate] 用户认证通过 [login] Token创建完成 ``` #### 2. 测试 CAS 登录流程 **步骤 1:** 访问受保护的资源(不在排除列表中) ```bash curl http://127.0.0.1:48080/admin-api/system/user/get-permission-info ``` **预期结果:** 重定向到 CAS 登录页 **步骤 2:** 在 CAS 登录页完成认证 **步骤 3:** CAS 回调到应用,携带 ticket **步骤 4:** 调用 `/admin-api/system/auth/cas/login` 获取本地 token ```bash curl http://127.0.0.1:48080/admin-api/system/auth/cas/login ``` **预期结果:** 返回本地 accessToken #### 3. 测试获取用户信息 ```bash curl http://127.0.0.1:48080/admin-api/system/auth/cas/mineInfo ``` **预期结果:** ```json { "code": 0, "data": { "username": "admin", "displayName": "管理员", "email": "admin@example.com", "mobile": "13800138000" } } ``` #### 4. 测试登出 ```bash curl http://127.0.0.1:48080/admin-api/system/auth/cas/logout ``` **预期结果:** 返回 CAS 登出 URL --- ## 附录 ### A. 过滤器执行顺序 | Order | 过滤器 | 状态 | 说明 | |-------|--------|------|------| | 0 | CasPathFilter | ✅ 启用 | 路径排除过滤 | | 10 | SingleSignOutFilter | ✅ 启用 | 单点登出 | | 20 | AuthenticationFilter | ❌ 禁用 | CAS 认证 | | 30 | Cas20ProxyReceivingTicketValidationFilter | ❌ 禁用 | Ticket 验证 | | 40 | HttpServletRequestWrapperFilter | ✅ 启用 | 请求包装 | | 50 | AssertionThreadLocalFilter | ✅ 启用 | ThreadLocal | ### B. 日志级别配置 ```yaml logging: level: cn.start.tz.module.system.framework.cas: INFO ``` ### C. 常见错误码 | 错误码 | 说明 | 解决方案 | |--------|------|----------| | 400 | 请求参数错误 | 检查请求格式 | | 401 | 未认证 | 先完成 CAS 登录 | | 403 | 用户被禁用 | 联系管理员 | | 404 | 用户不存在 | 先创建本地账号 | | 500 | 服务器内部错误 | 查看日志排查 | --- ## 更新日志 ### 2025-12-26 - ✅ 创建 CAS 集成文档 - ✅ 禁用 CAS AuthenticationFilter 和 ValidationFilter(因为 ignoreUrlList 不生效) - ✅ 完善 CasPathFilter 路径排除逻辑 - ✅ 完善 CasController.mineInfo 方法,返回完整用户信息 - ✅ 添加详细日志记录 - ✅ 修复 MapStruct 转换问题(使用 Builder 模式) - ✅ 实现本地登录和 CAS SSO 共存 ### 待完成 - ⏳ 研究 Apereo CAS 客户端正确的 ignorePattern 配置方式 - ⏳ 启用 CAS AuthenticationFilter 并正确配置忽略路径 - ⏳ 添加 CAS 属性(attributes)映射到本地用户 - ⏳ 实现 CAS 自动创建本地用户功能 - ⏳ 添加前端 CAS 登录示例代码 --- **文档版本:** v1.0.0 **最后更新:** 2025-12-26 **维护人员:** tz