本系统集成了 Apereo 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
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
--- #################### 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
<!-- Apereo CAS Client -->
<dependency>
<groupId>org.apereo.cas.client</groupId>
<artifactId>cas-client-core</artifactId>
<version>4.0.0</version>
</dependency>
@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; // 是否启用
}
@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 配置不生效
}
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);
}
}
@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);
}
}
接口: 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 登录异常 |
接口: GET /admin-api/system/auth/cas/mineInfo
说明: 前端用于检测用户登录状态
请求参数: 无
响应示例:
{
"code": 0,
"data": {
"username": "admin",
"displayName": "管理员",
"email": "admin@example.com",
"mobile": "13800138000"
},
"msg": ""
}
错误响应:
| 错误码 | 说明 |
|---|---|
| 401 | 未登录,请先登录 |
| 403 | 用户已被禁用 |
接口: 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": ""
}
接口: POST /admin-api/system/auth/login
说明: 使用用户名+密码登录(不受 CAS 影响)
请求示例:
{
"username": "admin",
"password": "admin123"
}
响应示例:
{
"code": 0,
"data": {
"userId": "1",
"accessToken": "084f8f0ef03b471e...",
"refreshToken": "b3e9a1f2d7c8...",
"expiresTime": 1735228800000
},
"msg": ""
}
症状: 访问 /admin-api/system/auth/login 返回 404
原因: CAS AuthenticationFilter 拦截了请求
解决方案:
CasPathFilter 日志:应该看到 [CAS Path Filter] 路径在排除列表中,跳过CAS认证// CasConfiguration.java
registration.setEnabled(false); // 禁用 CAS AuthenticationFilter
症状: 后置脚本错误:提取变量【accessToken】出错: No data, empty input
原因: CAS 重定向导致响应为空
解决方案:
ignore-url-list 配置正确症状: 日志显示 "创建访问令牌完成" 但返回的数据为 null
原因: MapStruct 转换问题(已修复)
解决方案: 已使用 Builder 模式手动构建响应对象
AuthLoginRespVO respVO = AuthLoginRespVO.builder()
.userId(accessTokenDO.getUserId())
.accessToken(accessTokenDO.getAccessToken())
.refreshToken(accessTokenDO.getRefreshToken())
.expiresTime(accessTokenDO.getExpiresTime())
.build();
症状: 配置了 ignore-url-list 但请求仍被拦截
原因: Apereo CAS 客户端的 AuthenticationFilter.doFilter() 是 final 方法,无法覆盖
解决方案:
使用 CasPathFilter 在 CAS 过滤器链之前进行路径排除(当前方案)
或者使用 CAS 客户端的 ignorePattern 参数(需要进一步研究)
http://192.168.19.219:6080/dev-api/protocols/cas20http://127.0.0.1:48080http://localhost:8086curl -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创建完成
步骤 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
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"
}
}
curl http://127.0.0.1:48080/admin-api/system/auth/cas/logout
预期结果: 返回 CAS 登出 URL
| Order | 过滤器 | 状态 | 说明 |
|---|---|---|---|
| 0 | CasPathFilter | ✅ 启用 | 路径排除过滤 |
| 10 | SingleSignOutFilter | ✅ 启用 | 单点登出 |
| 20 | AuthenticationFilter | ❌ 禁用 | CAS 认证 |
| 30 | Cas20ProxyReceivingTicketValidationFilter | ❌ 禁用 | Ticket 验证 |
| 40 | HttpServletRequestWrapperFilter | ✅ 启用 | 请求包装 |
| 50 | AssertionThreadLocalFilter | ✅ 启用 | ThreadLocal |
logging:
level:
cn.start.tz.module.system.framework.cas: INFO
| 错误码 | 说明 | 解决方案 |
|---|---|---|
| 400 | 请求参数错误 | 检查请求格式 |
| 401 | 未认证 | 先完成 CAS 登录 |
| 403 | 用户被禁用 | 联系管理员 |
| 404 | 用户不存在 | 先创建本地账号 |
| 500 | 服务器内部错误 | 查看日志排查 |
文档版本: v1.0.0 最后更新: 2025-12-26 维护人员: tz