ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CAS自定义登录失败提示消息全解析:从异常链路到动态文案落地

CAS自定义登录失败提示消息全解析:从异常链路到动态文案落地 接手过CAS二次开发的朋友应该都有过这种体验需求方一句“把登录失败的提示改得更友好一点”听起来是个半小时的文案活儿结果一上手才发现从后端异常到页面上的那句话中间隔着一整套消息解析链路。改一行properties不生效、改了英文不显示中文、想按锁定次数动态提示却发现无从上手这些坑我在CAS5和CAS6两个版本里都踩过一轮。这篇就把“自定义异常提示消息”这条链路完整拆开从消息怎么从后端流到前端到两个版本的差异再到真正能落地的改法一次性说透。我在实际项目里维护过的CAS版本主要是5.3.x和6.4.x这两个版本虽然都叫CAS但底层依赖和消息机制有不少细节差别。下面的内容会以这两个版本为主线既说清楚原理也给出可以直接抄走的配置和代码。1. 一条登录失败提示从后端到页面的完整路径1.1 登录失败时后端到底发生了什么先从一个最简单的场景说起用户输错密码CAS返回“用户名或密码不正确”。这句话并不是前端写死的也不是CAS某个页面里硬编码的中文而是一连串机制跑完后的结果。用户提交认证凭据后CAS的AuthenticationManager会调用注册好的认证策略去校验账号密码。校验失败时抛出一个AuthenticationException的子类异常常见的有BadCredentialsException、AccountDisabledException、AccountLockedException这些。这些异常不会直接暴露给浏览器而是被Webflow里的AuthenticationExceptionHandler捕获并转换成一个MessageDescriptor也就是一个包含“消息键参数”的对象。接下来这个MessageDescriptor会被塞进Webflow的messageContext随着流程流转到登录页模板。Thymeleaf模板再从messageContext里取出对应的消息文本渲染成用户看到的那段提示。整条链路可以概括成四步抛异常 → 转错误码 → 查消息文本 → 渲染到页面。1.2 默认消息从哪里来MessageSource与消息键格式CAS的消息文本统一由Spring的MessageSource管理默认资源文件就是classpath下的messages.properties以及按语言区分的messages_zh_CN.properties、messages_en_US.properties这类文件。如果你用过CAS的原生登录页看到的那句英文提示就是从这里查出来的。这里有个关键点CAS不是拿异常类的全限定名去查消息而是用“简单类名”拼出消息键。比如BadCredentialsException对应的键是authenticationFailure.BadCredentialsException用户名或密码不正确前缀authenticationFailure加异常类简单类名这是CAS约定好的格式。如果你想自定义某个异常的提示只需要保证键名跟异常类名一致就能覆盖。我见过有人在这儿把键名写成了全限定类名authenticationFailure.org.apereo.cas.authentication.BadCredentialsException那自然怎么改都不生效。1.3 为什么直接搜中文文案经常搜不到很多人第一次改提示的时候直接去源码里搜“密码不正确”“账号锁定”之类的中文结果发现搜不到。原因很简单CAS的源码里默认几乎全是大白话英文只有在部署时引入了中文语言包或者自定义消息文件页面上才会显示中文。也就是说你在页面看到的中文极大概率是某个版本的messages_zh_CN.properties翻译结果或者是前人已经自定义过的消息文件。明白这一点之后遇到“页面提示是英文”或者“改了配置没反应”这类问题第一反应不应该是翻模板而是去确认当前生效的MessageSource到底加载了哪些资源文件。定位到消息源问题就解决了一半。2. CAS5和CAS6消息机制差异改配置前先分清版本2.1 配置键的前世今生CAS5和CAS6都沿用了Spring Boot的MessageSource机制但配置项的写法有过一次比较明显的风格调整。CAS5时代很多配置还保留着驼峰风格和cas.*前缀比如cas.messageBundle.baseNamesclasspath:messages到了CAS6推荐写法改成了短横线风格cas.message-bundle.base-namesclasspath:messages两个版本对这两种写法都做了relaxed binding处理严格来说混着写也能识别但项目里最好统一。我建议CAS6一律用短横线风格CAS5如果是在老项目里改造就沿用既有风格避免排查问题时还要猜配置到底有没有被读到。另外一个容易被忽略的点CAS5到CAS6之间默认消息文件的位置发生过调整但CAS依旧允许你通过cas.message-bundle.base-names手动追加自定义资源。后面第三节的配置方式在这两个版本里是一致的。2.2 异常类型和消息键的变化CAS5里常见的认证异常在CAS6里基本都还在比如AccountLockedException、AccountDisabledException、CredentialExpiredException。但CAS6新增了一些更细化的异常类型典型的是InactiveAccountException专门用来表示账号从未激活或者被强制停用。这意味着如果你从CAS5升级到CAS6自定义消息文件里可能需要补充新异常对应的键否则某些场景会兜底显示成很丑的默认错误码。建议升级时对一遍异常类清单把新版本新增的类型都补上映射。2.3 Webflow和登录页渲染的差异CAS5到CAS6登录页的渲染基础设施从Spring Webflow的老版本升级到了新版本Thymeleaf的方言处理也有变化。最主要的影响是如果你自己写过登录页模板CAS6里#fields(error)这类取错误消息的写法和CAS5在小细节上有区别模板版本要对齐否则会出现“明明后端已经塞了消息页面上就是不显示”的情况。这一块在第五节展开讲这里先记住一个结论两个版本的自定义消息文件机制是通用的但模板和异常类型要按版本单独核对。3. 最小干预改文案自定义消息包覆盖默认键值3.1 新建消息文件如果你的需求只是“把某几条提示改成指定文案”不需要动Java代码最稳妥的改法就是新增一个自定义消息资源文件然后通过配置把它追加到MessageSource的加载列表里。在etc/cas/config目录下新建custom_messages.properties内容示例authenticationFailure.AccountNotFoundException账号不存在请检查后重新输入 authenticationFailure.AccountDisabledException该账号已停用请联系管理员 authenticationFailure.AccountLockedException账号被锁定请稍后再试或联系管理员 authenticationFailure.BadCredentialsException用户名或密码不正确请重新输入 authenticationFailure.CredentialExpiredException密码已过期请修改密码后登录文件名可以随便起不一定要叫custom_messages但建议用一个一眼能看出用途的名字避免跟别人的文件混在一起。放在etc/cas/config下面CAS启动时会自动加载这个目录下的资源比塞进源码包的resources里要方便得多。3.2 修改配置文件并解释关键参数然后修改application.propertiescas.message-bundle.base-namesclasspath:custom_messages,classpath:messages cas.message-bundle.encodingUTF-8 cas.message-bundle.cache-seconds0这里有两个参数特别值得说。第一base-names里面一定不要把classpath:messages丢掉。这段配置是“追加”而不是“替换”如果你只写了classpath:custom_messagesCAS只会在自定义文件里找消息找不到的键就全部漏回默认错误码页面会变得很难看。正确的做法是把自己的文件放在最前面保留原生的messages作为兜底。第二cache-seconds0的意思是关掉消息缓存。Spring的MessageSource默认会缓存解析过的消息生产环境这样的确性能好但调试阶段你会被它坑死——改了文件不重启页面纹丝不动。调试阶段设为0确认无误后再改回默认值。3.3 验证生效的几个步骤配置改完以后重启CAS用浏览器登录一次故意输错密码看页面上显示的是不是新文案。如果还是旧消息按顺序排查三件事确认custom_messages.properties是否真的被打进了运行时classpath注意看启动日志里有没有加载这个文件的记录。确认消息键的异常类名写没写对。拿不准的时候先看堆栈日志里抛出的到底是哪个异常类再去改对应的键。确认编码是不是UTF-8。properties文件默认是ISO-8859-1如果你用IDEA直接写中文一定要检查右下角文件编码否则中文会被读成乱码。这一步的改法是最小侵入的适合不用改逻辑、只需要调整展示文案的场景。但它的局限也很明显所有同类型异常只能显示同一条固定提示做不到“还剩几次尝试机会”“锁定到几点几分”这种动态内容。要做动态提示就得进入下一节。4. 精细化异常分类让不同错误提示真正说出来4.1 常见AuthenticationException类型对应表在做动态提示之前先盘点一下最常用的异常类型和它们适合的提示语义。我把CAS5/6里经常碰到的列成一个表方便你对照着设计消息键异常类语义建议提示方向BadCredentialsException账号密码不匹配用户名或密码错误AccountNotFoundException账号不存在账号不存在或未注册AccountDisabledException账号被禁用联系管理员AccountLockedException账号被锁定多次失败或管理员锁定锁定原因与解锁时间CredentialExpiredException密码过期引导修改密码InactiveAccountExceptionCAS6新增账号未激活激活引导有了这张表你就能按业务场景去设计文案而不是把所有失败都笼统地显示成“登录失败”。4.2 用自定义AuthenticationExceptionHandler实现动态消息固定文案能满足80%的需求但剩下20%的场景——比如“账号被锁定请在15分钟后再试”里的15分钟——必须由代码把动态参数塞进消息里。CAS的Webflow中负责异常转换的处理器是AuthenticationExceptionHandler这个接口。默认实现会按异常类型拼消息键我们可以覆写一个自己的实现来改变行为。核心逻辑是捕获到异常之后判断类型从异常对象里取出动态数据构造带参数的MessageDescriptor。下面这个示例基于CAS5和CAS6通用的接口package com.example.cas.handler; import org.apereo.cas.authentication.AuthenticationException; import org.apereo.cas.authentication.exceptions.AccountLockedException; import org.apereo.cas.web.flow.AuthenticationExceptionHandler; import org.apereo.cas.authentication.MessageDescriptor; import org.springframework.webflow.execution.RequestContext; public class CustomAuthenticationExceptionHandler implements AuthenticationExceptionHandler { Override public MessageDescriptor handle(final AuthenticationException e, final RequestContext requestContext) { if (e instanceof AccountLockedException) { AccountLockedException le (AccountLockedException) e; Object[] params new Object[]{ le.getCode(), // 这里可以塞入从异常或缓存中取出的动态参数 15 }; return new MessageDescriptor(custom.account.locked.detail, params); } if (e.getHandlerErrors() ! null !e.getHandlerErrors().isEmpty()) { String firstError e.getHandlerErrors().iterator().next(); return new MessageDescriptor(authenticationFailure. firstError); } return new MessageDescriptor(authenticationFailure.genericError); } }然后在配置类里注册这个Bean覆盖掉默认的authenticationExceptionHandlerBean public AuthenticationExceptionHandler authenticationExceptionHandler() { return new CustomAuthenticationExceptionHandler(); }消息文件里加上对应的键custom.account.locked.detail您的账号因多次尝试失败已被锁定请在{0}分钟后重新登录这样页面上展示的就是带动态参数的文案了。这里{0}占位符对应的是MessageDescriptor构造器传入的params数组参数顺序跟占位符一一匹配。4.3 进阶结合缓存实现次数统计动态消息不只可以来自异常对象自带的属性还能来自你自己维护的业务数据。比如记录同一IP或同一账号的失败次数达到阈值就锁定并在提示里告诉用户当前失败了几次。我常用的做法是在自定义handler里注入一个Redis或本地缓存客户端在抛出AccountLockedException之前查询失败计数然后组合进消息参数里。由于CAS的认证异常本身不携带业务数据这类信息只能靠外部存储传递所以第一步一定是在认证成功或失败的环节把计数维护好。需要提醒的是自定义AuthenticationExceptionHandler会完全接管默认行为如果新实现的返回逻辑有遗漏某些异常会得不到合适的消息键最终显示成原始错误码。因此建议在不完全清楚异常树的前提下保留一个向默认格式的fallback也就是上面示例里的authenticationFailure.xxx回退逻辑。5. 当默认登录页不吃这套前端渲染与消息键的配合5.1 Webflow中的messageScope与错误渲染后端把MessageDescriptor塞进RequestContext之后前端模板能不能显示出来取决于模板里怎么取消息。默认的casLoginView.html会在错误区域遍历messageContext中的错误消息。如果你用的是CAS自带登录页这一环基本不用操心。但很多项目会为了品牌定制重写整个登录页。这时候如果只是把表单样式改了错误消息渲染逻辑没带上就会出现“后端报错了页面却什么都不显示”的经典问题。5.2 自定义Thymeleaf模板时如何读取消息在自定义模板里最简单的做法是保留CAS默认的错误渲染片段。Thymeleaf模板中可以通过以下方式输出认证失败消息p th:if${#fields.hasErrors(error)} classlogin-error span th:eacherr : ${#fields.errors(error)} th:text${err}/span /p这里的error是Webflow中消息字段的固定名称CAS的流程会统一把认证错误绑定到这个字段上。你的自定义页面只要包含这段逻辑就能把后端通过MessageDescriptor传出的文案渲染出来。如果你在某个版本的模板里看到的是#fields.detailedErrors()也不要慌本质上它读取的还是同一个messageContext只是在Thymeleaf方言升级后API叫法略有不同。5.3 纯前端/JSON接口场景下的消息返回还有一种常见场景是业务系统对接CAS时并不跳转浏览器而是通过AJAX方式调用认证接口需要拿到JSON格式的报错信息。CAS原生登录流程主要面向浏览器跳转如果一定要走JSON需要自己提供一个REST接口来做认证并捕获异常。这种情况下自定义异常处理器的返回值就不能直接用了因为它绑定Webflow的RequestContext。建议单独写一个Controller内部调用认证管理器捕获AuthenticationException后自己解析异常类型然后返回统一结构{ code: ACCOUNT_LOCKED, message: 账号被锁定请在15分钟后再试, remainingMinutes: 15 }消息文本同样走MessageSource解析复用之前自定义的消息键这样前端拿到的文案和后端页面保持一套标准。6. 改提示消息过程中我踩过的六个实际问题6.1 改了文件不生效居然是缓存和路径双重问题第一次改消息时我把属性写进了src/main/resources/custom_messages.properties配置也加了base-names重启后页面纹丝不动。排查了半天发现Maven构建的时候根本没有把etc/cas/config下的文件拷贝到classpath我改的源码resources文件也没被增量构建同步。这事儿的教训是自定义消息文件放etc/cas/config后要确认启动脚本的classpath确实包含了这个目录如果是本地IDE调试还要注意target目录里的旧文件残留。调试期把cache-seconds0打开可以省掉反复重启的麻烦。6.2 中文乱码的根源不只是properties编码properties文件默认编码是ISO-8859-1所以直接往里面写中文重启后八成是乱码。用IDEA的话在Settings里打开Transparent native-to-ascii conversion编辑器里正常写中文保存时自动转成Unicode转义这是最省心的方案。如果项目里有多个环境还容易踩另一个坑开发机的properties是UTF-8部署到Linux后因为系统默认编码不同读出来的中文变成问号。解决方式是在cas.message-bundle.encoding里显式指定UTF-8不要让Spring去猜平台编码。6.3 键名覆盖陷阱别把异常类型名写错CAS消息键依赖异常类的简单类名一旦拼错就不匹配。常见错误包括把BadCredentialsException写成BadCredentials、把CredentialExpiredException和AccountExpiredException混为一谈。最简单可靠的办法是在认证失败日志里找到完整堆栈再对照异常类名去写消息键不要凭记忆拼。6.4 CAS5升CAS6时HandlerExceptionResolver的注册变化CAS5时代有人会把自定义的Spring MVCHandlerExceptionResolver注册成Bean来统一处理认证异常。这个做法在单个版本里没问题但升级到CAS6之后由于Spring Boot自动配置和Webflow初始化顺序有变化自定义的Resolver可能不再被优先加载导致提示又变回默认文案。解决办法是使用CAS官方提供的AuthenticationExceptionHandler扩展点而不是自己去抢Spring MVC的异常处理链条。我见过太多项目在升级时栽在这个点上的排查到最后往往要花一两天。6.5 自定义登录页不渲染错误消息这个坑主要在项目初期接入CAS时最容易出现设计稿要自定义登录页前端把模板整个重写了忘掉渲染错误消息的代码结果登录失败时页面安静如鸡一点提示都没有。后来把#fields(error)那段加上才算解决。如果你是完全从零写登录页务必记得Windows表单登录流程对错误消息字段名是有约定的保持error这个字段名可以少踩很多坑。6.6 多语言环境下的缺失键问题某些环境只配置了messages_zh_CN.properties没有完整的默认语言包。这时候如果业务系统浏览器的Locale是英文CAS会在找不到英文消息时回退到默认messages.properties。一旦默认包缺了某个键页面上就会直接展示消息键本身比如authenticationFailure.AccountLockedException。这类问题隐蔽性极高因为中文环境下怎么测都正常。建议在所有语言包里做一次键集合比对或者至少保证messages.properties里包含所有被自定义语言包覆盖的键。从我摸过的这些项目来看自定义CAS异常提示消息这件事难的不是写代码而是把整条链路看清楚异常的抛出位置、消息键的拼写规则、MessageSource的加载顺序、Webflow的消息传递方式、前端模板的渲染逻辑这五环缺一环都会出幺蛾子。如果你想做的只是替换几段固定文案第三节的配置方案最省力如果业务上有锁定次数、剩余时间这类动态提示诉求那就沉下心把第四节的handler扩展点吃透它会成为你后续做CAS二次开发时非常趁手的一个工具。我个人的习惯是在交付时额外维护一份消息键清单文档把自定义的键名、对应异常、参数占位符的作用都列出来并且把所有语言包同步更新。别小看这个动作等到半年后需求方回来说“能不能把提示再加个感叹号”你翻文档定位到那一行配置三分钟改完收工而不是又花一个下午重新解密自己当初写下的代码。
返回列表