CAS_INTEGRATION_GUIDE.md 15 KB

CAS 单点登录集成指南

📋 目录

  1. 概述
  2. 架构设计
  3. 配置说明
  4. 集成流程
  5. 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 登录流程

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

本地登录流程

sequenceDiagram
    participant User as 用户
    participant App as 本应用

    User->>App: POST /system/auth/login
    App->>App: 验证用户名密码
    App->>App: 创建 Token
    App->>User: 返回 accessToken

配置说明

application-local.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 排除的路径:

/admin-api/system/auth/**   // 管理后台登录接口(完整路径)
/system/auth/**              // 原有登录接口
/admin-api/system/cas/**     // CAS 相关接口(完整路径)
/system/cas/**               // CAS 相关接口
/file/**                     // 静态资源
/actuator/**                 // 监控端点
/swagger-ui/**               // Swagger UI

集成流程

1. 添加依赖(已完成)

<!-- Apereo CAS Client -->
<dependency>
    <groupId>org.apereo.cas.client</groupId>
    <artifactId>cas-client-core</artifactId>
    <version>4.0.0</version>
</dependency>

2. 创建配置类(已完成)

CasProperties.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

@Configuration
public class CasConfiguration {

    @Bean
    public FilterRegistrationBean<CasPathFilter> casPathFilterRegistration() {
        // Order = 0,优先级最高
        // 负责排除不需要 CAS 认证的路径
    }

    @Bean
    public FilterRegistrationBean<SingleSignOutFilter> singleSignOutFilter() {
        // Order = 10
        // 处理 CAS 单点登出
    }

    // CAS AuthenticationFilter 和 ValidationFilter 已禁用
    // 因为 Apereo CAS 客户端的 ignoreUrlList 配置不生效
}

3. 创建过滤器(已完成)

CasPathFilter.java

public class CasPathFilter implements Filter {
    private static final List<String> 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

@RestController
@RequestMapping("/system/auth/cas")
public class CasController {

    /**
     * CAS 登录回调接口
     * GET /system/auth/cas/login
     */
    @GetMapping("/login")
    public CommonResult<AuthLoginRespVO> 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<CasUserInfoRespVO> 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<String> 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)

响应示例:

{
  "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

说明: 前端用于检测用户登录状态

请求参数:

响应示例:

{
  "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

请求参数:

响应示例:

{
  "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 影响)

请求示例:

{
  "username": "admin",
  "password": "admin123"
}

响应示例:

{
  "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
// 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 模式手动构建响应对象

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 影响)

curl -X POST http://127.0.0.1:48080/admin-api/system/auth/login \
  -H "Content-Type: application/json" \
  -d '{"username":"admin","password":"admin123"}'

预期结果:

{
  "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: 访问受保护的资源(不在排除列表中)

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

curl http://127.0.0.1:48080/admin-api/system/auth/cas/login

预期结果: 返回本地 accessToken

3. 测试获取用户信息

curl http://127.0.0.1:48080/admin-api/system/auth/cas/mineInfo

预期结果:

{
  "code": 0,
  "data": {
    "username": "admin",
    "displayName": "管理员",
    "email": "admin@example.com",
    "mobile": "13800138000"
  }
}

4. 测试登出

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. 日志级别配置

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