CAS统一登录三大坑:票据、集群与OIDC接入实战 前阵子有个朋友找我聊天说他们公司准备上一套统一登录平台网上搜了一圈发现大家都在聊cas、cas oauth2 oidc最后选了Apereo CAS这个开源认证平台。结果刚刚跑通demo就被一堆问题教做人了——明明单机登录没问题一加Nginx负载均衡就掉线明明是同一套账号体系APP那边却怎么接都不对最要命的是用户明明在CAS退出了业务系统却还傻乎乎地觉得他登录着。说实话这不是CAS不行而是大多数人——包括当年的我——第一反应都是去搜配置、贴代码没人先把CAS最底层那套票据模型想清楚。这篇我就拿这几年趟过的坑当引子把CAS最常见的三大问题掰开揉碎讲一遍票据与会话的坑、集群部署下的票据共享、现代应用接入时的协议代差每一条都会给到对应的解决方案和配置思路。1. 票据与会话CAS 第一个大坑的根源拆解1.1 先把 TGT、ST、TGC 这三兄弟搞清楚很多人在CAS里栽跟头不是因为配置写不对而是压根没搞明白CAS到底靠什么“认人”。我习惯把这套流程压缩成一句话业务系统不直接信任用户只信任CAS签发的票据用户不向每个系统证明自己只向CAS证明一次。具体拆开就是五步用户访问业务系统AA发现没有本地会话302跳到CAS登录页后面带上service参数告诉CAS“登录完回我这”。CAS检查浏览器有没有TGC这个Cookie。没有就出登录页有就说明这人之前登录过直接放行。用户输账号密码CAS校验通过后服务端生成一个TGT票据Ticket-Granting Ticket同时往浏览器种一个TGCCookie。这玩意儿就是“登录过”的凭证。CAS再生成一个一次性ST票据Service Ticket拼在跳转URL里把用户送回业务系统A。A拿着ST回调CAS的serviceValidate接口CAS验证通过后告诉A“这人是合法的”A这才建立自己的会话。这里面的关键点在于ST是拿一次就废的默认有效期只有10秒TGT是长期凭证存在CAS服务端TGC是TGT的钥匙存在浏览器Cookie里。大多数接入方出问题都是没分清楚这三者的生命周期导致定位问题时完全找不到方向。1.2 最常见的三个翻车现场我在不同的项目里反复见过三类问题几乎每个团队都会踩中至少一个。第一类service参数不一致导致ST校验失败。CAS的校验规则是“生成ST时用的service地址”和“业务系统回调时用的service地址”必须完全一致差一个斜杠都不行。很多系统在配置跳转地址时登录前带了一堆参数或者Nginx做了一层转发导致URL变化结果就是CAS返回INVALID_SERVICE。这种问题最坑的地方在于它只在特定浏览器、特定路径下偶现线上复现成本极高。第二类ST过期。默认10秒的有效期正常情况下够用了但如果你在CAS服务器和业务系统之间挂了WAF、做了负载均衡、或者业务系统回调之前还要走一次消息队列10秒可能真的不够。我见过最夸张的一个案例业务系统拿到ST之后不是直接调用CAS而是丢进MQ里异步处理结果MQ消费延迟等消费者去验证的时候ST早凉透了。第三类本地会话和CAS会话脱节。业务系统的session超时时间往往比CAS短用户离开一小时再回来CAS这边还认得他但业务系统已经过期了于是被踢回登录页体验非常割裂。这个问题不在CAS本身而在于接入方完全没有理解“CAS登录≠业务系统登录”两个会话需要各管各的过期时间。1.3 解决方案先把生命周期理顺再动手配参数针对上面三类问题我建议按顺序处理不要一上来就调超时参数那是本末倒置。先说service参数。所有回调地址统一收口到配置中心不要写死在代码里或者前端页面里如果条件允许CAS服务注册表里的serviceId直接用精确匹配别一上来就正则.*正则太宽容易留下安全隐患也会让排查问题变成猜谜。切身体会我见过太多团队连CAS访问日志都没开出了问题只能靠肉眼对比URL。再说ST过期。先把链路画出来确认ST从生成到被消费要经过几个环节每个环节的网络耗时有多少。确定有延迟风险再调参数CAS里对应的配置大致是# ST默认有效期单位秒 cas.ticket.st.timeToKillInSeconds30 # TGT最大存活时间默认8小时 cas.tgt.maxTimeToLiveInSeconds28800 # TGT超过多久不用就失效默认2小时 cas.tgt.timeToKillInSeconds7200注意ST不要调到太大这玩意儿是一次性票据有效期越长被截获后盗用的风险窗口就越大。我的习惯是30秒封顶再多就说明链路设计有问题。最后说会话脱节。思路很简单业务系统的会话超时时间不能比CAS短太多否则用户会被反复踢出登录。如果你希望“用户回到站点还能保持登录”那就把业务系统的session超时和CAS的timeToKillInSeconds对齐或者干脆给用户开“记住我”把CAS的TGT存活时间拉长。具体的“记住我”配置每个版本都略有差别但原理一致CAS多维持一个长期有效的TGC之后用户再回来就不需要重新输密码。1.4 跨域Cookie为什么 B 系统总是“掉登录”这个问题比前三个都更隐蔽。比如公司有oa.example.com和hr.example.com两个系统CAS部署在sso.example.com。用户在SSO登录成功TGC种在sso.example.com的域下跳回oa.example.com没问题但再跳去hr.example.com时浏览器发现目标域和Cookie域不一致要么不带Cookie要么被浏览器的SameSite策略直接拦掉。解决跨域我梳理过几条路按推荐程度排序统一二级域名。所有系统和CAS都挂在同一个主域下然后在CAS里配置TGC的Cookie Domain为主域。这是最省事、最稳定的方案。# CAS 6.x中TGC Cookie域名配置注意前面的点 cas.tgc.domain.example.com配置之后sso.example.com种下的Cookie在hr.example.com也能带上。但前提是你们公司能从域名层面做统一很多大集团内部部门各自有域名这条路就走不通。用OIDC/OAuth2协议替代纯CAS协议详见第3章。Cookie这套机制天然是为“浏览器 传统Web应用”设计的放到移动App、小程序、前后端分离项目里本来就别扭。与其在CAS的Cookie上死磕不如让这些场景直接走OIDC拿tokentoken放在Authorization Header里不依赖浏览器Cookie策略跨域问题自动消失。业务系统各自保持会话CAS只承担认证职责。这是最保守的办法不做跨域Cookie各业务系统自己管理登录态CAS只在用户第一次进入某个系统时做一次认证。用户登录A系统以后再进B系统B系统发现没会话引导用户去CASCAS认得TGC就直接放行并签发新ST。整个过程会多一次302跳转但用户无感知。说实话现在很多中大型企业的真实落地就是这种模式CAS更像一个中央认证中心而不是会话保持中心。2. 集群化部署从单机到多节点票据共享是绕不开的坎2.1 一上负载均衡就掉线问题出在“票据在哪存”单机部署CAS的时候一切安好。把它放到Nginx后面做集群立刻出现一个经典症状用户登录成功页面跳回业务系统但业务系统拿着ST去CAS校验时可能被转发到另一台节点而那个节点根本不认识这张ST。原因不复杂我在1.1里说了用户登录成功后CAS会在服务端内存里存一张TGT。默认情况下这张TGT是存在当前节点JVM内存里的节点A存的TGT节点B看不到。用户第一次访问被Nginx分到节点A种下的TGC里关联的TGT在A节点下一次访问被分到节点BB在自己的内存里找不到这个TGT只能把用户打回登录页。所以当你决定让CAS集群化部署的那一刻起就必须把票据存储从“本地内存”挪到“共享存储”。这不是可选项是必选项。2.2 TicketRegistry 选型别用数据库Redis 是生产标配CAS的票据存储抽象叫TicketRegistry它有若干种实现我直接给结论实现方案优点缺点我的评价默认内存零配置集群不可用单机调试用Hazelcast嵌入式无外部依赖节点间组播/发现配置有门槛集群维护成本高小规模集群可以试试Redis成熟、快、运维体系完善需要多管理一个Redis生产首选JPA数据库不需要额外中间件每次读写都打DB高并发下性能差不推荐除非你只有MySQL且数据量极小你可能好奇为什么不用数据库。CAS的票据存储是高频率读写的场景用户每次访问资源、每次校验ST都会触发TicketRegistry的读写操作。数据库方案在并发达到一定量级以后连接池和行锁会成为瓶颈。Redis不管从性能、可靠性还是运维生态来看都是最平衡的选择。2.3 Redis 存票据的配置以及序列化那个坑启用Redis TicketRegistry分两步。第一步在CAS Overlay项目里引入依赖Maven大致是这样Gradle同理dependency groupIdorg.apereo.cas/groupId artifactIdcas-server-support-redis-ticket-registry/artifactId version${cas.version}/version /dependency第二步在application.properties里配置连接信息并显式启用cas.ticket.registry.redis.enabledtrue cas.ticket.registry.redis.hostredis.example.com cas.ticket.registry.redis.port6379 cas.ticket.registry.redis.passwordxxxx cas.ticket.registry.redis.database0 # 连接池按并发情况调这里给个参考值 cas.ticket.registry.redis.pool.max-total20 cas.ticket.registry.redis.pool.max-idle10 cas.ticket.registry.redis.pool.min-idle2配置本身不难难的是序列化。CAS往Redis里写入的TGT/ST对象是Java对象通过Kryo或JDK序列化存进去的。这里有个很隐蔽的坑如果CAS集群中不同节点的代码版本不一致或者Redis里残留了旧版本序列化的对象反序列化时会直接报错表现就是节点A写入的票据节点B读不出来。升级CAS版本的时候最稳妥的做法是先把Redis里的CAS Ticket数据清掉再滚动发布尽量不要让新旧版本同时连接同一个Redis长时间共存。2.4 TicketRegistry 搞定了SLO 却在集群下“半残”很多团队以为票据共享解决了CAS集群就完事了。结果一测单点登出又出问题用户在CAS点退出CAS确实销毁了自己的会话也给接入系统发了LogoutRequest但有的业务系统成功登出有的没反应。原因有两层。第一层接入方没有正确实现SLO回调。CAS的登出通知走的是后台HTTP请求往接入系统配置的logoutUrl发一个SOAP格式的LogoutRequest。很多系统的logout接口放在登录拦截器后面CAS后台来请求时没有携带业务系统的登录Cookie直接被拦截器当成“未登录”踢掉了还有的系统框架自带的SLO过滤器路径配错压根收不到通知。第二层CAS集群的SLO通知是异步、尽力而为的。默认情况下CAS发出SLO通知后不会重试一旦网络抖动、请求超时、目标系统短暂不可用这次登出通知就丢了。我的处理思路是不要过度依赖CAS的SLO而是做“双重保险”接入系统把SLO回调地址加入白名单确保不经过登录拦截器。接入系统同时在本地会话里记录“用户是从CAS登录来的”CAS后台通知到达时销毁本地会话。如果追求极致体验前端在CAS登出后拿到“已退出登录”的响应后再主动跳转各业务系统的退出地址做一次兜底清理。说句大实话真正把SLO做到100%可靠的公司很少因为SLO涉及CAS、每个接入系统、网络链路三方的配合。很多团队项目排期紧最后都选择接受“业务系统会话自然过期”这个妥协方案CAS登出后最多清掉自己的会话业务系统那边靠session超时兜底。你可以评估一下自己业务的合规要求不强求的话被动过期也是可接受方案。3. 现代应用的接入代差用 OIDC/OAuth2 补齐 CAS 的短板3.1 为什么移动端、前后端分离项目都在吐槽 CAS 协议CAS协议诞生得很早它是为“浏览器访问传统网页应用”设计的核心交互是HTTP重定向、服务端Session、Cookie。放在那个年代毫无问题但放到今天团队接移动App或前后端分离项目时立刻就会发现几个尴尬点移动App不是浏览器没有“Cookie”和“302重定向”的自然概念让用户在WebView里登录再往外传Cookie既麻烦又不安全。前端SPA拿不到HttpOnly的TGC Cookie即便拿到了也叫不动serviceValidate那种表单式的校验接口。前后端分离架构里后端API通常想做无状态鉴权希望前端每次请求带一个token而不是依赖会话Cookie。于是很多团队得出一个错误结论“CAS这个东西不适合现代应用落伍了。”其实不然Apereo CAS这个开源认证平台本身就是个多协议网关它不仅懂CAS协议也原生支持OAuth2、OIDCOpenID Connect、SAML2。面对现代应用你要做的不是更换认证平台而是让CAS在继续服务老系统的同时用OIDC迎接新系统。3.2 通过热词 cas oauth2 oidc 快速定位这套组合拳怎么打我查资料时看到热词“cas oauth2 oidc”说明大家已经意识到CAS和OIDC的组合才是当前的主流玩法。简单概括OIDC在CAS里的价值它把“登录状态”从“Cookie里的票据”换成了“请求头里的ID Token / Access Token”而CAS后端依然负责核对用户身份。业务方拿token调API去校验不碰Cookie跨域、App接入、前后端分离的痛点全部绕开。要在CAS里启用OIDC配置关键的几个参数不同版本路径略有差异以你实际使用的版本为准# 打开OIDC协议支持 cas.authn.oidc.enabledtrue # 认证中心的OIDC发行者标识APP/前端要用这个地址发现配置 cas.authn.oidc.issuerhttps://cas.example.com/cas/oidc # JWKS签名密钥文件用于签发和验证JWT cas.authn.oidc.jwks-filefile:/etc/cas/config/keystore.jwks然后在服务注册表里加一个OIDC客户端核心结构类似于{ class: org.apereo.cas.services.OidcRegisteredService, clientId: your-app-client-id, clientSecret: your-app-client-secret, serviceId: https://your-app.example.com/callback, name: Your SPA / Mobile App, supportedGrantTypes: [ java.util.HashSet, [authorization_code] ], supportedResponseTypes: [ java.util.HashSet, [code] ] }SPA和移动App接入时推荐用Authorization Code PKCE流程。它的思路是App没有client secret也不要紧前端生成一个动态的code_verifier把它的哈希值code_challenge跟着授权请求一起发出去换取token时再用原始code_verifier做校验。这样就算密钥在传输过程中被截获攻击者没有code_verifier也换不到token。3.3 一套CAS同时跑两种协议新老系统共存的过渡方案接下来是落地时最关心的问题老系统用的是CAS协议新系统要OIDCCAS需要部署两套吗不需要。Apereo CAS支持在同一套服务里同时启用多种协议。老业务系统继续走/cas/login做CAS协议登录新系统走/cas/oidc/authorize做OIDC授权两者共享同一个用户认证源和同一个TGT体系。用户无论在哪个入口登录CAS都能识别。服务注册表的类型决定了协议类型传统Web系统用CasRegisteredServiceOIDC系统用OidcRegisteredService。只要在CAS里把两类服务都注册好请求进来时它会根据客户端使用的协议自动匹配对应的处理流程。我见过不少公司用这套方案逐步迁移先把新项目全部用OIDC接入老项目维持原样等老项目重构时再顺势切换到OIDC整个过程不需要动CAS核心。3.4 OIDC 接入常见翻车点issuer、JWKS、claims启用OIDC之后踩坑点会和纯CAS协议不太一样这里集中讲三个。第一issuer不匹配导致ID Token校验失败。OIDC客户端拿到ID Token后标准动作是拿issuer和配置里的issuer比对。CAS配置的cas.authn.oidc.issuer是什么写给你的客户端开发同学时就必须是什么。很多人手滑写成https://cas.example.com实际生效的是https://cas.example.com/cas/oidc客户端校验时自然通过不了。第二JWKS密钥文件缺失或权限不对。CAS启动时如果配置了jwks-file但文件不存在或无法写入要么直接启动失败要么签发token时抛异常。建议提前生成好密钥文件并确保启动CAS的操作系统用户对该文件有读写权限。如果CAS部署在容器里记得把密钥文件挂载进容器。第三claims属性映射没配好。OIDC的token里默认会带sub表示用户标识但业务系统往往还想要邮箱、姓名、部门这些信息。CAS要把用户属性映射到OIDC标准声明里才能在ID Token或者UserInfo接口中把它们带出去配置思路大致是这样的映射关系邮箱用email、昵称用name自定义属性用claims-map或属性释放策略去控制。这一块每个CAS版本的名字略有不同但核心问题都是“属性源 → 属性仓库 → 释放策略”这条链路里的映射少配了一环导致JWT里缺字段。4. 趟完坑后的选型与运维建议版本、日志、服务注册与安全4.1 版本选择别被网上那些老教程带偏搜“cas开源认证平台”相关文章时你会发现网上大量教程还停留在CAS 4.x/5.x时代。如果照着那些文章操作在新版本上大概率跑不通。以Apereo CAS为例4.x到5.x改了很多配置项5.x到6.x又是一轮大调整很多cas.properties里的属性名完全变了。我的建议是新项目直接上6.x系列的LTS版本别追最新的preview版。CAS官方更新节奏不算快LTS版本意味着有更长时间的安全补丁和维护窗口。另外强烈建议用CAS Overlay方式构建项目不要直接修改官方分发包里的源码。Overlay的方式是建立一个自己维护的定制层官方升级时只需更新基线版本你改动的自定义部分会保留下来。没有Overlay升级就是噩梦。4.2 排查问题先学会这三板斧debug日志、重定向链路、模拟请求很多人在CAS上卡住不是不会配而是面对报错不知道从哪下手。我自己有一套固定的排查顺序第一步把CAS日志开到DEBUG。CAS的日志信息量很大但不要被淹没只需要盯着用户登录那一刻的日志看。搜TGT、ST、RegisteredService这几个关键词基本能看到票据是谁签发的、被哪个服务消费了、为什么校验失败。比如ServiceTicket校验失败时日志里会明确写出expected service和actual service的对比问题一下就定位了。第二步用浏览器开发者工具看整条重定向链路。从访问业务系统开始一直看到最终回到业务系统观察每一步的302 Location。重点看两件事service参数是否前后一致以及浏览器有没有在CAS域和业务系统域之间正确传递Cookie。跨域问题、service地址漂移问题在这一步就暴露了。第三步用curl模拟一次登录流程。这个方法在对接OIDC时尤其好用。用curl访问/cas/oidc/.well-known/openid-configuration能快速确认issuer、authorization_endpoint、token_endpoint这些核心地址是否和给到客户端的配置一致。如果客户端一直报地址404多半是这里的URL路径和你配置文档里写的不一致。4.3 服务注册表JSON 文件是生产环境的默认选择CAS的服务注册可以放在内存里演示用也可以放到JSON文件、数据库或Git仓库。我习惯用JSON文件方式。配置好cas.service-registry.json.location指向一个目录后所有接入系统的注册信息就以独立JSON文件的形式放在那里配合版本控制每次改动有迹可循。这里有一个非常关键的正则坑serviceId支持正则匹配但很多人写得过于宽泛比如https://.*就把所有HTTPS网站都放行了。CAS的判断逻辑是这样的请求里带的service参数要能匹配上某个注册服务的serviceId正则才能正常走登录流程否则直接拒绝。正则写太宽容易把无关站点也放进来写太严又会导致稍微改个端口就匹配不上。我的建议是精确到域名和固定路径端口变更时专门走一次变更流程去更新注册表而不是图省事用.*。4.4 安全加固认证中心的高危清单认证中心是全局系统的“钥匙串”它的安全等级应该高于普通业务系统。以下几个点我每次部署CAS都会检查强制HTTPS。TGT/ST/TGC这些票据如果走明文HTTP传输等于把钥匙直接送给抓包的人。生产环境必须全链路HTTPS并且把TGC的Secure属性打开。登录防爆破。CAS支持配置账号锁定策略比如连续输错N次密码锁定一段时间或者结合验证码。不要觉得有验证码就万事大吉接口层仍然要做频率限制。禁用默认密码策略。如果你接的是数据库用户源CAS的JDBC认证支持配置密码编码器比如BCRYPT。千万不要把用户密码明文存在数据库里然后直接让CAS去比对。审计日志。认证中心平时没人看但出事的时候大家都会来找你。至少把登录成功、登录失败、退出、票据校验失败这四类事件完整记录下来并且保证日志文件不能被业务系统随手删掉。用户源这块我简单放一段JDBC认证的参考配置适用于从已有用户表做认证的场景密码字段经过BCrypt加密的情况cas.authn.jdbc.query[0].urljdbc:mysql://127.0.0.1:3306/user_db cas.authn.jdbc.query[0].usercas_user cas.authn.jdbc.query[0].passworddb_password cas.authn.jdbc.query[0].sqlSELECT * FROM sys_user WHERE login_name ? cas.authn.jdbc.query[0].field-passwordpassword cas.authn.jdbc.query[0].password-encoder.typeBCRYPT注意sql里的表名、字段名、占位符?这些每家都不一样但配置思路是通用的CAS按login_name查出用户记录把数据库里的password字段取出来用BCRYPT校验用户输入。4.5 关于技术选型和落地节奏最后说几句CAS这套东西说难不难说简单也不简单。它的难点从来不在“怎么把一个应用接进来”而在“怎么把认证链路的设计想清楚”。我见过太多团队一上来就分工前端接OIDC、后端走CAS协议、运维去部署集群结果联调时发现各干各的谁都不知道对方的票据长什么样。我个人在实际操作中的体会是动手之前先拉着相关同事把三张图画明白——用户登录的时序图、票据的生命周期图、集群部署的流量走向图。这三张图对齐了后面的配置问题和报错排查效率至少翻一倍。另外一个小建议CAS如果不是被历史包袱绑住新项目尽量统一走OIDC接入。虽然标题里的CAS是核心那套重定向和Cookie机制在传统Web领域非常成熟但现代应用对token、对App、对前后端分离的需求实在太普遍了靠OIDC补齐这些短板CAS才能继续安稳地做中枢。