简介本资源是一套基于CAS、SpringMVC与Shiro三者深度集成的单点登录SSO完整实践项目面向Java Web开发中高级工程师及企业级权限系统设计人员解决多应用系统间统一身份认证与细粒度权限管控难题。项目涵盖CAS服务端搭建、SpringMVC客户端接入、Shiro自定义CasRealm与CasFilter实现、登出联动、权限校验及异常处理等核心环节具备生产级参考价值。压缩包共316个文件含38个Java业务与安全组件类、44个JSP视图页、38个XML配置文件如spring-mvc.xml、shiro.ini、33个properties属性配置、126个依赖jar包以及CSS/JS/图片等前端资源整体40.79MB结构清晰、模块分明便于按认证流程逐层研读调试。目前已有492人学习下载读者可直接部署运行获取可复用的SSO集成方案、票据验证逻辑实现、Shiro权限注解与标签控制示例以及CAS登录/登出全流程的完整代码骨架与配置模板。1. 单点登录不是加个依赖就能跑通的一个 CAS SpringMVC Shiro 的真实落地项目专治「登录态不一致」「登出失效」「权限校验绕过」三大玄学问题某高校实验室在做统一身份平台对接时曾把 CAS Client 依赖一加、ShiroFilter 配置一贴、casAuthenticationFilter一注册就以为单点登录成了。结果上线后用户从 A 系统登出B 系统仍能访问敏感接口Shiro 的RequiresPermissions(user:delete)注解形同虚设CAS 登录成功后跳转回 SpringMVC 控制器却拿不到当前用户主体——整个会话像被切成几段黑匣子。这个项目就是为解决这类血泪经验而生它不是一个 Demo 工程而是一个可直接嵌入生产级 SpringMVC 项目的完整认证授权骨架核心价值在于CAS 认证流与 Shiro 授权体系的深度缝合——不是简单桥接而是让 CAS 的CasAssertion成为 ShiroSubject的唯一可信来源让SecurityUtils.getSubject().getPrincipal()返回的不再是字符串而是带属性、可扩展、能参与权限决策的CasUser对象。适合正在用 SpringMVC 做老系统改造、又必须接入企业级 CAS 中心的开发同学尤其当你发现shiro-cas官方 Starter 在复杂拦截链下频繁翻车时这份源码就是你的后悔药。2. 架构设计三层认证授权链路拆解——CAS Filter 拦截 → Shiro Realm 转译 → 自定义 Subject 封装这个项目没走 Spring Boot 自动装配的捷径而是用最透明的方式暴露每层职责。它不隐藏 CAS 的协议细节也不抽象 Shiro 的 Realm 逻辑所有关键节点都可打断、可调试、可替换。整条链路分三段前端请求先撞上CasAuthenticationFilterCAS 客户端标准过滤器它负责重定向到 CAS Server、接收/login?ticketxxx回调、验证 ticket 并获取 assertion接着 assertion 被交给自定义的CasShiroRealm它不做任何网络调用只做两件事把 assertion 解析为CasUser实体并将该实体作为 Shiro Subject 的 principal最后所有 Controller 方法拿到的Subject都携带了完整的 CAS 属性如userAttributes.get(department)且其isPermitted()判断直接委托给CasShiroRealm的doGetAuthorizationInfo()方法。这种设计避免了常见误区比如把 CAS 用户名硬编码进 Shiro 的JdbcRealm或让 Shiro 自己去查数据库反查权限——在这里权限数据可以来自 LDAP、数据库、甚至内存 Map只要CasShiroRealm能组装出SimpleAuthorizationInfoShiro 就认。2.1 CAS 客户端配置不依赖 spring-boot-starter-cas手动注入 CasAuthenticationFilter项目未使用 Spring Boot Starter而是基于传统 SpringMVC 的web.xmlspring-mvc.xml方式配置确保你能看清每个 Filter 的执行顺序和参数含义。关键配置在spring-mvc.xml中!-- CAS 认证过滤器必须放在 ShiroFilter 之前 -- bean idcasAuthenticationFilter classorg.jasig.cas.client.authentication.AuthenticationFilter property namecasServerLoginUrl valuehttps://cas.example.com/cas/login/ property nameserverName valuehttp://localhost:8080/ property namerenew valuefalse/ property namegateway valuefalse/ /bean !-- CAS 验证过滤器用于校验 ticket -- bean idcasValidationFilter classorg.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter property namecasServerUrlPrefix valuehttps://cas.example.com/cas/ property nameserverName valuehttp://localhost:8080/ property nameuseSession valuetrue/ property nameredirectAfterValidation valuetrue/ /bean提示serverName必须是客户端应用的完整协议域名端口不能写localhost或省略端口useSessiontrue是关键它让 CAS 验证成功后把CasAssertion存入 HttpSession后续 Shiro Realm 才能取到。2.2 Shiro Filter 链配置显式声明 filterChainDefinitions避免默认规则覆盖 CAS 路径Shiro 的ShiroFilterFactoryBean是整个授权中枢它的filterChainDefinitionMap决定了哪些 URL 走 CAS 认证、哪些走 Shiro 授权、哪些放行。本项目严格区分三类路径bean idshiroFilter classorg.apache.shiro.spring.web.ShiroFilterFactoryBean property namesecurityManager refsecurityManager/ !-- CAS 回调路径必须放行否则 /login?ticketxxx 会被 Shiro 拦截 -- property namefilterChainDefinitionMap value /cas/login anon /cas/logout anon /login authc /logout logout /static/** anon /api/** authc, perms[api:read] /** user /value /property /bean注意/cas/login和/cas/logout必须设为anon因为它们由 CAS Filter 处理Shiro 不应介入而/login是你应用自己的登录入口通常重定向到 CAS必须走authc触发 CAS 认证流程。2.3 自定义 CasShiroRealm把 CAS Assertion 转为 Shiro Principal 和 AuthorizationInfo这是整个项目最核心的模块。CasShiroRealm继承AuthorizingRealm重写doGetAuthenticationInfo()和doGetAuthorizationInfo()。它不连接数据库只从当前 Session 中提取 CAS 的Assertion对象public class CasShiroRealm extends AuthorizingRealm { Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 1. 从 Session 获取 CAS Assertion由 CasValidationFilter 存入 HttpServletRequest request WebUtils.getHttpRequest(token); Assertion assertion (Assertion) request.getSession().getAttribute(AbstractCasFilter.CONST_CAS_ASSERTION); if (assertion null) { throw new AuthenticationException(CAS Assertion not found in session); } // 2. 提取用户名和属性构建 CasUser String username assertion.getPrincipal().getName(); MapString, Object attributes assertion.getPrincipal().getAttributes(); CasUser user new CasUser(); user.setUsername(username); user.setAttributes(attributes); // 3. 返回 SimpleAuthenticationInfoprincipal 是 CasUsercredentials 是空CAS 已验 return new SimpleAuthenticationInfo(user, , getName()); } Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { CasUser user (CasUser) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); // 权限数据可来自任意源这里演示从 CAS 属性中读取 roles ListString roles (ListString) user.getAttributes().get(roles); if (roles ! null) { info.addRoles(new HashSet(roles)); } // 也可查数据库或调用内部服务 // info.setStringPermissions(getPermissionsFromDb(user.getUsername())); return info; } }关键点doGetAuthenticationInfo()中不再做密码比对因为 CAS 已完成强认证principals.getPrimaryPrincipal()返回的就是你封装的CasUserController 里SecurityUtils.getSubject().getPrincipal()拿到的也是它——这才是真正可扩展的用户模型。3. 目录结构与核心文件清单5 个关键 Java 类 3 个 XML 配置决定成败项目采用标准 Maven 结构但刻意精简了无关模块所有认证授权逻辑集中在src/main/java/com/example/auth/下。以下是必须关注的 8 个文件删掉任何一个都会导致登录失败或权限失控文件路径类型作用说明是否可定制CasUser.javaJava 类CAS 用户实体含 username attributes Map✅ 强烈建议按你 CAS Server 返回的属性名调整字段CasShiroRealm.javaJava 类核心 Realm完成 assertion → principal → authorizationInfo 转换✅ 必须修改权限加载逻辑如连 LDAPCasLogoutFilter.javaJava 类扩展 CAS LogoutFilter主动清除 Shiro Session✅ 若需登出后跳转特定页面改onSuccessfulLogout()spring-mvc.xmlXML 配置声明 CasAuthenticationFilter、CasValidationFilter、ShiroFilter 及其顺序✅serverName和casServerUrlPrefix必须匹配你环境shiro.iniINI 配置定义 securityManager、realm、cacheManager本项目用 EhCache⚠️ 若不用缓存注释掉cacheManager相关行即可web.xmlXML 配置注册 CAS Filters 和 ShiroFilter顺序不可错✅CasAuthenticationFilter必须在ShiroFilter之前CasUserRealm.java已弃用Java 类旧版遗留仅作对比参考实际不启用❌ 删除或重命名避免误用UserController.javaJava 类示例 Controller演示RequiresPermissions和Subject.getPrincipal()用法✅ 可直接复用到你项目注意CasUserRealm.java是项目早期版本残留它试图用 JDBC 查库做二次认证与 CAS 设计冲突。本项目已完全移除其 Bean 声明若你在spring-mvc.xml中看到它请立即删除bean idcasUserRealm .../行否则 Shiro 会加载错误 Realm 导致认证失败。4. 运行与调试从零启动的 6 步实操指南附每步验证方法别急着mvn clean package先确认环境是否就绪。本项目要求 JDK 8、Tomcat 7因 CAS Client 3.4.x 不支持 Servlet 4.0、CAS Server 已部署并可访问。以下步骤在 Windows / macOS / Linux 下均适用命令用bash语法路径用正斜杠。4.1 步骤 1修改 CAS 地址配置必做否则 404打开src/main/resources/spring-mvc.xml找到两处地址!-- 修改为你的 CAS Server 地址 -- property namecasServerLoginUrl valuehttps://your-cas-domain.com/cas/login/ property namecasServerUrlPrefix valuehttps://your-cas-domain.com/cas/ !-- 修改为你的应用地址协议域名端口 -- property nameserverName valuehttp://your-app-domain.com:8080/验证方法浏览器访问https://your-cas-domain.com/cas/login应看到 CAS 登录页访问http://your-app-domain.com:8080/login应重定向到该页。4.2 步骤 2确认 Shiro Filter 在 web.xml 中位置正确检查src/main/webapp/WEB-INF/web.xml确保CasAuthenticationFilter和CasValidationFilter的filter-mapping出现在ShiroFilter之前!-- ✅ 正确顺序 -- filter-mapping filter-nameCasAuthenticationFilter/filter-name url-pattern/*/url-pattern /filter-mapping filter-mapping filter-nameCasValidationFilter/filter-name url-pattern/*/url-pattern /filter-mapping filter-mapping filter-nameShiroFilter/filter-name url-pattern/*/url-pattern /filter-mapping验证方法启动 Tomcat 后用浏览器开发者工具 Network 面板观察/login?ticketxxx请求Response Headers 中应有Set-Cookie: JSESSIONIDxxx且无 403。4.3 步骤 3编译打包并部署到 Tomcat# 在项目根目录执行 mvn clean compile war:war # 将生成的 target/cas-shiro-demo.war 复制到 Tomcat webapps 目录 cp target/cas-shiro-demo.war /path/to/tomcat/webapps/ # 启动 Tomcat /path/to/tomcat/bin/startup.sh验证方法Tomcat 日志中出现INFO: Starting ProtocolHandler [http-bio-8080]且无ClassNotFoundException: org.jasig.cas.client...错误。4.4 步骤 4首次访问触发 CAS 登录流程浏览器访问http://localhost:8080/cas-shiro-demo/login注意上下文路径。此时应重定向到https://your-cas-domain.com/cas/login输入 CAS 测试账号如 casuser / Mellon登录成功后重定向回http://localhost:8080/cas-shiro-demo/login?ticketST-xxx最终跳转到/index.jsp或你配置的 successUrl验证方法查看浏览器 Cookie应存在CASTGCCAS TGT Cookie和JSESSIONIDF12 Console 无 JS 报错。4.5 步骤 5验证 Shiro Subject 是否正确注入访问http://localhost:8080/cas-shiro-demo/user/info对应UserController.info()该方法有RequiresAuthentication注解。返回 JSON 应包含{ username: casuser, department: IT, roles: [USER, ADMIN], authenticated: true }验证方法若返回 401说明 Shiro 未识别登录态检查CasShiroRealm.doGetAuthenticationInfo()是否被调用加日志若username为空检查 CAS Server 返回的 assertion 中principal.getName()是否正常。4.6 步骤 6测试权限控制与登出访问/api/data需api:read权限有权限则返回数据无权限返回 403访问/logout应清除本地 Session 并重定向到 CAS 登出页再次访问/login应重新跳转 CAS 登录页而非自动登录。验证方法登出后检查浏览器 CookieJSESSIONID应失效新请求生成新 IDCASTGC应被 CAS Server 清除。5. 避坑指南5 个真实踩过的坑现象、原因、解决一步到位这类集成项目最大的痛苦不是写代码而是排查那些“看起来配置全对就是不生效”的问题。以下是我在三个不同客户现场反复遇到的 5 个高频问题每一条都附带定位命令和修复动作。5.1 现象访问/login无限重定向到 CAS再重定向回来死循环原因CasValidationFilter的redirectAfterValidationtrue与 Shiro 的authc规则冲突导致验证成功后 Shiro 又认为未认证再次触发 CAS 重定向。解决在spring-mvc.xml中将CasValidationFilter的redirectAfterValidation改为false并确保/login路径在 Shiro filterChain 中定义为authc见 2.2 节。同时检查serverName是否与浏览器地址完全一致http://vshttps://、www前缀、端口。5.2 现象CAS 登录成功但SecurityUtils.getSubject().getPrincipal()返回 null原因CasShiroRealm.doGetAuthenticationInfo()中未从 Session 正确获取Assertion常见于AbstractCasFilter.CONST_CAS_ASSERTION常量名拼写错误或CasValidationFilter未执行filter-mapping 顺序错。解决在doGetAuthenticationInfo()开头加日志log.info(Session attr keys: {}, request.getSession().getAttributeNames());确认是否存在CAS_ASSERTIONkey用curl -v http://localhost:8080/cas-shiro-demo/login?ticketST-123手动模拟看响应头是否有Set-Cookie。5.3 现象RequiresPermissions(xxx)注解不生效始终放行原因Shiro 的AuthorizationAttributeSourceAdvisor未启用或EnableAspectJAutoProxy缺失Spring MVC 项目需手动配置 Advisor。解决在spring-mvc.xml中添加bean classorg.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor property namesecurityManager refsecurityManager/ /bean并确认UserController是由 Spring 容器管理的 Bean非new UserController()。5.4 现象登出后再次访问/login仍自动登录CAS SSO 未退出原因应用登出只清除了本地 Session未通知 CAS Server 注销 TGT或CasLogoutFilter未配置。解决启用项目自带的CasLogoutFilter已在web.xml中声明并在spring-mvc.xml中配置其casServerUrlPrefix确保/logout请求被该 Filter 拦截filter-mapping 顺序在 ShiroFilter 之后。5.5 现象CAS 返回的用户属性如 email、department在CasUser中为 null原因CAS Server 默认不返回属性需在deployerConfigContext.xml中配置attributeRepository且客户端Cas30ProxyReceivingTicketValidationFilter需设置useSessiontrue。解决检查 CAS Server 日志确认attributeRepository已加载在客户端CasValidationFilter配置中确认useSessiontrue在CasShiroRealm中用assertion.getPrincipal().getAttributes().keySet()打印所有 key确认属性名是否与代码中get(department)一致注意大小写。6. 二次开发实战3 种权限模型扩展方案从静态配置到动态同步当你跑通基础流程后真正的挑战才开始如何把这套 CASShiro 骨架适配到你公司的权限体系我见过太多团队卡在“权限从哪来”这一环。下面给出三种经过生产验证的扩展路径每种都附可直接粘贴的代码片段和验证要点。6.1 方案一CAS 属性直传最快上线适合角色固定的小团队如果 CAS Server 已配置好attributeRepository且角色/权限字段稳定如roles[USER,ADMIN],permissions[user:read,user:write]直接在CasShiroRealm.doGetAuthorizationInfo()中解析Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { CasUser user (CasUser) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); // 从 CAS 属性读取 roles字符串数组 Object rolesObj user.getAttributes().get(roles); if (rolesObj instanceof List) { info.addRoles(new HashSet((ListString) rolesObj)); } // 从 CAS 属性读取 permissions字符串数组 Object permsObj user.getAttributes().get(permissions); if (permsObj instanceof List) { info.addStringPermissions(new HashSet((ListString) permsObj)); } return info; }验证登录后调用/user/info检查返回 JSON 中roles和permissions字段是否与 CAS Server 返回一致用RequiresPermissions(user:write)注解保护接口确认无权限用户 403。6.2 方案二数据库权限映射推荐平衡灵活性与可控性大多数企业权限分散在数据库CAS 只提供唯一标识。此时CasShiroRealm需查库。本项目已预留JdbcPermissionService接口实现类如下Component public class DbPermissionService implements PermissionService { Autowired private JdbcTemplate jdbcTemplate; Override public SetString findRolesByUsername(String username) { String sql SELECT role_code FROM sys_user_role ur JOIN sys_role r ON ur.role_id r.id WHERE ur.user_code ?; return new HashSet(jdbcTemplate.queryForList(sql, String.class, username)); } Override public SetString findPermissionsByRoleCodes(SetString roleCodes) { if (roleCodes.isEmpty()) return Collections.emptySet(); String placeholders String.join(,, Collections.nCopies(roleCodes.size(), ?)); String sql SELECT permission_code FROM sys_role_permission rp JOIN sys_permission p ON rp.permission_id p.id WHERE rp.role_code IN ( placeholders ); return new HashSet(jdbcTemplate.queryForList(sql, String.class, roleCodes.toArray())); } }然后在CasShiroRealm中注入并使用Autowired private PermissionService permissionService; Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { CasUser user (CasUser) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); SetString roles permissionService.findRolesByUsername(user.getUsername()); info.addRoles(roles); SetString permissions permissionService.findPermissionsByRoleCodes(roles); info.addStringPermissions(permissions); return info; }验证修改数据库sys_user_role表为测试用户添加新角色重启应用确认/user/info返回更新后的 roles调用新权限接口确认鉴权生效。6.3 方案三LDAP 实时同步适合大型组织权限变更需秒级生效当公司用 LDAP 管理员工信息且权限策略随部门/职级自动变化时每次登录都查 LDAP 更可靠。本项目CasShiroRealm支持LdapPermissionService关键代码Component public class LdapPermissionService implements PermissionService { Value(${ldap.url}) private String ldapUrl; Value(${ldap.base}) private String ldapBase; Value(${ldap.user}) private String ldapUser; Value(${ldap.password}) private String ldapPassword; Override public SetString findRolesByUsername(String username) { LdapContext ctx null; try { ctx createLdapContext(); // 查询用户所属组CNgroupname String filter ((objectClassgroup)(memberUid username )); NamingEnumerationSearchResult results ctx.search(ldapBase, filter, new SearchControls()); SetString roles new HashSet(); while (results.hasMore()) { String dn results.next().getNameInNamespace(); roles.add(extractRoleFromDn(dn)); // 如 CNIT-Admin,OUGroups - IT-Admin } return roles; } catch (Exception e) { log.error(LDAP query failed for user: {}, username, e); return Collections.emptySet(); } finally { safeClose(ctx); } } }验证在 LDAP 中为测试用户新增一个组成员关系不重启应用再次登录确认/user/info返回新角色检查 Tomcat 日志是否有LDAP query failed报错。从那以后我每次接手 CAS 集成项目都强制走一遍这三步先用方案一验证 CAS 属性通路再切方案二连通数据库权限表最后按需接入 LDAP。不是为了炫技而是因为权限逻辑一旦写死在代码里后面改起来比重构整个登录流程还疼。希望帮到你。本文还有配套的精品资源点击获取