ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

CAS统一身份认证实战:从票据原理到单点登录落地

CAS统一身份认证实战:从票据原理到单点登录落地 公司里那几套系统各自为政的日子我印象太深了。财务系统一套账号OA一套账号项目管理系统又一套账号每个人桌面上贴着一排便利贴上面全是不同系统的密码。IT部门每天收到最多的工单就是“密码又忘了”“账号被锁了”。后来我们决定上CASCentral 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?servicehttps://app1.example.com/user/info注意后面那个service参数这个参数是回调地址CAS登录成功后要带着票据跳回哪去。第二步你在CAS登录页输入账号密码CAS校验通过后在浏览器端写入一个TGCTicket-Granting Cookie同时生成一个STService Ticket然后302跳回刚才的service地址把ST附加在URL后面类似https://app1.example.com/user/info?ticketST-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代码没看明白先被这几个英文缩写劝退了。其实这些词没那么玄我一个个捋。TGTTicket-Granting Ticket这是你在CAS登录成功后CAS服务器通过TGC Cookie关联的一份服务端票据。它代表“你已经通过认证了”有效期通常跟着TGC走默认可能是一天或更长。你在同一个浏览器里访问任何接入CAS的系统TGT都会发挥作用保证你不会被反复要求登录。STService Ticket这是CAS临时签发给某个具体业务系统的票据用来完成那一次“验票”动作。ST是一次性的用完了就失效而且有时效一般默认几十秒到几分钟。想想演唱会门口的检票员你递过去票他撕个角这张票就废了不会有人拿同一张票从另一个门再进一次。Service URL也就是业务系统的回调地址。这个地址必须提前在CAS服务端登记否则CAS会拒绝回调。原因很简单CAS要防止别人拿着合法票据去钓鱼比如攻击者伪造一个回调地址诱导CAS把票据发过去那票据就可能泄露。所以白名单必须配好。理解了这三个概念CAS的整个交互就通了。你回头看那些报错、死循环、登录不上的问题大多都能归结到这三个东西没配对要么ST过期了要么Service URL没登记要么TGC时间太短导致频繁重新登录。3. 动手接入CAS服务端与客户端的完整落地过程3.1 服务端部署最简配置跑起一个CAS ServerCAS本身有官方实现最常用的是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/GoCAS只是一个协议你完全可以自己用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。这个设计是好事防止票据到处乱飞。但对初次使用者来说很容易忽略。解决办法很直接在服务端注册这个servicehttps://app1.example.com/**配好之后重启CAS问题自然消失。这里要特别提醒生产环境不要配成**全局通配否则白名单就形同虚设了。要精确到具体的域名或者上下文路径。4.2 票据过期与二次重放引起的死循环还有一个经典问题用户点进去一个页面被跳去CAS登录CAS说“票据已使用过”然后又跳回业务系统业务系统拿票据去验验了一个空结果又给用户弹回CASCAS又生成一个新票据……就这样循环往复浏览器一直在两个地址间跳来跳去。原因一般有两个。一个是ST被客户端验了两次。CAS的ST默认是一次性的第一次验证成功后服务端就把它标记为已消费。如果你在Filter里验了一次后面的业务代码又验了一次第二次必然失败。解决办法是把验证结果缓存到Session里不要反复调CAS接口。另一个是进程时钟不同步。CAS在验证ST时会检查时间戳如果业务应用服务器和CAS服务器之间的系统时间偏差超过允许范围即使票据刚生成也会被判为过期。这个坑很隐蔽尤其是虚拟化环境下宿主机休眠恢复后容易时间漂移。解决办法是给所有服务器配NTP自动校时并且检查CAS默认的allowedClockSkew参数必要时在合理范围内放宽一点。4.3 用户名大小写、邮箱格式带来的身份漂移业务系统原有的用户体系如果跟CAS返回的用户标识对不上就会出现“登录成功但找不到人”的情况。比如CAS里用户名是zhangsan业务系统用户表里存的是ZhangSan两边一比对匹配不上系统直接给用户弹一个“无权限”或者“用户不存在”。这个问题的本质是“身份标识没有对齐”。我建议在规划阶段就统一用户唯一标识的格式。要么全用小写字母要么用邮箱做唯一键但邮箱也要注意大小写——很多邮箱域名本身不区分大小写但你如果在这边存了ZhangExample.com那边存了zhangexample.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变化。每一次跳转对应着协议里的哪一步协议一对照问题定位通常不会超过十分钟。这个方法帮我解决过十几个线上线下问题希望你也能用上。
返回列表