公司里那几套系统各自为政的日子,我印象太深了。财务系统一套账号,OA一套账号,项目管理系统又一套账号,每个人桌面上贴着一排便利贴,上面全是不同系统的密码。IT部门每天收到最多的工单就是“密码又忘了”“账号被锁了”。后来我们决定上CAS(Central Authentication Service),也就是统一身份认证服务,把登录这件事彻底收口。当时所有人都觉得这就是个“登录页改造”,真正做完才发现,CAS不只是省了一次输入密码的事,它直接改变了整个系统群的边界和安全隐患。
这篇文章就围绕CAS的方案选型、原理拆解、实际接入过程和线上运维经验来写,给正准备搞统一登录、或者已经在集成CAS路上踩坑的同行一些参考。文章不挑具体框架版本,重点讲思路和关键动作,哪怕你用的是别的语言、别的中间件,这套逻辑也基本通用。
1. 老系统各自为政的日子,到底疼在哪里
1.1 用户端与运维端的双重折磨
先说痛点,不然你体会不到为什么非要搞CAS。
以前我们有三套Web系统,都是不同时期外包或者不同团队做的。A系统是Java写的,B系统是PHP写的,C系统甚至是某个老同事用ASP.NET搭的。每套系统的用户表都是独立的,A系统里有张三,B系统里也有张三,但密码各改各的。用户新入职的时候,IT要挨个在三套系统里建账号;离职的时候,又要挨个去禁用。漏掉一个账号,可能就成了安全隐患。
用户端的体验更糟。一个人要记三套密码,而且每套系统的密码策略还不一样,A系统要求8位含特殊字符,B系统要求6位纯数字,C系统干脆不允许改密码。密码忘了只能找IT重置,IT重置又要先确认这个人是不是本人,一来一回半小时没了。
运维端的麻烦更大。你想想,三套系统的登录日志是分开的,安全审计的时候根本没法回答“张三今天登录过哪些系统”这种问题。出了问题想追溯,得登录三台服务器分别查日志。而且每套系统对密码存储方式都不一样,有的用MD5,有的明文存,想想都后怕。
有人说,那不就上个LDAP或者直接统一认证吗?道理是这个道理,但老系统改造最忌讳“推倒重来”。你不可能让三套系统重新做一遍用户体系,也不一定有权限去改别人的代码。这时候CAS的价值就出来了:它不要求你改系统内部的用户模型,只需要在应用前面加一道“认证拦截”,把你原来那套登录逻辑替换成跳转到CAS认证中心。
1.2 为什么不能靠“复制用户表”来凑合
有人可能会想,既然各系统都有用户表,那我写个脚本,每天把主用户库的数据同步到各个系统里不就行了?账号一致了,密码让他们都换成一样的,不就等于统一登录了吗?
这个思路我劝你趁早放弃。第一,密码同步这件事,意味着你需要在多个系统里保存同一份密码数据,任何一个系统被拖库,所有系统都跟着遭殃。你的安全性不是由最强的系统决定的,而是由最弱的那套决定的。第二,同步是单向的还是有向的?改密码在哪边改?如果用户在A系统改了密码,B系统要不要跟着改?这里面的冲突处理、失败重试、延迟问题,足够你写半年。
CAS走的完全是另一条路:密码只存在认证中心这一处,各业务系统不再自己验证密码。用户登录业务系统时,业务系统发现没有登录凭证,就302跳转到CAS登录页;用户输入账号密码后,认证通过,CAS给浏览器发一个全局票据,浏览器拿着这个票据回业务系统,业务系统再用票据去CAS后台换用户信息。这套流程下来,业务系统本身根本不接触密码,自然也不用存储密码。
这才是“翻身”的关键所在——不是把老系统推翻,而是让老系统把“认证”这个包袱交出去,自己专心做业务。就跟一个团队里多了个靠谱的行政,大家不用再操心订会议室、订机票的事,各自干各自的活儿就行。
2. CAS到底靠什么解决“一次登录,全网通行”
2.1 一张票据背后的两次跳转
CAS的核心思想,用一句话说就是:认证逻辑集中化,业务系统只认票据不认人。
我第一次看CAS流程图的时候,觉得有点绕,后来拿现实里的场景类比就通了。你去一个园区办事,门口有个总服务台,你在总服务台刷身份证办一张临时通行证,然后拿着这张通行证去园区里各个楼。每栋楼的保安不会让你再刷一遍身份证,只认你手里的通行证。CAS就是那个总服务台,通行证就是CAS颁发的票据。
具体到一个业务请求,走的是这么几步。
第一步,你访问业务系统A的一个受保护页面,比如/user/info。业务系统的客户端过滤器发现你本次会话里没有登录用户信息,就返回一个302重定向,把浏览器带到CAS服务器的登录页,地址长这样:
https://cas.example.com/cas/login?service=https://app1.example.com/user/info注意后面那个service参数,这个参数是回调地址,CAS登录成功后要带着票据跳回哪去。
第二步,你在CAS登录页输入账号密码,CAS校验通过后,在浏览器端写入一个TGC(Ticket-Granting Cookie),同时生成一个ST(Service Ticket),然后302跳回刚才的service地址,把ST附加在URL后面,类似:
https://app1.example.com/user/info?ticket=ST-20240513-xxxxx第三步,业务系统A拿到ticket参数后,不能直接信它,因为任何人都可以伪造一个ticket塞在URL里。系统A的后端要带着这个ticket和它自己的信息,再次请求CAS服务器的一个校验接口,比如/cas/serviceValidate。CAS确认这个ticket是自己发出的、没过期、对应的service确实是系统A,就把用户信息以XML或JSON形式返回给系统A。
第四步,系统A收到用户信息后,把用户放进自己的Session里,然后跳回用户原本要访问的/user/info页面。这个时候,用户再点系统B的链接,系统B也执行同样的流程,但CAS发现你浏览器里已经有TGC了,就不会再让你输入账号密码,而是直接发一个新的ST给系统B,全程无感。
这四次交互里,最关键的就是那张TGC。它相当于CAS发给你的一张贴在浏览器上的“已办证”标签,CAS看到这个标签,就不再重复问你身份信息。而ST是短期的、一次性的,专门给某个业务系统用的入场券。
2.2 TGT、ST、Service URL:三个名词理清楚
我见过不少同事刚开始接CAS,代码没看明白,先被这几个英文缩写劝退了。其实这些词没那么玄,我一个个捋。
- TGT(Ticket-Granting Ticket):这是你在CAS登录成功后,CAS服务器通过TGC Cookie关联的一份服务端票据。它代表“你已经通过认证了”,有效期通常跟着TGC走,默认可能是一天或更长。你在同一个浏览器里访问任何接入CAS的系统,TGT都会发挥作用,保证你不会被反复要求登录。
- ST(Service Ticket):这是CAS临时签发给某个具体业务系统的票据,用来完成那一次“验票”动作。ST是一次性的,用完了就失效,而且有时效,一般默认几十秒到几分钟。想想演唱会门口的检票员,你递过去票,他撕个角,这张票就废了,不会有人拿同一张票从另一个门再进一次。
- Service URL:也就是业务系统的回调地址。这个地址必须提前在CAS服务端登记,否则CAS会拒绝回调。原因很简单,CAS要防止别人拿着合法票据去钓鱼,比如攻击者伪造一个回调地址,诱导CAS把票据发过去,那票据就可能泄露。所以白名单必须配好。
理解了这三个概念,CAS的整个交互就通了。你回头看那些报错、死循环、登录不上的问题,大多都能归结到这三个东西没配对:要么ST过期了,要么Service URL没登记,要么TGC时间太短导致频繁重新登录。
3. 动手接入CAS:服务端与客户端的完整落地过程
3.1 服务端部署:最简配置跑起一个CAS Server
CAS本身有官方实现,最常用的是Apereo CAS,它是个基于Spring Boot的应用,所以部署方式跟普通Java应用一样,打war包扔进Tomcat,或者直接打成可执行jar跑起来都行。如果你只是想体验一把,我建议先用最简单的方式:
- 下载官方overlay工程,它可以让你在不修改核心源码的前提下覆盖配置。
- 修改
application.yml,把服务端口、应用名称、认证方式配置到位。 - 选择认证源。默认可能支持静态账号,正式环境一般接JDBC、LDAP或者OAuth。初期测试,先用内存用户跑通流程。
以一个简单的内存用户配置为例,关键配置长这样:
cas: authn: accept: users: admin::123456这是最傻的认证方式,永远只有admin和123456这一对账号能登录。但它的价值在于让你先把流程跑通,别一上来就接LDAP或者数据库,流程没通之前排查问题会非常痛苦。
服务端跑起来之后,访问https://cas.example.com/cas/login能看到登录页,就算成功了一半。
3.2 客户端接入:让Java应用认识CAS
客户端接入的方式取决于你用的技术栈。
Java/Spring Boot:官方有
cas-client-core,同时Spring Security也支持CAS集成。最省事的办法是用Spring Security的CasAuthenticationFilter,配置一下CAS服务器地址、service地址、以及ticket校验地址就行。如果用的是Shiro或者自己写的拦截器,那就纯手写一个Filter,逻辑无非是:检查Session里有没有用户 -> 没有就跳CAS登录页 -> 拿到ticket调校验接口 -> 解析用户信息 -> 放入Session。PHP/Node/Go:CAS只是一个协议,你完全可以自己用HTTP请求实现客户端。核心就两步:一是构造登录跳转链接,二是调用
/serviceValidate接口并解析返回结果。CAS支持JSON格式返回,比解析XML省心很多。
我自己写过一个最小的Java过滤器,核心逻辑不到一百行,大概是:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) { HttpServletRequest request = (HttpServletRequest) req; HttpServletResponse response = (HttpServletResponse) res; // 1. 如果session里已经登录,直接放行 if (request.getSession().getAttribute("user") != null) { chain.doFilter(req, res); return; } // 2. 如果没登录,看看URL上有没有ticket参数 String ticket = request.getParameter("ticket"); if (ticket == null) { // 没有ticket,跳转到CAS登录页 String loginUrl = "https://cas.example.com/cas/login?service=" + URLEncoder.encode(currentUrl, "UTF-8"); response.sendRedirect(loginUrl); return; } // 3. 有ticket,去CAS后端校验并换取用户信息 String xml = httpGet("https://cas.example.com/cas/serviceValidate" + "?service=" + currentUrl + "&ticket=" + ticket); String user = parseUserFromXml(xml); if (user != null) { request.getSession().setAttribute("user", user); // 去掉URL上的ticket,避免浏览器里残留票据 response.sendRedirect(currentUrlWithoutTicket); } else { response.getWriter().write("CAS验证失败"); } }这段代码只是演示思路,生产环境里你要处理并发、超时、异常场景。但它说明一个道理:CAS客户端不复杂,复杂的是你想把每个边界情况都处理好。
3.3 前端登录流程对接:从点击到回调的完整链路
后端接完之后,前端不需要做什么特殊开发,依旧是普通的页面跳转。但有一个体验细节我提醒一下:业务系统跳转CAS登录页的时候,一定要带一个service参数,而且这个参数必须是“用户当前正在访问的完整URL”。如果不带,或者只带一个固定的首页地址,用户登录完之后就会被扔到首页,而不是他本来想看的那个页面,体验非常割裂。
更稳妥的做法是,在后端保存一个redirectUrl,跳CAS之前把当前地址存到Session里,登录成功后从Session里取出来再跳回去。CAS的service参数只负责让CAS知道该回哪,真正的精确回跳地址最好由业务系统自己保管。
另外还有一个前后端分离场景的坑。如果你前端是Vue/React,后端是纯API,不能用传统的302跳转做登录。因为前端页面和后端API可能不同域,浏览器拿到302跳转之后,CORS、Cookie归属都会乱套。这种情况下一般让前端直接拿着window.location.href跳去CAS登录页,登录成功后CAS再带着票据跳回前端页面,由前端把票据交给后端换取用户信息。整体思路不变,但要特别关注跨域下Cookie的传递。
4. 集成中最容易翻车的几个细节,我替你踩过了
4.1 回调地址没配白名单,登录成功却被拒之门外
我们第一次部署测试环境,配好了CAS服务端,也写好了客户端过滤器,点登录跳转到CAS登录页,输入账号密码,结果CAS报错,提示类似“未授权的服务”。我当时第一反应是客户端代码写错了,查了半天,最后发现是CAS服务端配置文件里的service白名单没加我们那个测试应用的回调地址。
Apereo CAS对service的校验是默认开启的,凡是没配在serviceRegistry里的地址,一律拒绝签发ST。这个设计是好事,防止票据到处乱飞。但对初次使用者来说,很容易忽略。解决办法很直接,在服务端注册这个service:
https://app1.example.com/**配好之后重启CAS,问题自然消失。这里要特别提醒:生产环境不要配成**全局通配,否则白名单就形同虚设了。要精确到具体的域名或者上下文路径。
4.2 票据过期与二次重放引起的死循环
还有一个经典问题:用户点进去一个页面,被跳去CAS登录,CAS说“票据已使用过”,然后又跳回业务系统,业务系统拿票据去验,验了一个空结果,又给用户弹回CAS,CAS又生成一个新票据……就这样循环往复,浏览器一直在两个地址间跳来跳去。
原因一般有两个。
一个是ST被客户端验了两次。CAS的ST默认是一次性的,第一次验证成功后,服务端就把它标记为已消费。如果你在Filter里验了一次,后面的业务代码又验了一次,第二次必然失败。解决办法是把验证结果缓存到Session里,不要反复调CAS接口。
另一个是进程时钟不同步。CAS在验证ST时会检查时间戳,如果业务应用服务器和CAS服务器之间的系统时间偏差超过允许范围,即使票据刚生成也会被判为过期。这个坑很隐蔽,尤其是虚拟化环境下,宿主机休眠恢复后容易时间漂移。解决办法是给所有服务器配NTP自动校时,并且检查CAS默认的allowedClockSkew参数,必要时在合理范围内放宽一点。
4.3 用户名大小写、邮箱格式带来的身份漂移
业务系统原有的用户体系如果跟CAS返回的用户标识对不上,就会出现“登录成功但找不到人”的情况。比如CAS里用户名是zhangsan,业务系统用户表里存的是ZhangSan,两边一比对,匹配不上,系统直接给用户弹一个“无权限”或者“用户不存在”。
这个问题的本质是“身份标识没有对齐”。我建议在规划阶段就统一用户唯一标识的格式。要么全用小写字母,要么用邮箱做唯一键,但邮箱也要注意大小写——很多邮箱域名本身不区分大小写,但你如果在这边存了Zhang@Example.com,那边存了zhang@example.com,字符串比较照样不同。
最稳妥的做法是,在业务系统用户表里增加一个cas_uid字段,专门存CAS返回的正式用户标识。以后所有用户关联都以这个cas_uid为准,不再拿姓名、邮箱这类可能变的字段做匹配。这个改造不需要动老表结构,加个字段建个索引就行,工作量不大,收益很大。
4.4 登出只走了个过场,会话却还活着
单点登录(SSO)做了,用户美滋滋地一键登录所有系统,结果有一天同事问:“我点了退出,为什么再点别的系统还是直接进去了?”
这就是登出没做彻底。CAS的登出分为两层:一是CAS中心自己的会话销毁,二是所有接入业务系统的本地会话销毁。如果你只跳了CAS的登出接口,CAS中心的TGT废了,但业务系统A里的Session还热乎着,只要业务系统A没有再校验一次,那个Session就能继续用。
要解决这个问题,正规的做法是业务系统都注册一个logout回调地址给CAS。CAS收到用户登出请求后,先销毁中心会话,然后同时通知各个业务系统的logout端点,让它们各自销毁本地会话。但这里有个前提:业务系统必须实现了那个logout回调接口。很多老系统没有这个接口,那就只能依赖Session超时机制兜底,把各个系统的Session超时时间统一调短一些,降低残留风险。
我自己在实操中还有一个经验:登出操作最好放在CAS中心做,别在业务系统里各自做。用户点某个业务系统的“退出”,其实是跳转到CAS的/cas/logout,由CAS广播退掉所有接入方。这才能做到“退一处,全退”。
5. 从“能登录”到“用得爽”:生产环境的进阶改造
5.1 会话超时策略与踢人逻辑
流程跑通、能登录、能登出,这只是起步。真正进入生产环节,你要开始面对各种“纠结”问题,比如会话到底要保持多久。
CAS中心会话和业务系统的会话是两回事。CAS中心的TGT时间长一点没关系,用户一天之内打开多个系统都不需要重新输入密码。但业务系统自己的Session如果设置得太长,风险就大了,比如用户在公司电脑上登录了OA,下班忘了退出,第二天保洁阿姨打开那个浏览器还能看到所有页面。如果Session设置得太短,用户可能正在写一份长文档,写着写着会话过期,保存不上,那也是很崩溃的。
我们的做法是把中心会话设成8小时,业务系统Session设成30分钟无操作自动失效。这样既保证了短时间的跨系统无感访问,又限制了业务系统的暴露风险。如果你有更强的安全要求,可以再加“踢人”功能:管理员在后台禁用某个用户时,同时向CAS发送一个强制登出或改密码的指令,让该用户在所有系统的会话立刻失效。这个功能CAS本身支持,但需要额外配置,别等出了安全事故再想起来。
5.2 多环境共用CAS的注意事项
开发环境、测试环境、生产环境都接入同一个CAS,这看起来很方便,实际很容易乱。最大的问题是:你在开发环境登录的TGC,跟测试环境登录的TGC混在一起,相互覆盖或者串号。
为什么?因为CAS是通过Cookie来保存TGT的,浏览器同一个域名下的Cookie是共享的。如果你开发、测试、生产共用同一个CAS域名,浏览器里只会存一份TGT,你在测试环境登录一次,再去访问开发环境的应用,CAS发现你有TGC,直接让你通过了,但你开发环境里对应的本地用户可能根本不存在,或者权限完全不一样。
所以多环境隔离是必须的。最省心的方案是:环境与CAS的映射彻底分开。
- 开发环境用本地Mock登录,或者单独的CAS实例。
- 测试环境用测试域的CAS。
- 生产环境用生产域的CAS,域名最好带上环境标识,比如
cas-test.example.com和cas.example.com。 - 如果实在只能用一个CAS实例,那至少保证各个环境的Service URL写清楚,并且业务系统本地Session的隔离做得严格一些,不要通过CAS的全局会话推断本地用户。
这个坑我印象太深了。当时开发环境跟测试环境共用一个CAS,前端同事反复跟我反馈“我明明换了账号,怎么还是显示老账号的信息”,排查了两天,最后崩溃地发现就是Cookie里的TGC没变,CAS一直用同一个TGT在发ST。
5.3 安全加固:HTTPS、票据有效期、审计日志
安全性这块,我再强调几点,都是生产环境必须做、但文档里可能不写完整的。
第一,生产环境CAS一定要走HTTPS。票据是直接放在URL或Cookie里的,如果走明文HTTP,中间人抓包就能直接拿到ST或TGC。尤其ST是放在URL上回跳的,非常容易被日志系统记录、被Referer泄露。HTTPS不是可选项,是硬性要求。
第二,票据有效期要按场景调。ST的有效期没必要太长,默认60秒足够,太长的话重放攻击的时间窗口太大。TGT有效期也别设置成“永不过期”,建议结合公司安全要求,控制在8到12小时。用户如果登录超过一天,重新输一次密码是合理的,不要为了体验牺牲安全。
第三,必须记录认证日志。CAS本身会记录登录日志,但你要确保这些日志被收集到统一日志中心,保留至少半年。审计时我们需要回答“谁在什么时间从哪个IP登录了哪个系统”,CAS登录日志配合业务系统的访问日志,就能补全这条链路。我建议在业务系统接收CAS返回用户信息时,也打一条日志,包含CAS用户ID、来源IP、目标URL、时间戳,方便后续追踪。
第四,定期轮换CAS服务器自身的密钥和加密配置。如果你用的是正式版Apereo CAS,默认的签名密钥、加密密钥在部署后务必改成自己的,不要用官方文档里的演示值。这个改起来不麻烦,但漏掉的概率特别高,很多人跑通之后就直接没动过那些配置。
6. 落地之后的几点真心话
项目上线三个月后再回头看,CAS带给我们的不只是“一次输入密码”的便利。最明显的变化是IT部不用天天重置密码了,安全审计终于能给出一个完整的认证链路图。原本各自为政的用户表虽然没有立刻合并,但因为所有认证请求都经过CAS,我们对“谁是谁”这件事终于有了统一的判断标准。
我自己在接CAS过程中最大的体会是:别把它当成一个“登录页插件”,它是一个架构组件。你前期规划时对身份标识的统一、对登出回调的考虑、对多环境隔离的重视程度,决定了后期线上好不好用。那些只想着“先跑通登录,其他的再说”的团队,后面大概率要返工。
如果你正在筹备类似的项目,我的建议是:先花半天时间把CAS官网的术语表和流程图看明白,再花一天把服务端跑起来,然后用一个最简单的Java Filter把最核心的过滤、跳转、验票、建会话流程手写一遍。这套流程走完,你会比直接套用框架里的高级功能更理解CAS到底在做什么。
最后分享一个小的排查技巧:CAS集成出问题时,别急着看代码,先用浏览器开发者工具把从最初请求到最终跳转的所有302链路记录下来,逐个看Location地址、ticket参数、Cookie变化。每一次跳转对应着协议里的哪一步,协议一对照,问题定位通常不会超过十分钟。这个方法帮我解决过十几个线上线下问题,希望你也能用上。