简介:本资源是一套基于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 逻辑,所有关键节点都可打断、可调试、可替换。整条链路分三段:前端请求先撞上CasAuthenticationFilter(CAS 客户端标准过滤器),它负责重定向到 CAS Server、接收/login?ticket=xxx回调、验证 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能组装出SimpleAuthorizationInfo,Shiro 就认。
2.1 CAS 客户端配置:不依赖 spring-boot-starter-cas,手动注入 CasAuthenticationFilter
项目未使用 Spring Boot Starter,而是基于传统 SpringMVC 的web.xml+spring-mvc.xml方式配置,确保你能看清每个 Filter 的执行顺序和参数含义。关键配置在spring-mvc.xml中:
<!-- CAS 认证过滤器,必须放在 ShiroFilter 之前 --> <bean id="casAuthenticationFilter" class="org.jasig.cas.client.authentication.AuthenticationFilter"> <property name="casServerLoginUrl" value="https://cas.example.com/cas/login"/> <property name="serverName" value="http://localhost:8080"/> <property name="renew" value="false"/> <property name="gateway" value="false"/> </bean> <!-- CAS 验证过滤器,用于校验 ticket --> <bean id="casValidationFilter" class="org.jasig.cas.client.validation.Cas30ProxyReceivingTicketValidationFilter"> <property name="casServerUrlPrefix" value="https://cas.example.com/cas"/> <property name="serverName" value="http://localhost:8080"/> <property name="useSession" value="true"/> <property name="redirectAfterValidation" value="true"/> </bean>提示:
serverName必须是客户端应用的完整协议+域名+端口,不能写localhost或省略端口;useSession=true是关键,它让 CAS 验证成功后把CasAssertion存入 HttpSession,后续 Shiro Realm 才能取到。
2.2 Shiro Filter 链配置:显式声明 filterChainDefinitions,避免默认规则覆盖 CAS 路径
Shiro 的ShiroFilterFactoryBean是整个授权中枢,它的filterChainDefinitionMap决定了哪些 URL 走 CAS 认证、哪些走 Shiro 授权、哪些放行。本项目严格区分三类路径:
<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"> <property name="securityManager" ref="securityManager"/> <!-- CAS 回调路径必须放行,否则 /login?ticket=xxx 会被 Shiro 拦截 --> <property name="filterChainDefinitionMap"> <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(); Map<String, Object> attributes = assertion.getPrincipal().getAttributes(); CasUser user = new CasUser(); user.setUsername(username); user.setAttributes(attributes); // 3. 返回 SimpleAuthenticationInfo,principal 是 CasUser,credentials 是空(CAS 已验) return new SimpleAuthenticationInfo(user, "", getName()); } @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { CasUser user = (CasUser) principals.getPrimaryPrincipal(); SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 权限数据可来自任意源:这里演示从 CAS 属性中读取 roles List<String> roles = (List<String>) user.getAttributes().get("roles"); if (roles != null) { info.addRoles(new HashSet<>(roles)); } // 也可查数据库或调用内部服务 // info.setStringPermissions(getPermissionsFromDb(user.getUsername())); return info; } }关键点:doGetAuthenticationInfo()中不再做密码比对,因为 CAS 已完成强认证;principals.getPrimaryPrincipal()返回的就是你封装的CasUser,Controller 里SecurityUtils.getSubject().getPrincipal()拿到的也是它——这才是真正可扩展的用户模型。
3. 目录结构与核心文件清单:5 个关键 Java 类 + 3 个 XML 配置决定成败
项目采用标准 Maven 结构,但刻意精简了无关模块,所有认证授权逻辑集中在src/main/java/com/example/auth/下。以下是必须关注的 8 个文件,删掉任何一个都会导致登录失败或权限失控:
| 文件路径 | 类型 | 作用说明 | 是否可定制 |
|---|---|---|---|
CasUser.java | Java 类 | CAS 用户实体,含 username + attributes Map | ✅ 强烈建议按你 CAS Server 返回的属性名调整字段 |
CasShiroRealm.java | Java 类 | 核心 Realm,完成 assertion → principal → authorizationInfo 转换 | ✅ 必须修改权限加载逻辑(如连 LDAP) |
CasLogoutFilter.java | Java 类 | 扩展 CAS LogoutFilter,主动清除 Shiro Session | ✅ 若需登出后跳转特定页面,改onSuccessfulLogout() |
spring-mvc.xml | XML 配置 | 声明 CasAuthenticationFilter、CasValidationFilter、ShiroFilter 及其顺序 | ✅serverName和casServerUrlPrefix必须匹配你环境 |
shiro.ini | INI 配置 | 定义 securityManager、realm、cacheManager(本项目用 EhCache) | ⚠️ 若不用缓存,注释掉cacheManager相关行即可 |
web.xml | XML 配置 | 注册 CAS Filters 和 ShiroFilter,顺序不可错 | ✅CasAuthenticationFilter必须在ShiroFilter之前 |
CasUserRealm.java(已弃用) | Java 类 | 旧版遗留,仅作对比参考,实际不启用 | ❌ 删除或重命名避免误用 |
UserController.java | Java 类 | 示例 Controller,演示@RequiresPermissions和Subject.getPrincipal()用法 | ✅ 可直接复用到你项目 |
注意:
CasUserRealm.java是项目早期版本残留,它试图用 JDBC 查库做二次认证,与 CAS 设计冲突。本项目已完全移除其 Bean 声明,若你在spring-mvc.xml中看到它,请立即删除<bean id="casUserRealm" .../>行,否则 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 name="casServerLoginUrl" value="https://your-cas-domain.com/cas/login"/> <property name="casServerUrlPrefix" value="https://your-cas-domain.com/cas"/> <!-- 修改为你的应用地址(协议+域名+端口) --> <property name="serverName" value="http://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-name>CasAuthenticationFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>CasValidationFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping> <filter-mapping> <filter-name>ShiroFilter</filter-name> <url-pattern>/*</url-pattern> </filter-mapping>验证方法:启动 Tomcat 后,用浏览器开发者工具 Network 面板观察
/login?ticket=xxx请求,Response Headers 中应有Set-Cookie: JSESSIONID=xxx,且无 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?ticket=ST-xxx - 最终跳转到
/index.jsp或你配置的 successUrl
验证方法:查看浏览器 Cookie,应存在
CASTGC(CAS TGT Cookie)和JSESSIONID;F12 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 登录页,而非自动登录。
验证方法:登出后检查浏览器 Cookie,
JSESSIONID应失效(新请求生成新 ID),CASTGC应被 CAS Server 清除。
5. 避坑指南:5 个真实踩过的坑,现象、原因、解决一步到位
这类集成项目最大的痛苦不是写代码,而是排查那些“看起来配置全对,就是不生效”的问题。以下是我在三个不同客户现场反复遇到的 5 个高频问题,每一条都附带定位命令和修复动作。
5.1 现象:访问/login无限重定向到 CAS,再重定向回来,死循环
原因:CasValidationFilter的redirectAfterValidation=true与 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?ticket=ST-123手动模拟,看响应头是否有Set-Cookie。
5.3 现象:@RequiresPermissions("xxx")注解不生效,始终放行
原因:Shiro 的AuthorizationAttributeSourceAdvisor未启用,或@EnableAspectJAutoProxy缺失(Spring MVC 项目需手动配置 Advisor)。
解决:在spring-mvc.xml中添加:
<bean class="org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor"> <property name="securityManager" ref="securityManager"/> </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需设置useSession=true。
解决:检查 CAS Server 日志,确认attributeRepository已加载;在客户端CasValidationFilter配置中确认useSession=true;在CasShiroRealm中用assertion.getPrincipal().getAttributes().keySet()打印所有 key,确认属性名是否与代码中get("department")一致(注意大小写)。
6. 二次开发实战:3 种权限模型扩展方案,从静态配置到动态同步
当你跑通基础流程后,真正的挑战才开始:如何把这套 CAS+Shiro 骨架,适配到你公司的权限体系?我见过太多团队卡在“权限从哪来”这一环。下面给出三种经过生产验证的扩展路径,每种都附可直接粘贴的代码片段和验证要点。
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<>((List<String>) rolesObj)); } // 从 CAS 属性读取 permissions(字符串数组) Object permsObj = user.getAttributes().get("permissions"); if (permsObj instanceof List) { info.addStringPermissions(new HashSet<>((List<String>) 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 Set<String> 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 Set<String> findPermissionsByRoleCodes(Set<String> 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(); Set<String> roles = permissionService.findRolesByUsername(user.getUsername()); info.addRoles(roles); Set<String> 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 Set<String> findRolesByUsername(String username) { LdapContext ctx = null; try { ctx = createLdapContext(); // 查询用户所属组(CN=groupname) String filter = "(&(objectClass=group)(memberUid=" + username + "))"; NamingEnumeration<SearchResult> results = ctx.search(ldapBase, filter, new SearchControls()); Set<String> roles = new HashSet<>(); while (results.hasMore()) { String dn = results.next().getNameInNamespace(); roles.add(extractRoleFromDn(dn)); // 如 CN=IT-Admin,OU=Groups -> "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。不是为了炫技,而是因为权限逻辑一旦写死在代码里,后面改起来比重构整个登录流程还疼。希望帮到你。
本文还有配套的精品资源,点击获取