ARTICLE DETAIL

资讯详情

深耕商务建站与企业官网运营的一线实战洞察。

CAS+SpringMVC+Shiro单点登录实战:解耦认证与权限

CAS+SpringMVC+Shiro单点登录实战:解耦认证与权限 简介本资源是一套基于CAS、SpringMVC与Shiro三者深度集成的单点登录SSO完整实践项目面向Java Web开发中高级工程师及企业级权限系统构建者解决多应用系统间统一身份认证与细粒度权限管控难题。压缩包共316个文件含126个jar依赖库支撑CAS客户端、Shiro安全框架及Spring生态、44个JSP页面登录跳转、票据验证响应等前端交互、38个Java类含自定义CasRealm、CasFilter及拦截器实现、38个XML配置SpringMVC与Shiro核心配置、33个properties环境与CAS服务端地址等参数化配置整体40.79MB结构清晰、模块职责分明。已有492人学习下载可直接部署运行完整呈现CAS服务端对接、客户端票据校验、Shiro权限动态加载、登出会话同步及异常处理等关键流程附带CSS/JS/GIF等UI资源便于快速理解SSO交互细节并复用于实际项目。1. CAS SpringMVC Shiro 单点登录不是堆技术而是解耦认证与业务的实战方案你有没有遇到过这样的场景一个企业内部有七八个老系统有的用 Struts2有的是 SpringMVC还有一套遗留的 JSPServlet 应用现在要统一登录入口要求用户一次登录、全站通行但又不能推翻重写这时候堆“Spring Security OAuth2”可能反而卡在适配层——而 CAS SpringMVC Shiro 的组合恰恰是这类存量系统改造中最稳的一条路。它不追求最新潮但胜在成熟、可插拔、边界清晰CAS 负责跨域身份核验你是谁Shiro 负责本系统权限控制你能做什么SpringMVC 则专注业务逻辑流转你接下来干啥。这套方案不是为炫技设计的而是为某高校教务平台、某制造企业 ERP 集成、某实验室多子系统管理等真实项目打磨出来的落地路径。如果你正在做毕设/课设需要一份能跑通、能讲清、能答辩的单点登录完整实现而不是只贴几行配置的“伪方案”那这份基于真实部署经验整理的 CASSpringMVCShiro 全流程设计就是你该抄的作业。2. 总体架构设计三层职责分离与四个核心模块的协同机制这套方案的本质是把“身份认证”从每个业务系统中剥离出来交由独立的 CAS Server 统一处理各业务系统Client只保留自身权限逻辑通过 Shiro 做细粒度授权。整个链路不是线性调用而是分层协作网络层走 HTTPS 保障票据安全协议层依赖 CAS 3.0 协议完成票据签发与验证应用层则由 SpringMVC 承接请求、Shiro 拦截并注入 Subject。这种结构让每个模块都可独立演进——比如未来把 CAS Server 升级到 Apereo CAS 6.x只要协议兼容业务系统几乎零改动。2.1 系统角色划分与通信边界我们明确划出三类角色每类角色有且仅有一个核心职责CAS Server认证中心独立部署的 Web 应用如cas-server-webapp只做两件事接收登录请求、发放 Service TicketST和 Proxy TicketPT验证 ST/PT 合法性并返回用户属性如username,email,department。它不存业务权限也不管用户能访问哪个菜单。Client 应用业务系统即你的 SpringMVC 项目它不处理用户名密码只信任 CAS Server 返回的票据。收到 ST 后主动向 CAS Server 的/p3/serviceValidate接口发起校验请求拿到 XML 响应后解析出用户标识再交由 Shiro 构建Subject。Shiro Realm权限桥接器这是最关键的胶水层。它不自己查数据库验证密码而是接收 CAS 校验成功后的username去本地数据库或 LDAP 查询该用户的角色与权限字符串如role:admin,perm:report:export完成从“身份”到“权限”的映射。提示很多初学者误以为 Shiro 在这里要做登录校验其实完全相反——Shiro 的doGetAuthenticationInfo()方法在此场景下永远不执行密码比对它只做“已知用户查权限”。真正的身份认证已在 CAS Server 完成。2.2 四大核心模块的功能定义与数据流向整个流程拆解为四个不可省略的模块每个模块对应一段可独立测试的代码或配置模块所属层级核心职责关键输出CAS Filter 拦截器Client 网络层拦截未登录请求重定向至 CAS Server 登录页拦截带ticket参数的回调请求ticket参数提取、重定向 URL 构造CAS Validation ServiceClient 服务层调用 CAS Server 的/serviceValidate接口传入service和ticket解析 XML 响应Assertion对象含principal.getName()和attributesShiro CAS RealmClient 安全层接收principal.getName()查询本地权限数据封装为SimpleAuthenticationInfoSimpleAuthorizationInfo含角色列表与权限字符串集合SpringMVC Controller 集成点Client 表现层提供/login入口实际跳转 CAS、/logout清理本地 Session 并重定向 CAS 注销ModelAndView返回重定向视图或ResponseBody返回 JSON这四个模块之间没有循环依赖数据单向流动Filter → Validation Service → Realm → Controller。你可以先 mock 掉 CAS Server用本地 HTTP Mock 工具如 WireMock模拟/serviceValidate返回固定 XML单独调试 Validation Service 解析逻辑也可以把 Realm 替换为内存 Map快速验证权限控制是否生效。2.3 技术栈版本选型依据与兼容性锚点本方案锁定以下组合非随意指定而是基于三年内多个生产项目踩坑后收敛出的稳定基线CAS ServerApereo CAS 5.3.xWAR 包部署版为什么不是 6.x6.x 默认强依赖 Spring Boot 2.3 和 Java 11而大量 SpringMVC 项目仍运行在 Java 8 Tomcat 7/8 上5.3.x 提供cas-server-webappWAR 包直接丢进 Tomcat 即可运行无构建门槛。Client 端 Shiroshiro-spring 1.4.2 shiro-cas 1.4.2注意shiro-cas 1.4.2 是最后一个原生支持 CAS 3.0 协议的版本。新版 shiro-cas 2.x 已移除对传统/serviceValidate的支持转向 CAS 5 REST API会额外增加 JSON 解析与 Token 管理复杂度对课设/毕设属于过度设计。SpringMVCspring-webmvc 4.3.30.RELEASE兼容 JDK 8关键点必须使用 4.3.x 系列。Spring 5.x 要求最低 JDK 8u101且部分注解如ControllerAdvice异常处理行为有变容易与 Shiro 的RequiresPermissions冲突。HTTP ClientApache HttpClient 4.5.13非 JDK 自带 HttpURLConnection原因CAS 校验需发送 POST 请求并携带Content-Type: application/x-www-form-urlencodedHttpClient 对编码、重定向、SSL 证书处理更鲁棒JDK 原生类在 Tomcat 8 下偶发SSLHandshakeException。这些版本不是“最旧”而是“最稳”——在某实验室部署的 12 套教学管理系统中该组合连续 26 个月无单点登录相关故障。3. 核心模块实现从 Filter 拦截到 Realm 权限映射的逐层落地这一章不贴“能跑就行”的配置而是带你手写每一处关键逻辑包括为什么这么写、参数怎么调、失败时看哪一行日志。所有代码均来自某制造企业 ERP 子系统的真实改造代码库已脱敏。3.1 CASFilter精准拦截与重定向构造非简单转发SpringMVC 中不能直接用 Shiro 的CasFilter因为其默认行为会覆盖原有DispatcherServlet的异常处理链。我们继承AbstractCasFilter重写onAccessDenied方法确保只对未认证请求重定向且保留原始请求路径用于 CAS 回调// src/main/java/com/example/filter/CasAuthenticationFilter.java public class CasAuthenticationFilter extends AbstractCasFilter { private String casServerLoginUrl; private String serverName; Override protected void onAccessDenied(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { // 1. 只对未登录且非 ticket 回调路径拦截 String uri request.getRequestURI(); if (uri.contains(/login) || uri.contains(ticket)) { super.onAccessDenied(request, response); return; } // 2. 构造 service 参数必须是当前应用的完整 URL含端口否则 CAS Server 校验失败 String serviceUrl request.getScheme() :// request.getServerName() : request.getServerPort() request.getRequestURI(); if (request.getQueryString() ! null) { serviceUrl ? request.getQueryString(); } // 3. 重定向到 CAS 登录页附带 service 参数 String redirectUrl casServerLoginUrl ?service URLEncoder.encode(serviceUrl, UTF-8); response.sendRedirect(redirectUrl); } // getter/setter 省略 }设计说明serviceUrl必须包含端口号如http://localhost:8080/app/loginCAS Server 默认校验service参数的 host:port 是否与白名单匹配若漏掉端口重定向后 CAS 会报INVALID_SERVICE错误。URLEncoder.encode(..., UTF-8)不可省略否则中文路径或特殊字符会导致 CAS Server 解析失败日志中出现Illegal character in query。此 Filter 必须在ShiroFilterFactoryBean的filterChainDefinitionMap中显式注册不能依赖shiro-cas自动注册——后者会覆盖你的自定义逻辑。3.2 CAS Validation Service健壮的票据校验与 XML 解析CAS Server 返回的是标准 XML非 JSON必须用DocumentBuilder解析而非正则或字符串截取。以下代码封装了重试、超时、SSL 信任等生产级细节// src/main/java/com/example/service/CasValidationService.java Service public class CasValidationService { private static final String CAS_VALIDATE_URL https://cas.example.com/cas/p3/serviceValidate; private final CloseableHttpClient httpClient; public CasValidationService() { // 1. 创建信任所有证书的 SSL Context开发环境用生产请配真实证书 SSLContext sslContext SSLContexts.custom() .loadTrustMaterial(null, (chain, authType) - true) .build(); // 2. 构建 HttpClient设置超时与重试 RequestConfig config RequestConfig.custom() .setConnectTimeout(3000) // 连接超时 3s .setSocketTimeout(5000) // 读取超时 5s .setConnectionRequestTimeout(3000) .build(); this.httpClient HttpClients.custom() .setSSLContext(sslContext) .setDefaultRequestConfig(config) .setRetryHandler(new DefaultHttpRequestRetryHandler(2, true)) .build(); } public Assertion validateTicket(String ticket, String service) throws Exception { // 3. 构造 POST 请求体 ListNameValuePair params Arrays.asList( new BasicNameValuePair(ticket, ticket), new BasicNameValuePair(service, service) ); UrlEncodedFormEntity entity new UrlEncodedFormEntity(params, Consts.UTF_8); HttpPost post new HttpPost(CAS_VALIDATE_URL); post.setEntity(entity); try (CloseableHttpResponse response httpClient.execute(post)) { int statusCode response.getStatusLine().getStatusCode(); if (statusCode ! HttpStatus.SC_OK) { throw new RuntimeException(CAS validation failed: statusCode); } String xmlResponse EntityUtils.toString(response.getEntity(), UTF-8); return parseCasResponse(xmlResponse); // 见下方解析方法 } } private Assertion parseCasResponse(String xml) throws Exception { DocumentBuilderFactory factory DocumentBuilderFactory.newInstance(); DocumentBuilder builder factory.newDocumentBuilder(); Document doc builder.parse(new InputSource(new StringReader(xml))); // 4. 标准 CAS XML 解析查找 cas:authenticationSuccess NodeList successNodes doc.getElementsByTagName(cas:authenticationSuccess); if (successNodes.getLength() 0) { throw new RuntimeException(CAS validation failed: no authenticationSuccess); } Element successEl (Element) successNodes.item(0); String username getTextContent(successEl, cas:user); // 5. 提取 attributes如 department, emailCAS 5.3 默认开启 MapString, Object attributes new HashMap(); NodeList attrNodes successEl.getElementsByTagName(cas:attribute); for (int i 0; i attrNodes.getLength(); i) { Element attrEl (Element) attrNodes.item(i); String name attrEl.getAttribute(name); String value getTextContent(attrEl, cas:value); attributes.put(name, value); } return new ImmutableAssertion(new SimplePrincipal(username), attributes); } private String getTextContent(Element parent, String tagName) { NodeList nodes parent.getElementsByTagName(tagName); return nodes.getLength() 0 ? nodes.item(0).getTextContent().trim() : ; } }关键参数说明setConnectTimeout(3000)避免 CAS Server 偶发延迟导致整个业务请求 hang 住。new DefaultHttpRequestRetryHandler(2, true)对网络抖动自动重试 2 次但不重试 POST因 CAS 校验是幂等操作重试安全。ImmutableAssertion是 Shiro 提供的不可变断言对象必须用它包装否则 Shiro 的CasRealm无法识别。3.3 Shiro CAS Realm权限映射的唯一可信入口这是整个方案的权限中枢。CasRealm不继承AuthorizingRealm而是直接实现AuthenticatingRealm因为它只负责认证信息转换授权逻辑由doGetAuthorizationInfo独立完成// src/main/java/com/example/realm/CasRealm.java public class CasRealm extends AuthenticatingRealm { Autowired private UserService userService; // 你的用户服务查数据库/LDAP Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { // 1. token 是 CasToken含 ticket 和 service但我们不校验——交给 Validation Service CasToken casToken (CasToken) token; String username casToken.getUsername(); // 从 CAS Server 返回的 principal // 2. 必须校验 username 非空否则 Shiro 会抛 NPE if (StringUtils.isBlank(username)) { throw new AccountException(CAS returned empty username); } // 3. 查询本地用户实体含 roles permissions User user userService.findByUsername(username); if (user null) { throw new UnknownAccountException(User username not found in local DB); } // 4. 构建 AuthenticationInfocredentials 传 null因密码不在本地校验 return new SimpleAuthenticationInfo( user, // principal null, // credentialsnull 表示不校验 getName() // realm name用于多 realm 场景 ); } Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { // 1. 获取用户名principals 里第一个就是 User 对象 User user (User) principals.getPrimaryPrincipal(); if (user null) return null; SimpleAuthorizationInfo info new SimpleAuthorizationInfo(); // 2. 加载角色如 role:admin, role:user SetString roles userService.findRolesByUserId(user.getId()); info.addRoles(roles); // 3. 加载权限字符串如 perm:order:create, perm:report:export SetString permissions userService.findPermissionsByUserId(user.getId()); info.addStringPermissions(permissions); return info; } }设计说明doGetAuthenticationInfo中credentials传null是强制约定表示“此 Realm 不参与密码校验”Shiro 会跳过credentialsMatcher。若误传密码会导致 Shiro 尝试比对而 CAS 场景下本地并无密码字段。userService.findByUsername()必须是精确查询不能模糊匹配。某次上线事故就因该方法用了LIKE %?%导致admin123用户登录后被误认为admin获得超级权限。addStringPermissions()是 Shiro 4.x 新增方法比旧版addStringPermission()更安全自动处理权限字符串中的冒号转义。4. 配置与集成SpringMVC Shiro 的 7 处关键配置点与避坑指南配置不是复制粘贴而是理解每行背后的契约。以下 7 处配置少一处轻则登录跳转失效重则权限全开所有用户都能访问/admin。4.1 web.xmlFilter 加载顺序的生死线DelegatingFilterProxy必须在CasAuthenticationFilter之前且CharacterEncodingFilter必须在最前!-- web.xml -- filter filter-nameencodingFilter/filter-name filter-classorg.springframework.web.filter.CharacterEncodingFilter/filter-class init-param param-nameencoding/param-name param-valueUTF-8/param-value /init-param /filter filter-mapping filter-nameencodingFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- CasAuthenticationFilter 必须在 ShiroFilter 之前 -- filter filter-namecasFilter/filter-name filter-classcom.example.filter.CasAuthenticationFilter/filter-class init-param param-namecasServerLoginUrl/param-name param-valuehttps://cas.example.com/cas/login/param-value /init-param init-param param-nameserverName/param-name param-valuehttp://localhost:8080/param-value /init-param /filter filter-mapping filter-namecasFilter/filter-name url-pattern/login/url-pattern /filter-mapping filter-mapping filter-namecasFilter/filter-name url-pattern/*/url-pattern /filter-mapping !-- Shiro Filter 必须在最后 -- filter filter-nameshiroFilter/filter-name filter-classorg.springframework.web.filter.DelegatingFilterProxy/filter-class init-param param-nametargetFilterLifecycle/param-name param-valuetrue/param-value /init-param /filter filter-mapping filter-nameshiroFilter/filter-name url-pattern/*/url-pattern /filter-mapping关键点CharacterEncodingFilter必须第一否则ticket参数含中文时会乱码CAS Server 解析失败。casFilter的url-pattern/*/url-pattern不能省略否则/api/data等路径无法触发重定向。DelegatingFilterProxy的targetFilterLifecycletrue必须设否则 Shiro 的FilterChainManager初始化失败所有RequiresRoles注解失效。4.2 spring-shiro.xmlShiro Bean 的 4 个强制依赖!-- spring-shiro.xml -- bean idsecurityManager classorg.apache.shiro.web.mgt.DefaultWebSecurityManager property namerealm refcasRealm/ !-- 1. SessionManager 必须指定否则 CAS 登录后 Session 不持久 -- property namesessionManager refsessionManager/ !-- 2. CacheManager 必须启用否则权限数据每次请求都查库 -- property namecacheManager refshiroCacheManager/ /bean !-- 3. SessionManager 配置禁用 Shiro 自动创建 Session -- bean idsessionManager classorg.apache.shiro.web.session.mgt.DefaultWebSessionManager property nameglobalSessionTimeout value1800000/ !-- 30分钟 -- property namedeleteInvalidSessions valuetrue/ !-- 关键CAS 场景下Session 由容器Tomcat管理Shiro 只读 -- property namesessionDAO refmemorySessionDAO/ /bean !-- 4. CacheManager必须用 EhCacheRedis 需额外依赖 -- bean idshiroCacheManager classorg.apache.shiro.cache.ehcache.EhCacheManager property namecacheManagerConfigFile valueclasspath:ehcache-shiro.xml/ /bean避坑 / 常见问题 / 排查现象登录成功后刷新页面又跳回 CAS 登录页原因sessionManager未正确配置Shiro 每次请求都新建Subject无法关联已有 Session。解决确认sessionManager的sessionDAO指向memorySessionDAOShiro 内置内存 DAO且globalSessionTimeout大于 CAS Ticket 有效期通常 10 分钟。现象RequiresPermissions(perm:xxx)不生效始终 403原因securityManager未注入cacheManager导致doGetAuthorizationInfo每次都被调用而你的userService可能返回空权限集。解决检查ehcache-shiro.xml是否存在且路径正确在CasRealm的doGetAuthorizationInfo开头加日志确认是否被高频调用。现象CAS Server 注销后Client 端仍能访问受保护资源原因未配置SingleSignOutFilterClient 未监听 CAS 的登出通知。解决在web.xml中添加filter filter-nameCAS Single Sign Out Filter/filter-name filter-classorg.jasig.cas.client.session.SingleSignOutFilter/filter-class /filter filter-mapping filter-nameCAS Single Sign Out Filter/filter-name url-pattern/*/url-pattern /filter-mapping并在spring-shiro.xml中注册singleSignOutHttpSessionListener。现象CAS 校验返回INVALID_TICKET但 ticket 明明刚拿到原因service参数 URL 编码错误或 CAS Server 白名单未包含当前域名端口。解决用浏览器开发者工具抓包对比重定向 URL 中的service参数与 CAS Server 日志中收到的service是否完全一致包括大小写、斜杠结尾。现象Shiro 的Subject.getPrincipals()返回null无法获取用户信息原因CasRealm.doGetAuthenticationInfo()返回的SimpleAuthenticationInfo构造时principal参数传了username字符串而非User实体对象。解决必须传User对象如上文代码Shiro 才能在Subject.getPrincipals().getPrimaryPrincipal()中拿到完整用户信息供 Controller 使用。5. 验收与答辩用 3 类测试用例 1 份答辩话术清单拿下高分验收不是“能跑就行”而是证明你真正理解了单点登录的契约边界。我带过的 17 个毕设/课设项目答辩高分的关键从来不是代码量而是能否用三句话说清“谁在什么时候做了什么失败了会怎样”。5.1 三类必测验收用例附预期结果与日志证据测试类型操作步骤预期结果关键日志证据位置正向登录流1. 访问http://localhost:8080/app/home2. 被重定向至https://cas.example.com/cas/login?service...3. 输入 CAS 账号密码登录4. 回调至/login?ticketST-xxx页面显示用户姓名Subject.isAuthenticated()trueSubject.getPrincipal()返回User对象CasAuthenticationFilter.onAccessDenied()日志重定向 URLCasValidationService.validateTicket()日志XML 响应CasRealm.doGetAuthenticationInfo()日志Found user: admin权限控制验证1. 以普通用户user1登录2. 访问/admin/dashboard需role:admin3. 访问/user/profile需role:user/admin/dashboard返回 403 或重定向至/unauthorized/user/profile正常显示CasRealm.doGetAuthorizationInfo()日志Loaded roles: [role:user]Shiro Filter 日志Authorization failed for user1 on /admin/dashboard单点注销验证1.user1在app1登录2. 在app2登录同一浏览器3. 访问https://cas.example.com/cas/logout4. 切回app1刷新页面app1页面跳转至 CAS 登录页Subject.getPrincipal()nullCAS Server 日志SingleLogout initiated for user1app1的SingleSignOutFilter日志Session xxx destroyed提示答辩时监考老师大概率会让你现场演示“权限控制验证”。提前准备好两个账号admin含role:admin和user1仅role:user并打开浏览器开发者工具的 Network 面板展示/admin/dashboard请求的响应状态码403和响应头X-Shiro-Authz-Status: denied比讲一百句原理都有力。5.2 答辩话术清单3 个高频问题与满分回答模板Q1为什么不用 Spring Security OAuth2而选 CAS Shiro✅ 标准回答“OAuth2 更适合第三方应用授权如微信登录而 CAS 是专为企业内网单点登录设计的协议。我们系统是封闭内网环境所有子系统都可控CAS 的 ST/PT 票据模型比 OAuth2 的 Access Token 更轻量Shiro 比 Spring Security 更易嵌入老 SpringMVC 项目无需重构 Controller 层。这不是技术落后而是场景匹配。”Q2CAS Server 如果宕机业务系统还能登录吗✅ 标准回答“不能。CAS Server 是认证中心宕机即全局无法登录。但我们的设计已规避雪崩1. 业务系统本地 Session 仍有效已登录用户不受影响2. 我们配置了 CAS Client 的failFastfalse在CasAuthenticationFilter中当 CAS Server 不可达时会降级为本地表单登录需额外开发保证核心业务可用。这是生产环境必须考虑的容灾点。”Q3如何保证 CAS 返回的用户属性如 email不被篡改✅ 标准回答“CAS Server 与 Client 之间的通信全程走 HTTPS且每次票据校验/serviceValidate都是 Client 主动发起的 Server-to-Server 请求票据本身由 CAS Server 用私钥签名Client 用公钥验证。XML 响应中的cas:user和cas:attribute都在签名范围内任何篡改都会导致 XML 解析失败或签名验证不通过。所以只要 CAS Server 私钥不泄露属性就是可信的。”5.3 交付物清单与答辩前必检项我的血泪经验你交的不是代码是一份可验证的工程交付物。答辩前我强制自己走一遍以下 checklist少一项答辩时就可能被问住✅pom.xml中shiro-cas版本明确写为1.4.2不是1.4.0或1.4.3并确认 Maven 依赖树中无冲突版本✅web.xml里CasAuthenticationFilter的casServerLoginUrl和serverName与实际部署地址一致开发用localhost答辩演示用192.168.x.x✅ehcache-shiro.xml文件存在且内容正确至少包含shiro-activeSessionCache缓存配置✅ 数据库中user_role和role_permission表有测试数据user1用户只关联role:useradmin用户关联role:admin✅CasRealm的doGetAuthorizationInfo()方法中userService.findRolesByUserId()返回的是SetString不是ListStringShiro 要求 Set从那以后我每次提交毕设代码都强制走一遍这个 checklist并用手机录屏演示完整登录→权限拦截→单点注销流程把视频放在docs/demo.mp4里。答辩老师没时间看代码但一定会点开视频看一眼——这一眼往往就是分数分水岭。希望帮到你。本文还有配套的精品资源点击获取
返回列表
PREV
查看更多资讯
NEXT
返回资讯列表