# 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