ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

CAS单点登录从原理到实战:TGT/ST票据交互与系统接入全解析

CAS单点登录从原理到实战:TGT/ST票据交互与系统接入全解析 做统一登录这么多年CAS这套东西我属实是又爱又恨。爱的是它老而弥坚设计思路干净撑起了无数老系统的认证半边天恨的是初次接触的人十有八九会被它那一堆 filter、service 参数和回调地址绕得晕头转向。但平心而论对于需要快速打通多个内部系统账号体系的场景它依然是那个最稳、最不容易翻车的“翻身好帮手”。这篇文章我不打算扯官方的长篇文档就按照实际落地项目的顺序把 CAS 单点登录从原理到排错掰开揉碎讲一遍。1. 先弄明白 CAS 到底在解决什么问题1.1 没有 SSO 的日子有多痛苦想象一下公司内部有 OA、ERP、Wiki、GitLab 五个系统每个系统各有一套用户表。员工每天上班要在五个页面登五次账号密码IT 部门要维护五份密码策略新同事入职要在五个系统里各建一次账号。密码忘了要找回五次改了密码要在五个地方同步。这种体验用“灾难”来形容一点不夸张。从开发者的角度看每个系统都要自己实现登录逻辑自己存 session自己管密码加密代码重复且容易出安全漏洞。更麻烦的是一旦某个系统被攻破拿到明文密码或者弱 Hash攻击者拿着同一套凭证去撞其他系统风险立刻横向蔓延。1.2 CAS 的核心思路把登录这件事抽出来单干CASCentral Authentication Service的思路非常朴素既然登录认证是所有系统都要用的公共能力那我干脆把它拆出来做成一个独立的认证中心。所有系统都不再自己验证用户名密码而是统一跳转到认证中心去登录。认证中心验证通过后发给用户一个“通行证”拿着这个通行证再去访问各个业务系统业务系统验证通行证有效就放行。这个“通行证”在 CAS 协议里叫 TGTTicket Granting Ticket存放在用户浏览器与 CAS 服务器之间的会话中通常以 Cookie 形式。而每次访问具体业务系统时临时签发的一次性凭证叫 STService Ticket有效期极短用一次就作废。整个体系用一句话概括认证中心负责发证业务系统负责验票浏览器负责存证。1.3 CAS 和 JWT、OAuth2.0 这些有什么不一样很多人会拿 CAS 和 JWT、OAuth2.0 对比我简单理一下CAS 是重定向跳转式的 SSO 方案用户浏览器全程参与适合浏览器端 B/S 架构系统的统一登录。OAuth2.0 更侧重授权目标是让用户允许第三方应用访问自己的资源典型场景是“用微信登录某个网站”侧重点在开放授权。JWT 是一种令牌格式本身不是认证方案它可以被用在 CAS、OAuth 等方案中作为 Token 的载体两者不在一个维度。如果你只是做企业内部系统的统一登录且系统都是服务端渲染的 Web 应用CAS 的成熟度和稳定性是经得起考验的。它没有 OAuth 那套授权码、Token 刷新的繁琐概念核心就是“跳转、验票、建会话”三板斧理解成本低出了问题也好排查。2. CAS 的体系架构与核心术语2.1 架构里有哪些角色用大白话描述CAS 体系里有三个角色浏览器User Agent员工本人操作的浏览器负责存 Cookie、发请求、跟随重定向。CAS Server认证中心独立的 Web 应用负责登录页展示、用户名密码校验、签发 TGT 和 ST。CAS Client业务系统各业务系统里接入的客户端组件Java 项目常用 cas-client 或 Spring Security CAS其他语言也有对应客户端负责拦截未登录请求、跳转认证中心、校验 ST。三者的交互过程表面看是浏览器地址栏一次次跳转实际干活的流程我放在第三章详细讲。2.2 核心术语TGT、TGC、ST、PGT这些缩写刚接触时容易混我整理成一张对照表方便查阅缩写全称中文俗称生命周期主要用途TGTTicket Granting Ticket票据授予票据较长默认几小时到一天证明用户已经在 CAS 登录过TGCTicket Granting Cookie票据授予 Cookie与 TGT 一致浏览器侧存 TGT 的标识关联服务端 TGTSTService Ticket服务票据极短默认几十秒到几分钟一次性凭证换取业务系统本地会话PGTProxy Granting Ticket代理授予票据较长用于 CAS 代理模式下的二次认证初学阶段你重点理解 TGT 和 ST 就足够了。TGT 存在 CAS 服务端内存或分布式缓存里TGC 是写给浏览器的 Cookie两者通过一个唯一 ID 关联。用户首次登录 CAS服务端生成 TGT同时给浏览器下发 TGC。后续访问其他业务系统时浏览器带着 TGCCAS 发现这个用户已经有有效的 TGT就不再要求输密码直接签发新的 ST。这就实现了“一次登录处处访问”。2.3 代理模式服务端之间的认证穿越还有一个高级概念叫 Proxy 模式场景是一个服务端应用需要以用户身份去访问另一个服务端应用比如门户系统需要拉取 OA 系统的待办事项。这涉及服务端到服务端的认证普通 ST 是发给浏览器端用的服务端拿不到这时需要用到 PGT 和 PTProxy Ticket。实际工作中遇到这种场景不多但真遇到了要能识别否则会卡在“为什么 A 系统调 B 系统接口拿不到用户身份”的问题上很久。3. CAS 单点登录的完整交互流程拆解3.1 首次登录浏览器引导下的三级跳假设用户从未登录过直接访问业务系统 A配置了 CAS Client完整流程是这样的浏览器请求http://app-a.example.com/mainCAS Client 拦截器发现 Session 里没有用户信息于是返回一个 302 重定向地址指向 CAS Server 的登录接口https://cas.example.com/cas/login?servicehttp%3A%2F%2Fapp-a.example.com%2Fmain注意这个service参数它表示“登录成功后我该回哪去”必须做 URL 编码。浏览器跟随重定向访问 CAS Server 的登录页输入用户名密码提交。CAS Server 校验通过后做了两件事在服务端创建 TGT并通过响应头Set-Cookie下发 TGC 给浏览器生成一个一次性 ST再次 302 重定向回业务系统地址类似http://app-a.example.com/main?ticketST-20240520-xxxxx浏览器带着 ST 访问业务系统 ACAS Client 拦截器拿到这个ticket参数后在服务端请求 CAS Server 的验证接口https://cas.example.com/cas/serviceValidate?servicehttp%3A%2F%2Fapp-a.example.com%2FmainticketST-20240520-xxxxxCAS Server 核验 ST 有效、且 service 参数与签发时一致返回一段 XML或 JSON里面包含用户名、属性等信息。CAS Client 解析后在业务系统本地创建 Session并把用户信息写入 Session。业务系统重定向去除地址栏中的ticket参数这一步很重要避免 ST 暴露在浏览器历史记录里浏览器最终看到的是干净的http://app-a.example.com/main用户处于登录状态。提示ST 是一次性的CAS Server 验证过一次之后立即失效。所以如果 CAS Client 重复去验证同一个 ticket会得到INVALID_TICKET的错误。这也是排查问题时需要留神的一个点。3.2 第二次访问其他系统静默登录的实现用户登录过 A 系统后再去访问业务系统 B流程缩短为浏览器访问业务系统 B被拦截302 跳转到 CAS Server 的/login?service...B的地址...。浏览器带着 TGC Cookie 访问 CAS ServerCAS Server 检查 TGC 对应的 TGT 是否有效未过期、未被销毁。如果有效CAS Server 不展示登录页直接生成一个新的 ST302 重定向回业务系统 B。业务系统 B 后面的流程和首次登录完全一致拿 ST 去验证、建本地 Session、去掉 ticket 参数。整个过程用户无感知浏览器从点击 B 系统的链接到看到 B 系统首页可能就一两秒钟中间地址栏快速闪几下。这就是 SSO 的“一次登录处处漫游”体验。3.3 登出如何做到“一处退出处处退出”登出是很多人容易忽略的问题。如果用户在 CAS Server 点了退出只销毁了 TGT但业务系统 A 和 B 的本地 Session 还在用户再去访问 A 系统时CAS Client 发现本地有 Session 就直接放行了造成“退出失效”的假象。CAS 的单点登出Single Log OutSLO原理是CAS Server 在销毁 TGT 之前会记录这个 TGT 曾经给哪些 service 签发过 ST这个列表叫 Registered Services然后向这些 service 的回调地址发送一个 LogoutRequest 消息SOAP 或 HTTP 方式通知各个业务系统销毁本地 Session。业务系统需要实现这个回调接口才能完成真正的单点登出。不过实话说SLO 在真实环境里属于“理想很丰满现实很骨感”的功能跨域、网络隔离、系统未实现回调接口等原因经常导致登出不彻底。我的经验是在内部系统中单点登录是刚需单点登出往往容忍度较高能保证 CAS Server 侧的 TGT 销毁再保证核心系统的本地 Session 同步销毁基本够用了。4. 动手部署一个生产可用的 CAS Server4.1 版本选型经典 Apereo CAS 6.6.x 实践目前社区活跃度最高的是 Apereo CASJava 技术栈版本迭代到现在已经到 6.6.x更高版本也有但 6.6 资料最全、案例最多适合大多数内部项目。有些人会觉得 Java 重但 CAS Server 本身是可以打成 WAR 包丢进 Tomcat 跑的运营成本并不高。如果你的环境偏好更轻量也可以找一些社区重写的 Go、Python 版本但它们对协议细节的覆盖程度参差不齐我建议生产环境还是优先用 Apereo CAS 官方版本省得在诡异的边界行为上踩坑。4.2 部署方式与核心配置Apereo CAS 6.6 官方主推的是 Overlay 方式意思是你下载一个空壳工程把自己需要的模块以依赖的方式加进去最后构建出一个自定义的 WAR 包。这种方式的好处是官方升级时你只需要切换依赖版本自己写的配置代码不受影响。构建完部署到 Tomcat第一件要改的事情是 HTTPS。CAS 安全规范要求生产环境必须走 HTTPS这是硬性要求不是建议——因为 TGT、ST 都是敏感凭证明文传输等于裸奔。内部环境如果确实没有正规证书可以用自签证书但所有客户端都要信任该 CA这又是一个工作量点。核心配置集中在application.properties里常见的关键配置项如下# 服务端基础地址所有回调都会基于这个地址拼接 cas.server.namehttps://cas.example.com cas.server.prefix${cas.server.name}/cas # 登录成功后的默认跳转地址 cas.view.default-redirect-urlhttps://app-a.example.com/index # TGT 超时时间单位秒默认 8 小时 cas.tgt.max-time-to-live-in-seconds28800 cas.tgt.time-to-kill-in-seconds7200 # ST 有效期单位秒 cas.ticket.st.time-to-kill-in-seconds60 # 允许的 service 白名单未匹配到的 service 直接拒绝 cas.service-registry.json.locationfile:/etc/cas/services/ # 认证方式这里用 JDBC 查用户表 cas.authn.jdbc.query[0].urljdbc:mysql://127.0.0.1:3306/cas_db?useUnicodetruecharacterEncodingutf8 cas.authn.jdbc.query[0].usercas_user cas.authn.jdbc.query[0].passwordyour_password cas.authn.jdbc.query[0].sqlSELECT * FROM sys_user WHERE login_name ? cas.authn.jdbc.query[0].password-encoder.typebcrypt4.3 Service 注册控制哪些系统可以接入Service 注册是 CAS Server 侧的一道闸门。新接入的业务系统必须在服务注册表里登记自己的 service 地址前缀CAS Server 才会给这个系统签发 ST。好处是防止有人恶意构造 service 参数让 CAS 往自己控制的地址发票据。在services目录下一个 JSON 文件对应一个业务系统示例{ class: org.apereo.cas.services.RegexRegisteredService, serviceId: ^http://app-a\\.example\\.com/.*, name: App A, id: 100001, evaluationOrder: 1, attributeReleasePolicy: { class: org.apereo.cas.services.ReturnMappedAttributeReleasePolicy, allowedAttributes: { class: java.util.TreeMap, uid: username, displayName: cn, email: mail } } }这里面的attributeReleasePolicy很关键。不少业务系统的本地用户表里需要邮箱、部门信息CAS Server 验证 ST 时可以返回这些属性但必须在 Service 注册表里声明允许释放哪些属性保护用户隐私。默认情况下 CAS 只会放行用户名想要更多属性需要显式配置映射。4.4 用户认证源从内存用户到数据库再到 LDAP开发环境可以先用内存用户cas.authn.accept.userscasuser::Mellon生产环境一般对接数据库或 LDAP公司统一账号体系通常是 AD 域。用 JDBC 配置时要注意密码加密方式的一致性常见的有 bcrypt、MD5不推荐但老系统常见、SHA 系列。如果老系统的密码是 MD5 存的CAS 侧需要配置 MD5 encoder但建议顺带做一个“首次登录强制改密”的过渡方案慢慢平滑迁移到 bcrypt。5. 业务系统接入 CAS 的完整实操5.1 Java 生态Spring Boot CAS 的最快姿势Java 后端接入 CAS我个人最推荐的方式是 Spring Boot 全家桶用 Spring Security CAS 单点登录模块。在pom.xml里加依赖dependency groupIdorg.springframework.security/groupId artifactIdspring-security-cas/artifactId version5.8.11/version /dependency配置核心就三件事定义 Service 地址、定义 CAS Server 地址、写一个 UserDetailsService 从 ST 返回的用户名去本地库加载权限。Configuration EnableWebSecurity public class CasSecurityConfig { Bean public ServiceProperties serviceProperties() { ServiceProperties sp new ServiceProperties(); sp.setService(http://app-a.example.com:8080/login/cas); sp.setSendRenew(false); return sp; } Bean public CasAuthenticationEntryPoint casEntryPoint(ServiceProperties sp) { CasAuthenticationEntryPoint entryPoint new CasAuthenticationEntryPoint(); entryPoint.setLoginUrl(https://cas.example.com/cas/login); entryPoint.setServiceProperties(sp); return entryPoint; } Bean public CasAuthenticationFilter casAuthenticationFilter(ServiceProperties sp, AuthenticationManager authManager) { CasAuthenticationFilter filter new CasAuthenticationFilter(); filter.setServiceProperties(sp); filter.setAuthenticationManager(authManager); filter.setFilterProcessesUrl(/login/cas); return filter; } Bean public CasAuthenticationProvider casAuthenticationProvider() { CasAuthenticationProvider provider new CasAuthenticationProvider(); provider.setServiceProperties(serviceProperties()); provider.setTicketValidator(new Cas30ServiceTicketValidator(https://cas.example.com/cas)); provider.setUserDetailsService(new CustomUserDetailsService()); provider.setKey(cas-auth-provider-key); return provider; } }要注意${server.address}是app-a.example.com:8080serviceProperties 里的地址必须和 CAS Serverservice参数的地址完全一致包括端口和路径。很多对接问题都是这个地址不一致导致的。5.2 非 Java 系统Python/Node 怎么接入如果不是 Java 生态CAS 官方没有统一客户端但社区方案成熟。Python 推荐用python-cas库Node.js 可以用cas-authentication。原理大同小异写一个中间件拦截所有请求检查 Session 里是否有用户没有则重定向到 CAS Server 的 login 地址带上当前地址作为 service捕获回调请求里的 ticket 参数用 requests 或 http 模块调用/serviceValidate验证验证通过后解析 XML/JSON 里的用户名写进 Session。Python 的代码骨架from flask import Flask, request, redirect, session from cas import CASClient app Flask(__name__) app.secret_key some-secret cas_client CASClient( version3, server_urlhttps://cas.example.com/cas, service_urlhttp://app-b.example.com:5000/login ) app.route(/login) def login(): ticket request.args.get(ticket) if not ticket: return redirect(cas_client.get_login_url()) user, attributes, pgtiou cas_client.verify_ticket(ticket) if user: session[user] user return redirect(/) return 登录失败, 401 app.before_request def check_login(): if request.endpoint login: return None if user not in session: return redirect(/login)Node.js 的cas-authentication也是类似的中间件思路。重点在于service_url参数的一致性很多联调问题都是这个地址写错。5.3 前端页面改造与登录按钮的隐藏逻辑前端关心的核心问题是什么时候显示“登录”按钮什么时候显示当前用户名。做法通常是后端在渲染模板时把当前用户信息塞进去用户已登录页面右上角显示用户名、退出按钮用户未登录显示“登录”按钮点击后跳转https://cas.example.com/cas/login?service当前页面地址前端拿到 401/302 响应时不要盲目弹提示应该触发跳转登录。如果业务系统是前后端分离架构Vue/React 后端 API需要额外注意。CAS 是基于服务端渲染的重定向流程前后端分离时有两种常见方案方案一网关层做 CAS Client浏览器访问页面时由网关完成 CAS 登录登录后把用户信息通过 HTTP Header 传给后端 API。方案二前端直接和后端约定一个/auth/cas/login接口后端返回 302 重定向地址前端用window.location.href跳转登录成功后service参数指回一个前端页面再由前端校验登录态后继续访问 API。方案二更常见但要注意前端路由的service参数不能带 hash很多 SPA 用 history 模式hash 不会被发送到服务端会被截断导致回跳地址不对。6. 生产环境里那些不得不防的坑6.1 ST 过期与时钟同步问题ST 默认有效期只有 60 秒这在现代网络环境下通常够用但有两个隐患应用服务器和 CAS 服务器的系统时间不同步会导致 ST 验证时出现“未来时间戳无效”的问题。这种问题表现很诡异验证接口报INVALID_TICKET查日志发现时间戳差了十几分钟。解决方法是统一用 NTP 同步时间。浏览器端到 CAS Server、CAS Server 到 Client 之间还有网络延迟如果 ST 有效期设得太短比如 10 秒在极端网络下也会出现过期。经验值是 60 秒到 120 秒比较稳。6.2 回调地址比对失败serviceValidate接口验证时对比的 service 参数必须是签发 ST 时使用的 service 参数完全一致一个字符都不能差。常见坑有HTTPS 和 HTTP 混杂应用通过 HTTP 访问配置里写了 HTTPS端口不一致应用实际跑在 8080service 参数写成了 80末尾斜杠差异http://app-a.com/main和http://app-a.com/main/是两个不同地址域名用了 IP 和域名混写http://127.0.0.1:8080和http://app-a.com:8080校验不过。排查时先把两个地址打印出来逐字符比对比瞎猜快得多。6.3 用户属性获取不到很多业务系统不仅需要用户名还需要邮箱、部门、手机号。如果发现拿不到属性按这个顺序排查CAS Server 的用户源数据库/LDAP/AD里有没有这些属性CAS 用户配置里有没有启用属性解析器Service 注册表的attributeReleasePolicy里有没有声明释放业务系统的 TicketValidator 是否配置了需要返回属性Java 端默认 CAS 3.0 协议会返回旧版需开启。其中第 3 点是最容易漏的我刚接触的时候在 CAS Server 配了属性却忘了在 Service 注册表里放行结果客户端一直拿不到。6.4 分布式会话下的 TGT 存储如果 CAS Server 做了集群部署TGT 不能存在单机内存里否则请求被负载均衡到另一台机器时从 TGC 找不到对应的 TGT用户被反复要求登录。解决方案使用 Redis 存储 TGT配置cas.ticket.registry.redis相关参数或使用数据库存储性能较差不推荐高频场景使用 Hazelcast/Infinispan 等分布式缓存集成复杂度略高。我见过不少小团队把 CAS Server 单机部署在虚拟机里觉得没必要做集群。但如果你的用户量超过几千人或者绑定了关键业务系统建议至少做一主一备 Redis 共享 TGT避免单点故障导致全员无法登录。6.5 回调地址泄露与 CSRF 防护CAS 登录接口没有做内置 CSRF Token这是协议设计的简化。实际项目中可以靠以下手段缓解CAS Server 侧开启cas.theme.allow-flow-customization时谨慎暴露自定义 JS防止被注入恶意代码业务系统的回调地址必须做白名单校验不能让登录接口接受任意service地址Service 注册表 URL 匹配尽量用精确的正则避免.*这种宽松匹配。7. 常见问题排查速查表现象可能原因排查方法访问业务系统无限重定向业务系统未信任 HTTPS 证书或 service 地址不匹配查看 CAS Client 日志把 service 参数打印出来比对登录页能出点登录没反应数据库连接异常、密码编码器不一致查看 CAS Server 日志中的AuthenticationException检查 JDBC 配置提示INVALID_TICKETST 已过期、重复验证、时钟不同步确认有效期检查是否重复调用验证接口NTP 同步时间提示INVALID_SERVICEservice 参数与注册表不匹配用浏览器 F12 看地址栏复制到注册表正则里测试登录成功后跳回原地址报 403业务系统本地 Session 创建失败或权限初始化失败查看业务系统日志确认 UserDetailsService 是否能从用户名加载到权限单点登出无效业务系统没实现登出回调接口检查 logout 参数或实现/logout/cas回调接口属性为空attributeReleasePolicy 未配置检查 Service 注册表 JSON 和 CAS 用户属性解析器排查问题有一个通用的建议先看 CAS Server 的日志再看 CAS Client 的日志。CAS Server 收到验证请求时会有完整的 ticket 校验过程日志能明确告诉你失败原因CAS Client 的日志则能看到它准备把用户重定向到哪里去、验证结果是什么。两端日志一对照80% 的问题都能定位。8. 安全加固与性能优化的最后几件事8.1 核心安全配置项生产环境必须检查这么几项强制 HTTPS所有 service 地址和登录地址必须是 HTTPS。ST 过期时间短一些尽量 60 秒。防止票据被窃取后长时间有效。TGC 设置 Secure HttpOnly 标志Apereo CAS 默认会设置但要确认。HttpOnly 防 XSS 读取 CookieSecure 防明文 HTTP 传输。限制 service 匹配正则不要用.*这种全匹配不然攻击者可以构造任何地址来获取 ST。启用密码策略策略模块如密码过期提醒、连续输错锁定CAS 提供cas.authn.password-policy相关配置需要启用。8.2 性能考量不要忽略一次认证的额外请求CAS 接入后每次新会话首次访问业务系统需要额外发起一次serviceValidate请求到 CAS Server。如果业务系统是短连接频繁创建会话的场景这个开销会明显放大。优化手段业务系统本地 Session 超时时间配长一点比如 8 小时避免频繁回 CAS 验证CAS Server 和业务系统之间走内网专用域名访问减少公网延迟业务系统本地做好 Session 缓存不要每次请求都去查数据库。8.3 日志监控与告警CAS Server 一定要配置好日志留存和监控告警。建议至少监控这几个指标serviceValidate成功率低于 95% 说明有大量系统对接异常TGT 创建速率突增可能意味着遭遇撞库失败认证次数连续失败超过阈值要告警登出请求数异常增多的可能是登出回调风暴。把这些指标接入现有监控体系Prometheus Grafana 或 Zabbix 都可以能大幅降低被问题时才知道系统挂了的情况。9. 经验心得总结与设计建议我在实际部署 CAS 过程中最深的一个体会是单点登录的技术难点从来不在服务端而在与各个业务系统的接入规范上。CAS Server 部署好后后面数十个业务系统接入如果每个系统对接人理解不一致就会出现各种千奇百怪的问题有的把 service 地址带上了 URL 编码导致匹配失败有的回调地址填的是本机 localhost还有的直接在浏览器地址栏手动构造 ticket 参数去访问那当然会失败。所以我强烈建议在项目初期制定一份内部对接文档明确业务系统接入的 service 地址填写规范统一 HTTPS、统一域名、明确端口回调地址匹配规则HTTP 还是 HTTPS路径是否包含末尾斜杠用户信息属性的映射字典统一属性名避免 A 系统叫emailB 系统叫mail联调步骤和常见报错处理清单。这份文档能替你省掉大量重复答疑的时间。另外如果公司内部系统复杂比如同时存在老式 JSP 系统、前后端分离新系统、第三方商业系统我建议采用分阶段的接入策略第一批接入 2-3 个核心系统验证流程和配置的标准化第二批接入重活系统解决属性映射、权限同步等细节最后再接入边缘系统有些老系统如果实在改不动可以考虑用一个轻量反向代理包一层认证代理完成 CAS 登录后把用户信息透传给老系统。回头看CAS 的设计虽然有年头了但它的核心思想——把认证从业务系统中剥离出来统一管理、统一发放凭证、统一登出——至今仍然是企业内部统一身份认证的基石。理解清楚 TGT、ST、TGC 这几个概念和一条完整的重定向链路后续无论用 Apereo CAS 还是其他类 CAS 协议的产品都会轻松很多。我现在每接到一个统一登录需求第一反应还是先想想 CAS 这套方案能不能复用因为它的稳定性和生态真的对得起“好帮手”这三个字。
返回列表