上个月接手一套遗留系统的登录模块改造权限中心的认证服务端还是 CAS 5.3。看到版本号那一刻我第一反应是头大——现在搜“CAS 配置”出来的大多是 6.x、7.x 的玩法5.3 的官方文档都已经躺进 legacy 目录了。但在企业里这种“老但稳定”的版本存量非常大特别是已经接入十几个子系统的认证中心没人敢为了追新随便动地基。这篇东西就是我改造过程的完整记录覆盖 war overlay、链接 MySQL、自定义密码加密、登录页面替换、认证校验、自定义异常映射以及很多人问的 AJAX 登录。如果你正好被 CAS 5.3 的历史包袱绊住脚或者只是想搞明白 SSO 服务端内部到底怎么流转这篇文章可以直接照着抄。CAS 5.3 最大的特点是它基于 Spring Boot 2.0 的配置体系但又保留了从 4.x 继承下来的 Spring Web Flow 登录流程。这意味着你既可以用application.properties管理数据源、服务注册这些底层配置又要跟 WebFlow 的execution参数、Thymeleaf 模板这些“上古”概念打交道。版本虽然老但它内部的分层其实一点不乱搞懂这一版之后迁移到 6.x 的成本也没有想象中高。1. 为什么还在折腾 CAS 5.3先弄清楚版本背后的配置语系1.1 5.3 的“双配置”结构和后续版本差在哪CAS 5.3 服务端的代码组织方式本质是 Maven 的 war overlay你下载官方 war 包作为依赖自己工程里的文件可以“覆盖”到 war 包里同名文件上。这跟我见过不少团队直接用cas.war丢进 Tomcat 再改内部配置文件的方式完全不同——后一种做法一旦升级版本就全丢了overlay 才能在保留自研代码的同时跟上主版本。配置层面5.3 同时存在cas.properties和application.properties。前者放cas.*前缀的 CAS 自身属性后者放 Spring Boot 层的属性比如数据源、server.port。但在实际运行时Spring Boot 配置中心会把两者一起加载所以你也可以把所有内容塞进application.properties只要注意cas.前缀属性从哪个配置文件读都行。到了 CAS 6.x官方把默认构建方式换成了 Gradle且很多属性名做了统一调整。如果你只看 6.x 的教程来操作 5.3有几个关键点会对不上比如 5.3 里 JDBC 认证的前缀是cas.authn.jdbc.query[0]6.x 已经改成带password-policy等更细粒度的结构。所以本文所有配置都以 5.3.14 为准这也是 5.3 系列最后的补丁版本。1.2 标题里六件事的依赖顺序标题列出的“链接 MySQL、自定义密码加密、登录页面、登录校验、自定义异常、AJAX 登录”其实是服务端认证能力从底层到上层的递进关系。先把数据源接好才有用户数据的来源密码加密策略不对数据库里存的哈希永远是摆设登录页面是用户直接接触的入口登录校验的结果要能区分“密码错误”“账号锁定”这些状态就需要自定义异常参与最后前端如果不想整页刷新就得提供 AJAX 友好的接口。这个顺序也对应本文的章节顺序。后面几节我不会按官方文档的目录抄而是按我在真实项目里踩坑的顺序来写先跑起来再连库再改界面再调逻辑最后处理前后端交互。2. 环境准备与 war 包 overlay让 CAS 先跑起来再谈改造2.1 JDK、Servlet 容器、构建工具的版本组合CAS 5.3 的编译目标是 JDK 8这一点必须当成硬性约束。我见过有人图省事直接用 JDK 11 启动结果汤姆猫直接抛UnsupportedClassVersionError排查半天才发现是 JDK 版本太新。所以本机至少装 JDK 8建议是 8u201 之后的版本安全性好一些。Servlet 容器方面官方支持 Tomcat 8.5/9.0或者 Jetty 9.x。我用的是 Tomcat 9.0注意 CAS 5.3 的 WebFlow 对 session 依赖很强容器切到轻量级 Jetty 也能跑但没必要在生产环境玩花活。构建工具用 Maven 3.5Gradle 虽然可以但 5.3 的官方 overlay 历史模板以 Maven 居多社区案例也多选 Maven 省心。2.2 overlay 工程的最简结构新建一个空的 Maven 工程packaging设为war然后引入 CAS 的 webapp 依赖project xmlnshttp://maven.apache.org/POM/4.0.0 modelVersion4.0.0/modelVersion groupIdcom.example/groupId artifactIdcas-overlay/artifactId version1.0.0/version packagingwar/packaging properties cas.version5.3.14/cas.version maven.compiler.source1.8/maven.compiler.source maven.compiler.target1.8/maven.compiler.target /properties dependencies dependency groupIdorg.apereo.cas/groupId artifactIdcas-server-webapp-tomcat/artifactId version${cas.version}/version typewar/type /dependency dependency groupIdmysql/groupId artifactIdmysql-connector-java/artifactId version8.0.11/version /dependency /dependencies /project构建时执行mvn clean package生成目标 war 后扔到 Tomcat 的webapps目录下。这里有个容易忽略的点war 解压后如果访问路径是/cas那所有 CAS 的请求地址都要带/cas前缀比如登录地址是http://localhost:8080/cas/login。2.3 先验证默认登录再动配置刚部署完最干净的 CAS 5.3如果你还没配置任何认证源它会提供一个内置的演示账号用户名casuser密码Mellon。访问/cas/login能看到经典蓝白登录页用这个账号能直接登录。这个默认账号存在的意义就是让你在“什么都没配”的情况下先确认环境是通的。很多初学者一上来就改数据库配置结果分不清是环境问题还是配置问题排除链路就被拉长了。我的习惯是先默认跑通再加数据源再关掉默认账号每一步都可回滚。3. 链接 MySQL数据源配置与用户表设计3.1 用户表需要什么字段CAS 的 JDBC 认证并不强制你使用它自带的 schema你完全可以拿现有的用户表。但有一个前提这张表必须能根据用户名唯一查出“密码字段”和“状态字段”。我习惯建一张这样的表CREATE DATABASE sso_db DEFAULT CHARACTER SET utf8mb4; USE sso_db; CREATE TABLE sys_user ( id BIGINT NOT NULL AUTO_INCREMENT, username VARCHAR(64) NOT NULL, password VARCHAR(128) NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0禁用, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, PRIMARY KEY (id), UNIQUE KEY uk_username (username) ) ENGINE InnoDB DEFAULT CHARSET utf8mb4;password字段的长度建议留到 128不要用 32。因为你今天可能是 MD5明天想换成带盐的 SHA-256长度不够就得改表结构这是我没少吃过的亏。3.2 application.properties 里的 JDBC 认证配置把下面的配置加到src/main/resources/application.propertiesserver.port8080 server.servlet.context-path/cas # 数据源 cas.authn.jdbc.query[0].urljdbc:mysql://127.0.0.1:3306/sso_db?useUnicodetruecharacterEncodingutf8useSSLfalseserverTimezoneAsia/Shanghai cas.authn.jdbc.query[0].usersso_user cas.authn.jdbc.query[0].passwordsso_pass cas.authn.jdbc.query[0].driverClasscom.mysql.cj.jdbc.Driver # 查询语句? 是用户名占位符 cas.authn.jdbc.query[0].sqlSELECT id, username, password, status FROM sys_user WHERE username ? AND status 1 # 返回字段映射 cas.authn.jdbc.query[0].fieldMap.usernameusername cas.authn.jdbc.query[0].fieldMap.passwordpassword cas.authn.jdbc.query[0].fieldMap.statusstatus注意这里的前缀是cas.authn.jdbc.query[0]。[0]是数据源索引你可以配置多个数据源比如一个查用户、一个查管理员。SQL 里的?是 CAS 预编译语句的位置占位符用来绑定用户输入的用户名。fieldMap的作用是把查询结果集的列名映射到 CAS 内部用户凭证的属性上如果列名和属性名一致理论上可以省略但保留出来的话排查问题会直观很多。3.3 驱动、时区、SSL 这些隐形坑MySQL 8 的连接驱动类名是com.mysql.cj.jdbc.DriverMySQL 5.x 是com.mysql.jdbc.Driver。如果你用的驱动版本和数据库服务端版本不一致最常见的是Communications link failure。另外连接串里的serverTimezoneAsia/Shanghai必须加上否则 MySQL 8 默认时区和 JVM 时区不一致CAS 初始化连接池时会直接报错。useSSLfalse在测试环境能省掉一堆证书告警。生产环境如果数据库有 SSL那另当别论。还有编码参数characterEncodingutf8建议固定写死避免数据库里存在中文用户名时登录页面显示乱码。配置完成后重启 Tomcat再用那个内置账号casuser/Mellon登录你会发现已经登不进去了。因为此时 CAS 已经不使用默认的内存用户而你的数据库里还没有任何数据。需要往sys_user插一条测试记录密码字段先随便填一个明文也可以因为下一步我们会处理密码加密。4. 自定义密码加密给 PasswordEncoder 一个清晰的接入点4.1 内置加密类型和自定义类型的区分CAS 5.3 的 JDBC 认证里密码比对最终都走 Spring Security 的PasswordEncoder接口。接口只有两个方法encode和matches。CAS 内置了几种实现可以在配置文件中用type指定cas.authn.jdbc.query[0].passwordEncoder.typeDEFAULT cas.authn.jdbc.query[0].passwordEncoder.typecom.example.MyPasswordEncoderDEFAULT表示不加密也就是明文比对STANDARD对应 Spring Security 的StandardPasswordEncoderBCRYPT对应BCryptPasswordEncoder。如果你在维护旧系统密码可能是 MD5、SHA-256甚至加了固定盐内置类型往往对不上这时就要写自己的实现类。4.2 自定义 PasswordEncoder 的完整写法假设旧系统的密码规则是“明文 MD5小写十六进制”那实现类可以长这样package com.example; import java.nio.charset.StandardCharsets; import java.security.MessageDigest; import org.springframework.security.crypto.password.PasswordEncoder; public class Md5PasswordEncoder implements PasswordEncoder { Override public String encode(CharSequence rawPassword) { return md5(rawPassword.toString()); } Override public boolean matches(CharSequence rawPassword, String encodedPassword) { if (encodedPassword null) { return false; } return md5(rawPassword.toString()).equalsIgnoreCase(encodedPassword); } private String md5(String value) { try { MessageDigest digest MessageDigest.getInstance(MD5); byte[] bytes digest.digest(value.getBytes(StandardCharsets.UTF_8)); StringBuilder builder new StringBuilder(); for (byte b : bytes) { builder.append(String.format(%02x, b)); } return builder.toString(); } catch (Exception e) { throw new IllegalStateException(MD5 error, e); } } }然后在配置里指定cas.authn.jdbc.query[0].passwordEncoder.typecom.example.Md5PasswordEncoder cas.authn.jdbc.query[0].passwordEncoder.characterEncodingUTF-8自定义类实现PasswordEncoder后会由 CAS 反射创建所以类必须有一个公共无参构造器。characterEncoding这个参数也值得写它控制 CAS 对原始密码做字节编码时用的字符集如果数据库存的是 GBK 而这里是 UTF-8那同样的密码永远匹配不上。4.3 matches 永远返回 false 的排查顺序我调试密码问题时的固定排查顺序是先确认数据库里的密文和明文密码是否真的对应同一套算法再确认自定义 Encoder 的算法细节是否转小写、是否加盐最后确认characterEncoding。这中间最常见的坑是数据库字段长度不够导致的密文截断或者密文里混着不可见字符。还有一点经验matches方法里要容忍大小写。数据库里既有可能是 32 位大写 MD5也有可能是小写返回前用equalsIgnoreCase比直接equals稳得多。如果你以后想从 MD5 平滑升级到 BCrypt可以在这个 Encoder 里做双算法兼容检测密文前缀是否带{bcrypt}再选择对应的比对逻辑。5. 登录页面替换别动 execution、别漏 _eventId5.1 模板覆盖机制CAS 5.3 的登录页由 Thymeleaf 渲染模板在 war 包内的templates/casLoginView.html。overlay 的好处在这里体现出来了在你自己的工程里创建一个同名文件src/main/resources/templates/casLoginView.html构建后就能覆盖掉默认页面。直接写整页模板也可以但更聪明的做法是复制默认页面的基础结构只改样式和文案。默认模板里除了 HTML 标签外还充斥着 Thymeleaf 的th:*属性这些属性是 WebFlow 和页面交互的关键一个都不能乱删。5.2 表单结构execution 和 _eventId 为什么不能删一个可以工作的自定义登录页表单部分至少长这样!DOCTYPE html html xmlns:thhttp://www.thymeleaf.org head meta charsetUTF-8 link relstylesheet th:href{/css/cas.css} title统一认证平台/title /head body div classlogin-container form idfm1 methodpost th:action{/login} !-- 这两个隐藏字段是 CAS WebFlow 维持会话的核心 -- input typehidden nameexecution th:value${flowExecutionKey}/ input typehidden name_eventId valuesubmit/ p用户名/p input typetext nameusername th:value${username} autocompleteoff/ p密码/p input typepassword namepassword autocompleteoff/ div th:if${status} classerror-message p th:text${status}错误信息/p /div button typesubmit登 录/button /form /div /body /htmlexecution是 Spring WebFlow 的流程执行标识相当于服务端给这个会话发的一张入场券。没有它表单提交后 CAS 会直接拒绝提示找不到 flow execution。_eventIdsubmit则表示用户在 WebFlow 里触发的是“提交登录”事件。这两个字段不用理解得多深记住“不能删”就够了。th:value${username}是 CAS 在认证失败后回填用户名的机制方便用户不用重新输入账号。但密码字段不会回填这是安全设计不是 bug。5.3 页面里的错误信息从哪里来当登录失败时CAS 会往 model 里塞一个status属性默认值是类似“Invalid credentials”的英文消息。你可以在自定义页面用th:if${status}判断并渲染。但更关键是别让用户看到一堆内部异常堆栈这就引出后面“自定义异常”这一节。静态资源方面默认的 CSS 路径是/css/cas.css你也可以在src/main/resources/static/css/cas.css里放自己的样式覆盖掉默认蓝色风格。不想走 CSS 覆盖也可以直接用内联样式但生产环境建议从前端规范考虑维护性比省事重要得多。6. 登录校验与自定义异常从“密码错误”到“账号已锁定”6.1 登录请求在 CAS 内部的流转链路用户点“登录”后请求进入 Spring WebFlow。WebFlow 会按定义好的状态机执行认证逻辑先调用注册的AuthenticationManager在这个管理器里多个AuthenticationHandler按 order 依次尝试认证。我们前面配置的 JDBC query handler只是其中一个 handler。AuthenticationHandler的职责是“拿凭证换取 Principal”。它查出用户行用PasswordEncoder.matches比密码成功就返回一个HandlerResult失败就抛出安全异常。所有 handler 都通过之后CentralAuthenticationService才会创建 TGT 票据。理解这条链之后你会发现一个关键点如果你想区分“用户不存在”“密码错误”“账号锁定”这些情况只靠普通 JDBC handler 不够。因为 JDBC query 模式默认只把“查不到用户”或“密码不匹配”统一抛成FailedLoginException前端只能看到一句笼统的“用户名或密码错误”。要精细化就要自定义 handler。6.2 自定义异常类继承 CAS 的 AuthenticationException自定义异常的作用是让不同登录失败原因有独立的 Java 类型这样页面和接口才能根据类型做差异化处理。继承 CAS 的异常基类即可package com.example; import org.apereo.cas.authentication.AuthenticationException; public class AccountLockedException extends AuthenticationException { private static final long serialVersionUID 1L; public AccountLockedException(String msg) { super(msg); } }注意 CAS 的AuthenticationException并不是 Spring Security 那个它是 CAS 自己定义的、携带 handler 执行信息的运行时异常。如果你的 IDE 里报构造器签名不匹配翻一下当前版本源码按实际构造器重写即可。6.3 自定义 Handler接管用户状态判断继承AbstractUsernamePasswordAuthenticationHandler在内部做三件事查用户、查状态、比密码。比密码这一步直接调用父类的getPasswordEncoder().matches这样能复用之前自定义的加密器。package com.example; import java.security.GeneralSecurityException; import java.util.List; import java.util.Map; import javax.security.auth.login.FailedLoginException; import org.apereo.cas.authentication.AbstractUsernamePasswordAuthenticationHandler; import org.apereo.cas.authentication.AuthenticationHandlerExecutionResult; import org.apereo.cas.authentication.credentials.UsernamePasswordCredential; import org.springframework.jdbc.core.JdbcTemplate; public class MyUserAuthenticationHandler extends AbstractUsernamePasswordAuthenticationHandler { private JdbcTemplate jdbcTemplate; public void setJdbcTemplate(JdbcTemplate jdbcTemplate) { this.jdbcTemplate jdbcTemplate; } Override protected AuthenticationHandlerExecutionResult authenticateUsernamePasswordInternal( UsernamePasswordCredential credential, String originalPassword) throws GeneralSecurityException { String sql SELECT id, password, status FROM sys_user WHERE username ?; ListMapString, Object rows jdbcTemplate.queryForList(sql, credential.getUsername()); if (rows.isEmpty()) { throw new FailedLoginException(用户不存在); } MapString, Object row rows.get(0); int status ((Number) row.get(status)).intValue(); if (status ! 1) { throw new AccountLockedException(账号已被锁定); } if (!getPasswordEncoder().matches(originalPassword, (String) row.get(password))) { throw new FailedLoginException(用户名或密码错误); } return createHandlerResult(credential, this.principalFactory.createPrincipal(credential.getUsername())); } }Handler 注册到 CAS 的认证执行计划里可以用配置类package com.example; import org.apereo.cas.authentication.AuthenticationEventExecutionPlanConfigurer; import org.apereo.cas.authentication.AuthenticationHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.jdbc.core.JdbcTemplate; import javax.sql.DataSource; Configuration(myAuthenticationConfiguration) public class MyAuthenticationConfiguration { Bean public AuthenticationHandler myUserAuthenticationHandler(DataSource dataSource) { MyUserAuthenticationHandler handler new MyUserAuthenticationHandler(); handler.setJdbcTemplate(new JdbcTemplate(dataSource)); handler.setPasswordEncoder(new Md5PasswordEncoder()); return handler; } Bean public AuthenticationEventExecutionPlanConfigurer myAuthenticationPlanConfigurer() { return plan - plan.registerAuthenticationHandler(myUserAuthenticationHandler(null)); } }这种方式比在application.properties里写cas.authn.custom.handler要容易控制得多因为 JdbcTemplate 可以直接通过 Spring 注入。如果你想在配置文件中指定自定义 handler 的类型CAS 5.3 也支持cas.authn.custom.handler[0].typecom.example.MyUserAuthenticationHandler但那样的话 handler 内部的数据源依赖不好处理bean 注册是更符合 Spring 习惯的做法。6.4 错误消息映射让页面显示人能看懂的话自定义异常只是 Java 类型要变成页面上的“账号已被锁定”还需要在消息资源文件里加 key。在src/main/resources下建一个messages_zh_CN.propertiesauthenticationFailure.AccountLockedException账号已被锁定请联系管理员 authenticationFailure.AccountExpiredException账号已经过期 authenticationFailure.AccountDisabledException账号被禁用 authenticationFailure.AccountBadCredentialsException用户名或密码错误CAS 在 WebFlow 认证失败时会根据异常类的全限定名去资源文件里找authenticationFailure.异常类名对应的文案。找不到就用默认英文。这一步经常被忽略结果就是后端改了老半天前端看到的还是那句 “Invalid credentials”。7. AJAX 登录不刷新页面拿 TGT再换 ST7.1 直接对 /login 发 AJAX 为什么不合适登录页是 Thymeleaf 渲染的表单提交默认是整页刷新。如果你用fetch直接 POST/login服务端返回的是一个完整的 HTML 页面——认证成功是重定向认证失败还是登录页根本不是 JSON。前端很难从中提取出规范的错误码更拿不到票据信息。那能不能通过解析 HTML 里的隐藏字段来模拟表单提交能但很脆弱。HTML 结构变一下你的解析逻辑就废了。更稳的做法是用 CAS 5.3 自带的 REST API或者在服务端自己写一个 JSON 接口。下面两种方式我都用过各有适用场景。7.2 官方 REST API/v1/tickets 的完整交互在 pom 里加上 REST 支持依赖dependency groupIdorg.apereo.cas/groupId artifactIdcas-server-support-rest/artifactId version${cas.version}/version /dependency重新构建部署后REST API 就挂在/v1/tickets上。创建 TGTcurl -X POST http://localhost:8080/cas/v1/tickets \ -H Content-Type: application/json \ -d {username:admin,password:123456}成功的话响应状态是201 Created响应头Location指向新建的 TGTLocation: http://localhost:8080/cas/v1/tickets/TGT-xxxxxxxxxxxxxxxx拿到 TGT 之后再用它换取某个应用的 STcurl -X POST http://localhost:8080/cas/v1/tickets/TGT-xxxxxxxxxxxxxxxx \ -H Content-Type: application/x-www-form-urlencoded \ -d servicehttp://localhost:8081/app/callback响应体就是 ST 票据例如ST-xxxxxxxx。前端拿到 ST 后拼到业务系统的地址上跳转http://localhost:8081/app/callback?ticketST-xxxxxxxx。业务系统拿这个 ticket 去 CAS 校验就能完成单点登录。前端用 fetch 实现核心代码如下async function ajaxLogin(username, password, service) { const resp await fetch(/cas/v1/tickets, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ username, password, service }) }); if (resp.status ! 201) { const err await resp.json().catch(() ({})); throw new Error(err.message || 登录失败); } const tgtUrl resp.headers.get(Location); const stResp await fetch(tgtUrl, { method: POST, headers: { Content-Type: application/x-www-form-urlencoded }, body: new URLSearchParams({ service }) }); if (!stResp.ok) { throw new Error(获取服务票据失败); } const st await stResp.text(); return ${service}?ticket${encodeURIComponent(st)}; }REST API 方式的优点是官方维护、路径稳定基本不用自己写认证代码。缺点也很明显返回格式是固定约定遇到需要返回自定义业务字段比如用户角色列表时官方接口帮不上太多忙。7.3 自定义 JSON 接口适合要严格控制返回格式的场景如果你的场景是前后端分离前端希望在同一个 JSON 里拿到code、message、tgt、用户信息那自定义 Controller 更合适。实现逻辑不复杂注入 CAS 的认证支持组件手动完成“凭证认证 发放 TGT”。package com.example; import java.util.HashMap; import java.util.Map; import org.apereo.cas.authentication.Authentication; import org.apereo.cas.authentication.AuthenticationResult; import org.apereo.cas.authentication.AuthenticationSystemSupport; import org.apereo.cas.authentication.credentials.UsernamePasswordCredential; import org.apereo.cas.ticket.TicketGrantingTicket; import org.apereo.cas.CentralAuthenticationService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.HttpStatus; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RestController; RestController public class ApiLoginController { Autowired private AuthenticationSystemSupport authenticationSystemSupport; Autowired private CentralAuthenticationService centralAuthenticationService; PostMapping(/api/auth/login) public ResponseEntityMapString, Object login(RequestBody LoginRequest req) { UsernamePasswordCredential credential new UsernamePasswordCredential(req.getUsername(), req.getPassword()); MapString, Object result new HashMap(); try { AuthenticationResult authResult authenticationSystemSupport .handleAndFinalizeSingleAuthentication(credential); Authentication authentication authResult.getAuthentication(); TicketGrantingTicket tgt centralAuthenticationService.createTicketGrantingTicket(authentication); result.put(code, 0); result.put(message, 登录成功); result.put(tgt, tgt.getId()); result.put(username, authentication.getPrincipal().getId()); return ResponseEntity.ok(result); } catch (Exception e) { result.put(code, 1001); result.put(message, e.getMessage()); return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body(result); } } public static class LoginRequest { private String username; private String password; public String getUsername() { return username; } public void setUsername(String username) { this.username username; } public String getPassword() { return password; } public void setPassword(String password) { this.password password; } } }注意这个接口的路径不能和 CAS 内部的/login冲突。放在/api/auth/login这种独立路径下最省心。接口内部抛出的异常就是你自定义的AccountLockedException、FailedLoginException等统一在前面 catch 住后转成 JSON 错误码。7.4 自定义接口模式下前端怎么处理这种模式下前端拿到的是一段纯 JSON状态码可以通过业务字段判断const resp await fetch(/cas/api/auth/login, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ username, password }) }); const data await resp.json(); if (data.code 0) { // 拿到 TGT业务端如果想走完整的 SSO 流程可以再用 TGT 换 ST } else { alert(data.message); // “账号已被锁定请联系管理员” }如果你想让自定义接口同样具备“换 ST”的能力可以再把createServiceTicket的逻辑加进去或者在前端拿到 TGT 后用第三节的方法继续调用 REST API。两种方式混合使用也很常见核心就是认证走你控制的 JSON 接口票据交互走官方路径。8. 部署后的真实排查日志开关、常见报错与处理8.1 必须打开的日志开关CAS 5.3 默认的日志级别是 INFO排查认证问题时基本不够用。建议在src/main/resources/log4j2.xml里把关键包的级别调到 DEBUGLogger nameorg.apereo.cas leveldebug additivityfalse AppenderRef refcasConsole/ AppenderRef refcasFile/ /Logger Logger nameorg.springframework.webflow leveldebug additivityfalse AppenderRef refcasConsole/ AppenderRef refcasFile/ /Logger Logger nameorg.springframework.security.web leveldebug additivityfalse AppenderRef refcasConsole/ AppenderRef refcasFile/ /Logger改完日志后重启服务再走一次登录流程重点看两个地方认证阶段是否出现HandledAuthenticationEvent或异常抛出记录WebFlow 阶段 session 是否有execution匹配失败。日志里能看到Initial flow execution url这类字样基本就能判断登录页有没有正常进入 WebFlow。8.2 常见报错和对应的处理方式现象原因处理Failed to configure a DataSource数据源参数不完整或连接失败检查 url、user、password先用数据库客户端直连测试Communications link failureMySQL 连不上端口或网络不通检查 3306 端口、防火墙、连接串的 host/portAccess denied for user xxx...数据库账号权限不足给账号授权或换一个有权限的账号登录总提示用户名或密码错误PasswordEncoder 算法和数据库密文不一致用同一个 Encoder 对已知密码手动加密比对数据库No such flow execution自定义表单把execution隐藏字段删了加回input typehidden nameexecution th:value${flowExecutionKey}/Certificate for host doesnt match访问 CAS 的协议和 service 注册协议不匹配检查 service 注册 URL 是http还是https以及是否启用了 HTTPS 强制重启后已有登录用户掉线默认 TicketRegistry 是内存存储生产环境把 TicketRegistry 配到 Redis 或数据库不要用默认内存8.3 一个值得养成的部署习惯最后说个实操层面的建议CAS 服务端的配置应该独立到外部配置文件里而不是都写在工程包内。5.3 支持通过 JVM 参数指定配置目录例如-Dcas.config/etc/cas/config或者直接把application.properties放到/etc/cas/config下。这样升级 war 包时数据库账号、密钥这些敏感配置不会跟着工程一起打包也不会被同事不经意间提交到代码仓库。另外如果 CAS 跑完登录流程之后业务系统总报“未授权服务”十有八九是 service registry 里没有配置允许的服务地址。CAS 5.3 默认只允许白名单里的 service 使用 ST测试阶段可以用 JSON service registry 快速加一条规则而不是关闭整个校验。这个点不算配置本身但在“登录校验”的完整闭环里经常成为最后一公里的堵点值得你多看一眼。