CAS单点登录REST认证实战:票据流程、代理认证与联调避坑指南 1. 从一次接口联调失败说起REST认证到底卡在哪去年帮一个朋友排查他们内部系统的登录问题场景很典型前端是一个独立部署的单页应用后端拆成了七八个微服务统一用CAS做单点登录。浏览器里访问页面一切正常跳转、登录、回跳都跑得通。但问题出在他们新做的一个移动端H5页面这个页面不走浏览器重定向那套流程而是想直接用接口的方式拿票据、换用户信息结果怎么调都是401。这个场景其实特别常见。很多人第一次接触CAS的REST认证都会下意识觉得不就是发个HTTP请求吗然后照着文档拼几个URL就开始调最后卡在票据校验、参数编码、会话保持这些细节上。CAS的REST认证和浏览器端的重定向认证底层逻辑是两套东西前者是给非浏览器客户端移动端、桌面客户端、服务间调用、命令行工具准备的它把原本依赖Cookie和重定向的流程改造成了纯HTTP接口交互。这篇内容就围绕CAS单点登录里的REST认证展开把票据的申请、校验、代理票据、参数细节、常见报错这些讲透。适合正在做多端统一登录、或者被CAS接口联调折磨过的开发者。我会尽量把每一步为什么这么设计讲清楚而不是只丢几个URL给你。毕竟CAS这套东西光看文档很容易真正踩坑都在参数和协议细节里。2. CAS REST认证的协议底座它和浏览器流程差在哪2.1 票据是核心Cookie只是浏览器流程的附属品要理解REST认证得先把CAS的核心抽象拎出来票据Ticket。CAS整个单点登录体系本质就是围绕票据的签发、传递、校验、销毁来运转的。浏览器流程里用户被重定向到CAS服务端登录成功后CAS生成一个TGTTicket Granting Ticket这个TGT通常以Cookie形式存在浏览器里代表这个用户已经登录过了。然后CAS再签发一个STService Ticket通过URL参数回传给业务系统业务系统拿ST去CAS服务端校验校验通过就认为用户合法。REST认证做的事情是把这套流程里的重定向和Cookie这两个浏览器专属的机制拿掉换成显式的HTTP接口调用。也就是说登录不再靠表单提交加重定向而是直接POST用户名密码到CAS的票据接口CAS返回一个TGT字符串。拿ST不再靠重定向而是带着TGT去请求票据接口CAS返回ST字符串。校验ST还是老样子业务系统拿ST去校验接口换用户信息。这么一改造任何能发HTTP请求的客户端都能接入CAS不再受限于浏览器环境。这就是REST认证存在的意义。2.2 TGT和ST在REST流程里的角色分工很多人调REST认证时容易混淆TGT和ST的用途这里必须掰清楚票据类型签发时机作用范围生命周期REST流程里的角色TGT用户名密码认证成功后CAS服务端全局较长可配置代表已登录会话用来换STST携带TGT请求时签发绑定单个service极短一次性交给业务系统校验换用户身份PGT携带TGT且请求带pgtUrl时签发代理场景中等用于代理认证换PGTIOUPGTIOU与PGT配对签发代理场景极短业务系统用它换回真正的PGT关键点在于TGT是会话凭证ST是服务凭证。TGT泄露等于账号被盗ST泄露只能被用一次且只对一个service有效。所以REST认证里TGT的保管要格外小心绝不能写进前端代码或者日志里。2.3 REST认证的完整交互链路把整个流程串起来一次典型的REST认证包含这么几步客户端POST用户名密码到/cas/v1/ticketsCAS校验通过后返回201响应头Location里带着TGT的访问地址。客户端从Location里提取TGT或者直接解析出TGT字符串。客户端POST TGT加service参数到/cas/v1/tickets/{TGT}CAS返回200响应体里是ST字符串。客户端把ST作为参数请求业务系统的接口。业务系统拿ST去CAS的/cas/serviceValidate或/cas/p3/serviceValidate校验拿到用户信息。业务系统建立自己的会话后续请求不再依赖CAS。这个链路里第1步和第3步是REST认证特有的第5步是CAS通用校验。理解了这条链路后面所有参数和报错都能对号入座。3. 票据接口的实操细节从拿TGT到换ST3.1 申请TGTPOST请求里藏着的坑申请TGT的接口是POST /cas/v1/tickets请求体是表单格式包含username和password两个字段。看起来简单但坑不少。第一个坑是Content-Type。CAS服务端默认期望application/x-www-form-urlencoded如果你用JSON发很多版本的CAS会直接返回415或者400。我见过有人用Postman测试时选了JSON body怎么调都不通换成form-data或者x-www-form-urlencoded立马就好了。第二个坑是参数编码。用户名密码里如果有特殊字符比如、、必须做URL编码。尤其是密码里带的情况如果不编码服务端收到的密码里会变成空格认证必然失败。这个坑特别隐蔽因为报错信息通常只说认证失败不会告诉你密码被改了。第三个坑是HTTPS要求。CAS默认对REST接口有安全策略很多配置下只允许HTTPS访问票据接口。如果你在内网用HTTP调可能会被拒绝。这个要看服务端的cas.tgc.secure和cas.webflow相关配置测试环境可以临时放开生产环境千万别。一个正常的请求长这样curl -i -X POST \ -H Content-Type: application/x-www-form-urlencoded \ -d usernametestuserpasswordPassw0rd%2B123 \ https://cas.example.com/cas/v1/tickets成功的话返回201响应头里会有Location: https://cas.example.com/cas/v1/tickets/TGT-1-abc123xyz...这个Location就是TGT的完整访问地址最后一段就是TGT字符串。3.2 从响应头里抠出TGT别用错解析方式拿到201响应后TGT藏在Location头里。这里有个实操细节不同HTTP客户端对Location的解析行为不一样。有些客户端会自动跟随重定向把201变成200Location头就丢了。所以调这个接口时一定要关闭自动重定向跟随。比如Java的HttpClient默认是跟随重定向的需要显式设置HttpClient client HttpClient.newBuilder() .followRedirects(HttpClient.Redirect.NEVER) .build();Python的requests默认也跟随要加allow_redirectsFalseresp requests.post(url, datapayload, allow_redirectsFalse) tgt_url resp.headers.get(Location) tgt tgt_url.rsplit(/, 1)[-1]如果Location头拿不到还有一种方式是CAS在响应体里也可能返回TGT但这不是标准行为依赖具体版本。稳妥做法还是从Location里取。3.3 用TGT换STservice参数必须精确匹配拿到TGT后下一步是POST到/cas/v1/tickets/{TGT}请求体里带service参数curl -i -X POST \ -H Content-Type: application/x-www-form-urlencoded \ -d servicehttps%3A%2F%2Fapp.example.com%2Fcallback \ https://cas.example.com/cas/v1/tickets/TGT-1-abc123xyz成功返回200响应体就是ST字符串形如ST-1-xxxxx。这里的核心坑是service参数必须和业务系统注册的service完全一致。CAS对service的匹配是严格字符串比对除非配置了正则匹配协议、域名、端口、路径、末尾斜杠任何一个字符不同都会导致校验失败。我见过最典型的是业务系统注册的是https://app.example.com/callback请求时写成了https://app.example.com/callback/多了个斜杠ST校验直接失败。注册的是HTTPS请求时用了HTTP协议不匹配。端口号省略了但注册时带了:443。所以调这个接口前先去CAS服务端确认service的注册规则是精确匹配还是正则匹配。如果是精确匹配把service字符串原样复制过来别手敲。3.4 ST校验p3接口和普通接口的区别业务系统拿到ST后要去CAS校验。CAS提供两个校验接口/cas/serviceValidate返回XML格式的用户信息。/cas/p3/serviceValidate返回JSON格式字段更丰富支持属性释放。现在新版本CAS基本都推荐用p3接口因为JSON好解析而且能拿到用户属性。请求方式curl https://cas.example.com/cas/p3/serviceValidate?servicehttps%3A%2F%2Fapp.example.com%2FcallbackticketST-1-xxxxx返回的JSON里serviceResponse.authenticationSuccess.user是用户名attributes是释放的属性。如果校验失败会返回authenticationFailure里面带code和description比如INVALID_TICKET、INVALID_SERVICE。这里有个经验ST是一次性的校验一次就失效。如果你在业务系统里做了重试逻辑第一次校验成功后因为网络问题又重试了一次第二次必然失败。所以校验成功后要立刻建立本地会话别重复校验同一个ST。4. 代理认证与PGT多系统互信场景怎么玩4.1 什么时候需要代理票据普通REST认证解决的是客户端拿ST访问业务系统的问题。但有些场景更复杂业务系统A需要代表用户去访问业务系统B而B也接了CAS。这时候A不能直接拿用户的ST去访问B因为ST是绑定service的A的ST对B无效。CAS的解决方案是代理认证Proxy Authentication。A在拿ST的时候额外带一个pgtUrl参数CAS会签发一个PGTProxy Granting Ticket并通过回调把PGTIOU发给A。A用PGTIOU换回PGT之后A就能用PGT去申请针对B的PTProxy Ticket用PT访问B。这套机制在微服务架构里特别有用比如网关服务需要代表用户调用下游多个服务每个服务都要独立校验身份代理票据就是干这个的。4.2 pgtUrl回调的配置要点申请带代理的ST时请求体里加pgtUrlcurl -i -X POST \ -H Content-Type: application/x-www-form-urlencoded \ -d servicehttps%3A%2F%2Fapp.example.com%2FcallbackpgtUrlhttps%3A%2F%2Fapp.example.com%2FproxyCallback \ https://cas.example.com/cas/v1/tickets/TGT-1-abc123xyzCAS收到后会主动向pgtUrl发起一个HTTPS请求带上pgtId和pgtIou两个参数。业务系统需要提供一个回调接口接收这两个值并返回200。这里的坑集中在回调可达性和证书信任上pgtUrl必须是HTTPSCAS默认拒绝HTTP回调。CAS服务端必须能访问到pgtUrl如果业务系统和CAS之间有防火墙回调会超时。如果用的是自签证书CAS服务端需要信任该证书否则回调握手失败。我见过一个案例业务系统部署在内网CAS在另一个网段回调地址写的是内网IPCAS根本访问不到结果ST能拿到但PGT永远拿不到。排查了半天才发现是网络不通。4.3 用PGT换PT访问下游系统拿到PGT后业务系统A要访问B时用PGT去申请PTcurl -i -X POST \ -H Content-Type: application/x-www-form-urlencoded \ -d targetServicehttps%3A%2F%2Fb.example.com%2Fapi \ https://cas.example.com/cas/v1/tickets/PGT-1-xxxxx返回的响应体就是PT形如PT-1-xxxxx。A把PT作为参数传给BB拿PT去CAS校验就能拿到用户信息。注意这里参数名是targetService不是service这是代理流程和普通流程的一个区别。很多人复制粘贴时忘了改导致申请PT失败。5. 联调中最容易翻车的几个点5.1 票据过期与时钟偏移TGT和ST都有有效期ST通常很短几十秒到几分钟。如果客户端拿到ST后没有立即使用等了一会儿再校验可能就过期了。更隐蔽的是服务器时钟偏移CAS服务端和业务系统的时间如果差了几分钟票据的有效期判断就会出问题明明刚签发的ST业务系统校验时却说过期。排查这类问题先对时。生产环境所有节点都应该走NTP同步偏差控制在秒级以内。测试环境如果发现票据时好时坏八成是时钟问题。5.2 service参数编码的连环坑service参数在URL里要编码在请求体里也要编码而且编码规则要对。常见错误只编码了一次但实际需要双重编码因为service本身作为参数值又嵌在另一个参数里。用了错误的编码方式比如把空格编成而不是%20。编码后CAS解码出来的service和注册的不一致。稳妥做法是用标准库的URL编码函数别手写。Java用URLEncoder.encode(service, UTF-8)Python用urllib.parse.quote(service, safe)。注意safe表示连斜杠都编码因为service里的斜杠如果不编码可能被当成路径分隔符。5.3 401和403的区别认证失败还是授权失败联调时看到401和403要分清401 Unauthorized认证没通过通常是TGT无效、用户名密码错、票据过期。403 Forbidden认证过了但没权限通常是service未注册、代理配置不允许、IP白名单限制。CAS的REST接口在service未注册时可能返回403而不是401这个容易误导。遇到403先检查service注册和代理授权配置。5.4 日志里别打印TGT最后强调一个安全习惯TGT绝对不能出现在日志里。TGT等价于用户会话泄露了别人就能冒充用户。很多框架的HTTP日志会打印完整URL而TGT在URL路径里一不小心就写进日志了。配置日志时要把/cas/v1/tickets/路径脱敏或者干脆不记录这类请求的完整URL。6. 一套可复用的REST认证封装思路6.1 把票据流程封装成客户端实际项目里不建议每次业务代码都手写HTTP请求。封装一个CAS REST客户端把申请TGT、换ST、校验这些操作包起来对外只暴露login(username, password)和validate(ticket, service)这样的方法。封装时要注意几点TGT缓存同一个用户短时间内多次请求可以复用TGT避免频繁认证。但要注意TGT过期和登出时的清理。线程安全如果客户端是单例TGT缓存要用线程安全的Map。异常映射把CAS返回的错误码映射成业务异常方便上层处理。6.2 票据缓存的过期策略TGT缓存不能永久存要设置合理的过期时间。CAS服务端TGT的默认有效期通常是2小时客户端缓存可以设得比它短一点比如1.5小时避免用到快过期的TGT。缓存key用用户名value存TGT和过期时间。每次用之前检查是否过期过期就重新认证。用户主动登出时要调CAS的登出接口销毁TGT同时清掉本地缓存。6.3 失败重试的边界REST认证的失败分两类可重试和不可重试。网络超时、连接被拒可重试但要限制次数避免雪崩。用户名密码错、票据无效不可重试重试多少次都一样。封装时要把这两类区分开别一股脑重试。尤其是票据校验失败重试同一个ST只会一直失败因为ST一次性。7. 写在最后的一点实操体会CAS的REST认证文档看着简单真正调通需要把票据生命周期、参数编码、service匹配、代理回调这几块都吃透。我自己的经验是联调时先把CAS服务端的日志级别调到DEBUG看清楚每次请求CAS到底收到了什么参数、做了什么判断比在客户端瞎猜快得多。另外准备一个能手动发HTTP请求的工具把每一步拆开单独验证别一上来就写代码跑全流程那样出错了根本不知道是哪一步的问题。代理认证那块如果项目里暂时用不到可以先跳过但要知道它的存在。等哪天微服务之间需要互信调用回头再看PGT和PT这套机制会发现CAS早就把方案准备好了。