
我第一次接触CAS是在一个校园信息化项目里。学生、老师、OA、教务、图书馆七八个系统各管各的账号用户每天要记五六套密码光找回密码的工单就能堆满一个客服组。后来决定统一接入CAS单点登录登录一次全部打通那会儿我还只是个调接口的小角色也正是因为这个项目我啃下了单点登录这一整套东西。说它是翻身路上的好帮手一点不夸张。CASCentral Authentication Service中央认证服务是一套开源的单点登录协议也是它的参考实现。这些年无论是在校园网、企业内部系统还是在需要统一身份认证的项目里出场率都极高。你在浏览器里看到类似cas/login?servicexxx这种地址基本可以判断后端接的就是CAS这套东西。搞懂它不光是多会一个框架而是真正理解了一次登录到处通行这整套认证体系的设计思路这对做后端、做架构、做信息化集成的朋友来说都是实打实能写进项目履历的技能。这篇东西我不会给你念官方文档就按我从部署到对接踩过来的一条线把这套系统怎么跑起来、怎么和业务系统打通、出了问题怎么查完整捋一遍。适合正在做统一认证项目、或者想往架构方向走的开发者。1. 为什么说CAS是翻身路上的好帮手1.1 一个让我入坑的真实场景很多项目做大之后都会遇到同一个尴尬系统越来越多账号体系却各玩各的。今天在A系统改密码明天B系统还是旧密码这个月新入职的同事要挨个系统开通账号稍微大一点的组织光账号管理就能吃掉一个运维的人力。我遇到的那个校园项目就是典型的这类情况。图书馆系统、教务系统、OA系统、邮件系统每个系统都有自己的用户表有的还存在不同的目录服务里。领导一拍板要搞统一身份认证大家就开始在选型会上争论是自研一套还是用现成的当时有人提议直接用Spring Boot写个简单的登录中心后来被否决了——看起来简单实际上要考虑会话保持、票据安全、和异构系统对接的协议兼容性短时间内根本搞不定。最后敲定用CAS理由很务实第一它是Apereo基金会维护的开源项目有十多年历史生态成熟第二它本身就是一套完整协议不仅仅是代码各语言的客户端SDK都有第三它的部署方式相对独立不绑架现有系统的技术栈Java、PHP、Python、.NET都能接。1.2 单点登录到底解决了什么痛点先说单点登录这四个字。翻译成人话就是你在一个系统里登录成功之后再访问其他已经接入的系统不需要重新输账号密码。这个体验听起来很普通但背后的技术难点是一大堆。核心难点在于浏览器访问每个业务系统默认只能携带这个系统自己域名下的Cookie系统A没法直接告诉系统B这个人已经登录过了。要打通这个信任关系就需要一个所有系统都信任的第三方——认证中心。CAS的角色就是这个公认的裁判所有系统都不再自己验证账号密码而是把验证这件事统一交给CAS Server然后CAS再用一套凭证告诉各个业务系统这个人确实登录过身份信息是什么。这套机制比很多团队自己写的共享Session要靠谱得多。共享Session的问题是所有业务系统要么共享存储要么共享密钥安全边界很容易被突破而CAS用的是票据机制业务系统之间并不直接共享敏感数据每个系统只跟CAS Server打交道信任关系可控、可审计。1.3 什么情况下才值得用CAS我见过不少团队把CAS当成万能药不管什么项目都要硬上。这里我必须泼一盆冷水CAS适合的是多个独立业务系统、需要统一认证入口的场景。如果你只是一个单体应用或者一个前后端分离的小项目自己做好登录态就行完全没必要引入CAS。但如果你遇到下面这几种情况CAS就非常合适已经存在或规划了多个业务系统各自独立部署、独立开发甚至技术栈都不一样。需要一个统一的账号管理体系希望用户在登录一次之后跨系统访问不再重复认证。业务上有合规或审计要求需要记录谁在什么时间访问了哪个系统。已经有LDAP、AD或数据库用户体系希望接入方不用重复建设账号。反过来如果只是内部两三个小工具自己拼一个登录跳转也能凑合。判断标准很简单接入的系统会不会持续增加会不会有外部系统需要接入如果答案是会那CAS的投入就值得。2. CAS单点登录的核心原理我尽量讲得通俗一点2.1 票据体系TGT、TGC、ST到底是什么CAS最核心的设计不是登录页长什么样而是一整套票据体系。简单说就是CAS Server在背后不断给浏览器和业务系统发凭证这些凭证各有各的用途和生命周期。TGTTicket Granting Ticket登录成功后CAS Server在服务端创建的一张总通行证代表这个用户已经通过了认证。用户后续访问任何接入系统都是靠它来快速签发访问凭证。TGCTicket Granting CookieTGT存在服务器端但它需要一个和浏览器对应的标识这个标识就是TGC。它一般存在CAS Server域名下的Cookie里浏览器每次访问CAS Server都会带上它。STService Ticket用户要访问某个业务系统时CAS Server临时签发的一次性票据。这个票据是发给业务系统的业务系统拿到后要去CAS Server验真验真通过才算登录成功。把这套东西类比成逛游乐场就很好懂TGT是你入园时买的手环证明你今天可以玩所有项目TGC就是手环上贴的标签工作人员一眼能认出你有没有手环ST则是你走到某个具体项目门口时工作人员临时给你的一个游玩凭证项目入口用这个凭证去总台核实核实无误才放你进去。关键点在于ST是一次性的用完就作废防止被重放攻击。这个设计我看过很多自研单点登录的团队没做好导致票据被反复使用安全上出了不少问题。2.2 完整的登录流程从跳转到回调搞清楚票据后整个流程就串起来了。我用两个业务系统A和B举例走一遍完整的登录流程浏览器访问业务系统AA发现会话里没有登录信息于是重定向到CAS Server的登录地址并附带一个service参数参数值就是A自己的一个回调地址。浏览器到了CAS登录页用户输入账号密码提交。CAS Server验证账号密码成功后在服务端创建TGT并通过响应头在浏览器写入TGCCAS域下的Cookie。然后CAS Server带着一个ticketST-xxx的参数把浏览器重定向回业务系统A的service回调地址。业务系统A从请求中拿到ST在服务端向CAS Server发起校验调用 /serviceValidate 或 /p3/serviceValidate 接口把ST交回去。CAS Server验证ST合法后返回该用户的用户名和其他属性信息。业务系统A拿到这些信息在自己系统里建立本地会话相当于发一个自己的Cookie给浏览器。到这一步用户和A系统已经登录完成。接着用户访问业务系统B会走一遍近乎一样的流程但第3步会有差别浏览器带着TGC去CAS ServerCAS Server发现这个TGC对应的TGT还在有效期内就不再要求用户输入账号密码直接生成一个新的ST跳转回B。B再拿ST去验真登录完成。整个过程用户没有任何感知。这整个过程解释了为什么单点登录能一次登录处处通行因为TGT/TGC在CAS域下是共享的而各个业务系统只需要信任CAS Server校验ST的结果。2.3 一张表看懂CAS与OAuth2、JWT的区别新手最容易把CAS和OAuth2、JWT搞混。我在面试候选人的时候经常有人把这几个概念混在一起说。它们确实都是认证/授权领域的方案但解决的问题层级不一样。方案主要解决什么问题典型特点常见场景CAS多个业务系统的统一登录SSO重定向跳转、票据ST一次性校验企业/校园统一身份认证OAuth2授权第三方应用访问用户资源授权码、Token、权限范围微信登录、GitHub登录JWT无状态地传递用户身份信息自包含、可签名、服务端不存会话前后端分离的API认证简单说CAS的侧重点是登录打通OAuth2的侧重点是授权开放JWT则是一种Token封装格式。实际项目里有不少是组合使用用CAS做企业内部统一认证对外提供API时再用OAuth2或JWT保护资源。搞明白这一点就不至于在方案选型时眉毛胡子一把抓。3. 从零部署一套CAS Server踩坑全记录3.1 环境准备与项目骨架选择CAS Server本身是一个Java Web应用官方推荐用Overlay方式部署——就是在一个空的工程里覆盖官方配置好处是升级时不会冲突自己的定制代码不会散落得到处都是。我用的版本是CAS 6.6.x基于Spring Boot。环境准备如下JDK 11CAS 6.x要求JDK 8跑不起来Maven 3.6Tomcat 9也可以用内置的Spring Boot容器MySQL 5.7生产环境用数据库存储用户和票据Redis可选多节点部署时做票据共享先拉取Overlay工程git clone https://github.com/apereo/cas-overlay-template.git cd cas-overlay-template git checkout 6.6这个工程本质是一个空壳子真正的业务代码都不在里面只是帮你把CAS Server的依赖和构建流程搭好。构建时用Maven把它打成WAR包直接扔到Tomcat里就能跑。有一点我要重点提醒CAS 4.2之后生产环境默认强制要求HTTPS。这一度劝退了很多第一次用的人。不是官方故意刁难而是票据在HTTP明文传输下容易被截获重放安全没保障。自己测试的时候可以临时关闭这个限制但生产环境我建议老老实实上HTTPS后面我会再讲怎么配。3.2 核心配置从单机到数据库认证CAS Server启动后的自带管理页面不算复杂但配置项非常多。我最常用的核心配置都集中在application.properties里。先说最基础的单机配置# CAS服务地址必须和实际访问地址一致 cas.server.namehttps://sso.example.com cas.server.prefix${cas.server.name}/cas # 开启登录成功后的JSON服务注册管理 cas.service-registry.core.init-from-jsontrue cas.service-registry.json.locationfile:/etc/cas/services # 登录页和Cookie相关配置 cas.tgc.securetrue cas.tgc.secure-requestfalse第一次跑的时候建议先用内存中的账号测试把整条链路跑通再换数据库。可以用CAS自带的casuser/Mellon这个默认账号来快速验证。等流程通了再把认证切到数据库。数据库认证的配置是生产项目里最常用的方式。我以MySQL为例# 数据源配置 cas.authn.jdbc.query[0].urljdbc:mysql://localhost:3306/cas?useUnicodetruecharacterEncodingUTF-8 cas.authn.jdbc.query[0].usercas_user cas.authn.jdbc.query[0].passwordyour_password cas.authn.jdbc.query[0].driver-classorg.mariadb.jdbc.Driver # 用户表查询SQL这里要求结果必须包含password字段 cas.authn.jdbc.query[0].sqlSELECT * FROM sys_user WHERE login_name? cas.authn.jdbc.query[0].field-passwordpassword # 数据库里存的密码是BCrypt加密后的值CAS默认用BCrypt校验 cas.authn.jdbc.query[0].password-encoder.typeBCRYPT这里的原理是CAS在登录时用用户提交的用户名执行这条SQL取出数据库里对应的密码密文再和用户输入的密码做比对。如果密码明文入库会有一堆安全问题日常工作我强烈建议密码一定要做哈希加密直接用BCrypt就行Spring Security自带的工具就能生成。3.3 指定服务白名单让客户端可以对接CAS Server默认不允许任意业务系统都来对接必须先在服务注册表里登记。这个设计不少人第一次没搞懂以至于本地调试时明明跳转逻辑都对了CAS却报Unauthorized Service错误。服务注册表最简单的形式是JSON文件。我先建一个文件放业务系统A的配置{ class: org.apereo.cas.services.RegexRegisteredService, serviceId: ^https://app1\\.example\\.com/.*, name: 应用A, id: 1, evaluationOrder: 1 }class字段是CAS反序列化时需要的固定类名一般照抄。serviceId是正则表达式用来匹配业务系统传来的回调地址。注意这里的正则最容易写错。实际项目里我见过有人直接把https://app1.example.com写上去结果CAS严格要求整个URL匹配才放行回调地址带了路径就直接报错。evaluationOrder是匹配优先级多个服务时按顺序去匹配。如果有很多个系统要接入用管理界面一个个登记也行但JSON文件方式在初始化脚本化部署时特别好维护git一存就能审计变更记录。这里有个容易被忽略的坑服务注册表的类型、路径等配置一旦调整CAS需要重新加载才会生效。JSON文件修改后可以通过服务管理接口触发reload也可以用cas.service-registry.core.init-from-jsonfalse配合REST接口动态管理。团队如果比较规范建议把服务注册纳入统一的配置管理流程里而不是登录服务器上去改文件。3.4 用Java客户端的角度验证对接效果CAS Server就绪之后最重要的就是验证客户端对接。如果你用Java技术栈最简单的方式是使用官方cas-client-core这个SDK通过Servlet Filter方式接入。以Spring Boot的Web应用为例先在pom.xml里加依赖dependency groupIdorg.jasig.cas.client/groupId artifactIdcas-client-core/artifactId version3.6.4/version /dependency然后在配置类里注册过滤器。核心过滤器就两个一个是认证过滤器AuthenticationFilter一个是票据校验过滤器TicketValidationFilter。Bean public FilterRegistrationBeanAuthenticationFilter authenticationFilter() { FilterRegistrationBeanAuthenticationFilter registration new FilterRegistrationBean(); registration.setFilter(new AuthenticationFilter()); registration.addUrlPatterns(/*); registration.setOrder(1); registration.addInitParameter(casServerLoginUrl, https://sso.example.com/cas/login); registration.addInitParameter(serverName, https://app1.example.com); return registration; }看到casServerLoginUrl和serverName这两个参数就明白业务系统需要知道两件事CAS Server在哪以及自己在哪。这两个地址有一个对不上登录就会出问题。如果请求到达业务系统时发现用户没登录AuthenticationFilter就会把请求重定向到CAS登录页。登录成功后CAS跳回回调地址这时TicketValidationFilter拦截到请求里的ticket参数去CAS Server校验校验通过后会把用户名放到Session里业务代码直接取用即可。实际项目中对接语言五花八门PHP有phpCAS库Python有python-cas.NET也有对应的客户端实现。核心原理都一样跳转到CAS登录页拿到ST再回CAS Server校验。有了这套思路接什么语言都不是问题。4. 常见问题排查实录附速查表4.1 最常见的死循环跳转我在排查CAS问题时遇到最多的故障就是死循环跳转用户在浏览器里看到一个页面还没加载出来就马上跳到登录页然后又被弹回来来来回回刷屏。这种问题八成是业务系统没有正确建立本地会话。CAS回调带回了ST业务系统却没去校验本地Session始终不写入登录状态下次请求又触发认证过滤器重新跳回CAS。CAS一看有TGC又秒签一个ST跳转回来结果业务系统还是不认就这样绕圈。排查思路很简单先看业务系统日志里有没有票据校验的调用记录。没有说明过滤器配置漏了或者回调地址没被正确识别。有再看校验接口返回是什么——常见的是INVALID_TICKET或INVALID_SERVICE前者说明票据过期或重复使用后者说明这个回调地址不在白名单里。另外还要重点检查service参数的编码问题。CAS跳转会拼接一个原始的URL如果这个URL是个带参数的业务地址必须做URL编码否则参数会被截断导致回调地址对不上一样会陷入死循环。4.2 票据校验报错与刷新失效还有一个高频问题用户登录成功后业务系统第一次打开没问题一刷新页面就报错提示ticket校验失败。这在所有票据式SSO里几乎都会遇到。原因就是前面提到过的ST是一次性的。用户第一次访问时CAS签发的ST被业务系统拿去验证验证完这个票据就作废了。用户刷新页面时浏览器地址栏仍然保留着?ticketST-xxx业务系统再拿这个已作废的票据去验证CAS自然拒绝。解决方式有两种路径要么业务系统在校验成功之后立刻302重定向把地址栏里的ticket参数去掉要么在过滤器层面识别出校验失败且原因是票据问题自动再做一次跳转认证。官方客户端SDK一般已经处理了这个场景但如果是团队自研的客户端代码就很容易踩到这个坑。我自己的习惯是服务端拿到ticket后必须保证成功或失败都有日志记录时间戳、原始URL、票据号都要留。这样哪怕线上出了问题也能从日志里马上追溯到是哪一步断了。4.3 编码、证书、域名这些边角坑除了上面两个大头还有几个边角问题特别磨人。第一个是编码乱码。CAS Server和业务系统之间的跳转会多次携带URL参数如果两边编码不一致用户名里有中文就极容易变成乱码。部署Tomcat时server.xml里的URIEncodingUTF-8一定要显式配置。数据库连接串也要加上useUnicodetruecharacterEncodingUTF-8否则用户属性从数据库出来就是乱码。第二个是HTTPS证书问题。CAS Server配置了HTTPS后客户端调用校验接口时必须信任CAS的证书。如果CAS用的是企业内部自签证书业务系统JVM的信任库里默认没有它调用时就会报PKIX path building failed。解决办法是把自签证书导入业务系统的cacerts或者用统一的CA证书签发服务来生成CAS证书这样所有系统都能自然信任。第三个是域名问题。TGC是写在CAS Server域名下的Cookie所以CAS Server的域名必须是所有业务系统都能访问的公共域名不能是某个业务系统的内网主机名。否则浏览器根本不会带上TGC每次访问都会重新要求登录。多环境联调时一定要让所有参与方都配置好统一的CAS域名并使用一致的协议HTTP或HTTPS尽量避免混用。我把这些问题整理成一张速查表方便大家以后直接拿着对照现象根本原因排查方向登录成功后死循环跳转本地会话未建立查业务系统日志看票据校验是否执行刷新页面报ticket失效ST一次性使用地址栏残留校验成功后重定向去掉ticket参数报Unauthorized Service回调地址不在服务白名单检查serviceId正则是否完全匹配校验接口报PKIX错误证书不在信任库导入证书或换用受信任CA只有在登录页反复被踢TGC未写入或域不对检查CAS域名、TGC的secure配置用户名中文乱码编码配置不一致统一URIEncoding和数据库编码5. 再往深走一步多节点部署与会话共享5.1 TicketRegistry换成Redis单机版CAS跑通只能算入门。真正常见的报错是用户明明登录成功了下一跳系统却说没登录或者CAS Server每次重启所有在线用户全部掉线。原因是CAS默认把TGT/ST都存在了内存里进程一重启或切换节点票据就丢了。生产环境一般要部署多个CAS Server节点做高可用这时候就必须把票据存储从内存换到统一存储。我用的最多的是Redis。配置不复杂cas.ticket.registry.redis.hostredis.example.com cas.ticket.registry.redis.port6379 cas.ticket.registry.redis.passwordyour_password票据存在Redis之后无论用户被负载均衡到哪个CAS节点都能从同一个Redis里读到TGT/ST单点登录就不会因为节点切换而断掉。这里有个细节值得提票据在Redis里要设置合理的过期时间。CAS默认的TGT过期时间是8小时ST过期时间是10秒左右。生产环境建议根据组织的安全策略去调整比如财务系统相关的接入票据有效期就要短一些过期时间的配置项也在application.properties里不用改代码。5.2 单点注销与回调通知用户提到单点登录往往忽略单点注销——在CAS退出之后所有接入系统都应该跟着退出。这是CAS功能里比较复杂的一环也是很多项目后期补的课。CAS的注销流程大致是用户访问CAS的logout地址CAS会找到该用户已访问过的所有业务系统注册的logout地址然后逐个发送回调通常是后台通知业务系统收到回调后清除本地会话。要实现这个能力业务系统在对接时就要提前把自己的logout地址告诉CAS。在服务注册JSON里可以配置{ logoutUrl: https://app1.example.com/logout, class: org.apereo.cas.services.RegexRegisteredService, serviceId: ^https://app1\\.example\\.com/.*, name: 应用A, id: 1 }这个配置我是强烈建议在一开始就定好不然后期几十个系统一个个补工作量大到怀疑人生。而且有些系统没有提供logout回调接口那就不只是配置问题而是要改业务系统代码。另外CAS注销回调是通过服务器对服务器的HTTP请求来通知的所以业务系统的logout地址必须是CAS Server能访问到的地址。如果业务系统在内网CAS Server在外网中间有隔离单点注销就会失败。这个在架构设计时要提前想清楚。5.3 掌握这套东西之后能往哪些方向延伸CAS学到位之后它给你的不只是会用一个系统而是一整套身份认证设计的通用思维。后面再去看OAuth2、OIDC、SAML会发现很多概念都是相通的重定向、回调地址、一次性凭证、信任关系。这些协议本质上都是解决如何在多个互不认识的系统之间安全地传递登录状态这个问题。如果你在团队里承担架构角色还能在CAS之上继续做东西比如接入企业微信、钉钉、飞书的扫码登录把扫码登录作为一种认证方式接到CAS里。做成统一认证网关配合API Gateway一层把SSO能力从Web系统延伸到接口访问。对接LDAP/AD实现组织架构同步和账号生命周期管理。我个人实际体会是光把用户认证集中起来并不难真正难的是后续的账号生命周期管理、权限模型划分、审计合规这些脏活。CAS把最底层的认证协议和票据安全井井有条地处理好了你就能把精力集中在业务身份体系上。最后再分享一个小技巧CAS Server提供了完整的审计日志日志里能看到每次登录、每次票据校验的详细过程。排错的时候不要只盯着业务系统日志CAS Server的日志信息量要大得多配合DEBUG级别日志基本能定位九成以上问题。我每次接手陌生环境里的CAS系统第一件事就是开DEBUG日志看一遍完整的登录链路整个系统的用户量、访问路径、票据生命周期就都清楚了后面再怎么改配置心里都有底。