☰
CAS+SpringMVC+Shiro单点登录集成实战与避坑指南
2026/10/14 15:15:06 网站建设 项目流程

简介:本资源是一套将CAS、SpringMVC与Shiro三者整合实现单点登录(SSO)的完整实践示例,面向具备一定Java Web基础、希望深入理解SSO认证流程与安全框架集成的开发者。内容围绕CAS服务端与客户端配置、Shiro的CasRealm与CasFilter定制、SpringMVC拦截器处理票据校验,以及登录登出、权限控制与异常处理等关键环节展开,帮助读者打通认证票据在服务端与各应用间的流转逻辑。压缩包共316个文件,约40.79MB,以jar依赖包、jsp页面、java源码、xml配置与properties文件为主,另含少量前端图片、js与css资源,结构贴近真实项目。目前已有492人学习下载,可作为搭建SSO环境、理解CAS与Shiro协作机制、排查票据验证问题的参考实例。

1. CAS 结合 SpringMVC 与 Shiro 的单点登录:为什么你的第一次集成就翻车

很多团队第一次做单点登录,都是从一个看似简单的需求开始的:公司内部有好几个后台系统,用户不想每个系统都注册一遍、登录一遍。于是有人翻出 CAS——Central Authentication Service,中央认证服务,一个老牌但极其稳定的 SSO 协议实现。然后问题来了:CAS 服务端搭好了,客户端怎么跟 SpringMVC 和 Shiro 揉在一起?

我见过太多项目卡在这一步。CAS 官方文档给的是 Servlet Filter 的集成方式,Shiro 也有自己的 Filter 链,SpringMVC 还有自己的拦截器和上下文。三套 Filter 机制叠在一起,谁先谁后、谁管认证、谁管授权、Session 怎么共享、Ticket 怎么校验,任何一个环节没对齐,结果就是无限重定向或者 401 死循环。

这篇笔记就是拆这个问题的。我会从 CAS 协议的核心流程讲起,然后一步步把 SpringMVC 的 Controller 层、Shiro 的 SecurityManager 和 CAS 的 Ticket 校验串起来,中间会给出可直接抄的配置和代码,最后把那些让我加班到凌晨的坑一个个列出来。适合正在做内部系统整合、需要落地 SSO 的后端开发,也适合已经集成过但被重定向问题折磨过的同行。

2. CAS 协议的核心流程与 Shiro 集成选型

2.1 CAS 的 Ticket 机制到底在干什么

CAS 的核心其实就两个东西:TGT 和 ST。TGT(Ticket Granting Ticket)是用户第一次在 CAS 服务端登录成功后,服务端种在浏览器里的一个 Cookie 对应的凭证,它代表“这个用户已经认证过了”。ST(Service Ticket)是客户端服务拿着 TGT 去换的一次性票据,用来证明“这个用户确实有权限访问我”。

整个流程走一遍:用户访问系统 A,系统 A 发现没有本地 Session,于是重定向到 CAS 服务端,带上自己的 service 参数。CAS 服务端检查有没有 TGT,没有就让用户登录,登录成功后生成 ST,重定向回系统 A 的回调地址。系统 A 拿到 ST,在后台向 CAS 服务端发起校验请求,CAS 返回用户身份信息,系统 A 建立本地 Session,完成登录。

这个流程里最关键的一点:ST 是一次性的,校验一次就失效。这意味着你不能在前端拿 ST 做任何缓存,也不能在多个请求里复用同一个 ST。很多新手在这里翻车,就是因为把 ST 当成了长期凭证。

2.2 为什么选 Shiro 而不是 Spring Security

Spring Security 确实有官方的 CAS 支持,配置起来也算顺手。但如果你已经在一个用 Shiro 做权限控制的项目里,再引入 Spring Security 就意味着一套应用里跑两套安全框架,Filter 链会打架,Session 管理也会冲突。

Shiro 的优势在于它的 Filter 链是可插拔的,你可以自定义一个 CasFilter 挂在 authc 的位置,接管认证逻辑。Shiro 的 Subject 和 SecurityManager 模型也比 Spring Security 的上下文更直观,至少对我来说,调试的时候更容易看清“当前用户是谁、有什么权限”。

选型建议:新项目如果没有任何安全框架,Spring Security + CAS 是更省心的选择;老项目已经用了 Shiro,就老老实实写一个 CasFilter,别折腾双框架共存。

2.3 最小可跑的集成架构

一个能跑通的架构至少包含三个部分:CAS 服务端(独立部署)、客户端应用的 Shiro 配置、以及 SpringMVC 的 Controller 层回调处理。

CAS 服务端我一般用官方推荐的 WAR 包部署,数据库认证源接 MySQL,这些不是本文重点。客户端这边,Shiro 的配置需要做三件事:定义 CasFilter、配置 SecurityManager 使用 CAS 的 Realm、在 SpringMVC 里暴露一个 /cas/callback 接口用来接收 ST。

下面是一个最小的 Shiro 配置片段,用 Java Config 方式写,比 XML 更容易看清依赖关系:

@Configuration public class ShiroCasConfig { @Bean public CasFilter casFilter() { CasFilter filter = new CasFilter(); // 认证失败时跳转到 CAS 服务端登录页 filter.setLoginUrl("https://cas.example.com/cas/login"); // 认证成功后回到这个地址,由 Controller 处理 ST filter.setSuccessUrl("/cas/callback"); return filter; } @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager) { ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean(); factory.setSecurityManager(securityManager); Map<String, Filter> filters = new HashMap<>(); filters.put("cas", casFilter()); factory.setFilters(filters); Map<String, String> chain = new LinkedHashMap<>(); // 回调地址放行,否则 ST 校验请求会被拦截 chain.put("/cas/callback", "anon"); // 其余请求走 CAS 认证 chain.put("/**", "cas"); factory.setFilterChainDefinitionMap(chain); return factory; } }

这段代码的逻辑说明:CasFilter 继承自 Shiro 的 AuthenticatingFilter,核心是重写 isAccessAllowed 和 onAccessDenied 方法。isAccessAllowed 判断当前 Subject 是否已认证,未认证则触发 onAccessDenied,在 onAccessDenied 里构造 CAS 登录 URL 并重定向。参数方面,loginUrl 必须指向 CAS 服务端的 /login 路径,successUrl 是本地回调地址,这两个地址的域名和端口必须和 CAS 服务端配置的 service 白名单一致,否则 CAS 会拒绝重定向。

3. 从零搭建 CAS 客户端:Shiro Realm 与 SpringMVC 回调

3.1 自定义 CasRealm 的认证逻辑

Shiro 的 Realm 是认证和授权的数据源。CAS 集成场景下,Realm 的认证逻辑不再是查数据库比对密码,而是校验 ST 并解析 CAS 返回的用户信息。

public class CasRealm extends AuthorizingRealm { private String casServerUrl = "https://cas.example.com/cas"; private String clientServiceUrl = "https://app.example.com/cas/callback"; @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) { CasToken casToken = (CasToken) token; String ticket = (String) casToken.getCredentials(); if (StringUtils.isBlank(ticket)) { throw new AuthenticationException("ST 为空,无法认证"); } // 构造 CAS 校验地址 String validateUrl = casServerUrl + "/serviceValidate?ticket=" + ticket + "&service=" + URLEncoder.encode(clientServiceUrl, "UTF-8"); // 发起 HTTP 请求校验 ST String response = HttpUtils.get(validateUrl); // 解析 XML,提取用户身份 String username = CasXmlParser.parseUsername(response); if (username == null) { throw new AuthenticationException("CAS 校验失败,ST 无效"); } // 返回认证信息,principal 是用户名,credentials 是 ST return new SimpleAuthenticationInfo(username, ticket, getName()); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { String username = (String) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 这里可以从本地数据库加载角色和权限 info.setRoles(roleService.findRolesByUsername(username)); info.setStringPermissions(permissionService.findPermissionsByUsername(username)); return info; } }

逻辑说明:doGetAuthenticationInfo 接收的 token 是 CasToken,credentials 里存的是 ST。校验地址的 service 参数必须和 CAS 服务端配置的 service 完全一致,包括协议、域名、端口、路径,差一个字符都会导致校验失败。CAS 返回的是 XML 格式,需要解析出 cas:user 节点的值作为用户名。参数方面,casServerUrl 和 clientServiceUrl 建议放在配置文件中,不要硬编码,因为测试环境和生产环境不一样。

3.2 SpringMVC 回调接口怎么写才不丢 ST

CAS 校验成功后重定向回客户端,URL 上会带一个 ticket 参数。SpringMVC 的 Controller 需要接收这个参数,然后触发 Shiro 的登录流程。

@Controller public class CasCallbackController { @RequestMapping("/cas/callback") public String callback(@RequestParam("ticket") String ticket, HttpServletRequest request, HttpServletResponse response) { // 构造 CasToken,交给 Shiro 认证 CasToken token = new CasToken(ticket); Subject subject = SecurityUtils.getSubject(); try { subject.login(token); } catch (AuthenticationException e) { // 认证失败,重定向到 CAS 登录页重新走流程 return "redirect:https://cas.example.com/cas/login?service=" + URLEncoder.encode("https://app.example.com/cas/callback"); } // 认证成功,跳转到首页 return "redirect:/index"; } }

逻辑说明:这个接口必须配置为 anon,否则 Shiro 的 CasFilter 会拦截它,导致 ST 还没校验就被重定向走。subject.login(token) 会触发 CasRealm 的 doGetAuthenticationInfo,完成 ST 校验。如果校验失败,不要直接抛异常给用户,而是重定向回 CAS 登录页,让用户重新登录。参数方面,ticket 参数名是 CAS 协议固定的,不能改;service 参数在重定向时必须和 CAS 服务端配置的一致。

3.3 Shiro 的 Session 管理与 CAS 单点登出的配合

单点登录做完了,单点登出是下一个坑。CAS 的单点登出需要客户端暴露一个 /cas/logout 接口,CAS 服务端在用户登出时会向所有注册的客户端发送登出通知。

@Controller public class CasLogoutController { @RequestMapping("/cas/logout") public String logout(HttpServletRequest request) { Subject subject = SecurityUtils.getSubject(); if (subject != null) { subject.logout(); } // 清除本地 Session request.getSession().invalidate(); return "redirect:https://cas.example.com/cas/logout"; } }

逻辑说明:subject.logout() 会清除 Shiro 的认证信息,request.getSession().invalidate() 清除 Servlet Session。然后重定向到 CAS 服务端的登出地址,CAS 会清除 TGT,并向其他客户端发送登出通知。参数方面,这个接口也必须配置为 anon,否则登出请求会被拦截。注意,CAS 的单点登出依赖服务端能访问到客户端的登出地址,如果客户端在内网,需要确保网络可达。

4. 避坑指南:CAS 集成中最容易翻车的五个地方

4.1 无限重定向:ST 校验通过但 Session 没建立

现象:用户登录成功后,浏览器一直在 CAS 服务端和客户端之间来回跳转,最终报“重定向次数过多”。

原因:CasFilter 的 isAccessAllowed 判断的是 Subject.isAuthenticated(),但 ST 校验成功后,Shiro 的 Subject 并没有自动变成已认证状态。需要在 CasRealm 返回 AuthenticationInfo 后,Shiro 才会更新 Subject 状态。如果 CasFilter 在回调接口之前执行,就会一直认为用户未认证。

解决:确保 /cas/callback 配置为 anon,让回调接口先执行 subject.login(),完成认证后再放行其他请求。另外检查 CasFilter 的 successUrl 是否配置正确,如果 successUrl 指向的地址也被 CasFilter 拦截,就会形成死循环。

4.2 ST 校验返回 400:service 参数不匹配

现象:CAS 服务端返回 400 Bad Request,提示“service not authorized”。

原因:客户端构造的 service 参数和 CAS 服务端配置的白名单不一致。常见的不一致包括:http 和 https 混用、端口号省略或多余、路径末尾多了斜杠、URL 编码方式不同。

解决:在 CAS 服务端的 service 注册文件里,把客户端的 service 地址完整写进去,包括协议、域名、端口、路径。客户端构造 service 参数时,用 URLEncoder.encode 编码,确保特殊字符不会丢失。测试时可以在浏览器地址栏直接访问校验地址,看 CAS 返回的具体错误信息。

4.3 Shiro 的 Session 和 CAS 的 TGT 不同步

现象:用户在系统 A 登出后,系统 B 仍然保持登录状态。

原因:CAS 的单点登出需要客户端主动清除本地 Session,如果客户端没有暴露 /cas/logout 接口,或者 CAS 服务端无法访问到该接口,登出通知就发不出去。

解决:在每个客户端都实现 /cas/logout 接口,并配置为 anon。CAS 服务端的登出通知地址需要和客户端的实际地址一致。如果客户端在内网,确保 CAS 服务端能通过网络访问到。测试时可以先手动访问 /cas/logout,确认本地 Session 被清除。

4.4 SpringMVC 的拦截器和 Shiro Filter 顺序冲突

现象:自定义的 SpringMVC 拦截器在 Shiro 认证之前执行,导致获取不到当前用户信息。

原因:Shiro 的 Filter 是在 Servlet 容器层面执行的,而 SpringMVC 的拦截器是在 DispatcherServlet 内部执行的。Filter 的顺序由 web.xml 或 Java Config 中的 FilterRegistrationBean 决定,如果 Shiro Filter 的 order 值比 SpringMVC 的 DispatcherServlet 大,就会后执行。

解决:在 Spring Boot 项目中,通过 FilterRegistrationBean 设置 Shiro Filter 的 order 为 1,确保它在 DispatcherServlet 之前执行。如果是传统 web.xml 项目,把 Shiro Filter 的 mapping 放在 DispatcherServlet 之前。

4.5 CAS 服务端证书不被信任导致校验失败

现象:ST 校验时抛出 SSLHandshakeException,提示“unable to find valid certification path”。

原因:CAS 服务端使用 HTTPS,客户端 JVM 的 truststore 里没有导入 CAS 服务端的证书。

解决:把 CAS 服务端的证书导出,用 keytool 导入到客户端 JVM 的 cacerts 文件中。命令如下:

# 导出 CAS 服务端证书 openssl s_client -connect cas.example.com:443 </dev/null | openssl x509 -out cas.crt # 导入到 JVM truststore keytool -import -alias cas -keystore $JAVA_HOME/jre/lib/security/cacerts -file cas.crt

参数说明:-alias 是证书别名,-keystore 是 truststore 路径,默认密码是 changeit。导入后重启客户端应用生效。如果不想改 JVM 全局 truststore,可以在 CAS 校验的 HTTP 客户端里单独配置 SSLSocketFactory。

5. 进阶:用 CAS 代理模式做跨系统权限传递

CAS 除了基本的 SSO,还支持代理模式(Proxy Mode),允许一个客户端服务代表用户去访问另一个客户端服务。这个模式在微服务架构里特别有用:网关服务认证用户后,拿着 PT(Proxy Ticket)去调用下游服务,下游服务校验 PT 后信任网关传递的用户身份。

代理模式的核心是 PGT 和 PT。PGT 是客户端服务在用户认证成功后,向 CAS 服务端申请的一个凭证,用来生成 PT。PT 和 ST 类似,也是一次性的,但 PT 可以指定目标服务。

下面是一个申请 PGT 和生成 PT 的代码片段:

// 在 CasRealm 认证成功后,申请 PGT String pgtUrl = "https://app.example.com/cas/proxyCallback"; String pgtRequestUrl = casServerUrl + "/proxy?pgt=" + pgt + "&targetService=" + URLEncoder.encode(pgtUrl, "UTF-8"); // CAS 会回调 pgtUrl,带上 PGTIOU,客户端需要保存 PGTIOU 和 PGT 的映射

逻辑说明:PGT 的申请需要在 ST 校验成功后立即进行,CAS 会通过回调 pgtUrl 的方式把 PGT 和 PGTIOU 传给客户端。客户端需要把这两个值存起来,后续生成 PT 时使用。参数方面,pgtUrl 必须是 HTTPS,CAS 服务端会校验这个地址的证书。

代理模式的坑在于:PGT 的有效期和 TGT 绑定,TGT 过期后 PGT 也失效。另外,PT 的生成需要指定 targetService,这个地址也必须在 CAS 服务端的白名单里。如果下游服务很多,白名单维护会变成一个体力活。

我一般会在网关层做统一的 PT 管理,下游服务只负责校验 PT,不直接和 CAS 服务端交互。这样可以把 CAS 的复杂性收敛到一个点上,出问题的时候排查范围也小。

最后一个习惯:每次改完 CAS 相关配置,先用 curl 手动走一遍完整流程,确认 ST 校验、PT 生成、登出通知都正常,再交给前端联调。这个习惯帮我省了至少三次通宵排查的时间。希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询