ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

CAS撞车:Java并发CAS与SSO单点登录面试全解

CAS撞车:Java并发CAS与SSO单点登录面试全解 有一个朋友上周去面一家做供应链系统的公司一面聊并发编程面试官问他“AtomicInteger为什么线程安全”他顺口就回答了CAS二面聊简历里的单点登录项目面试官又追问了一句“你们用的CAS方案TGT、ST、TGC分别是什么角色”他当时直接愣了一下——同一个缩写他能把并发底层讲得头头是道却说不清认证票据之间的关系。这不是他准备不认真而是“CAS”这个词在技术面试里经常撞车。Java并发领域里的CAS是指Compare-And-Swap也就是比较并交换一种CPU级别的原子操作而Web认证领域的CAS是Central Authentication Service一个开源的单点登录协议和服务端实现。如果你搜“CAS面试题总结”或者准备“springbootshiro集成cas”这类项目复盘大概率会遇到两套完全不同的考点。本文打算站在面试官视角把这两类问题拆开讲一遍从核心原理到真实项目里的命中点尽量做到能直接拿去用。1. 面试里的CAS有两副面孔先搞清楚面试官问的是哪一套1.1 Java并发里的CAS一个原子化的判断加更新操作很多人在简历上写了“熟悉Java并发编程”然后被问到CAS时都会先想到这个方向。说白了它是解决并发安全的一种无锁方案目标是让“判断某个内存值是否等于预期值如果是就把它改成新值”这个动作变成单个不可分割的原子操作。如果面试官问的是并发CAS后续通常会跟着几个固定的岔路口CAS和synchronized的区别、ABA问题、AtomicInteger的实现原理、高并发下自旋的代价。这些都是我在第2章和第3章会展开的内容。1.2 认证领域的CAS多系统登录的公共票据中心如果面试官问的是项目经历尤其是那种简历里出现过“单点登录”或“统一认证平台”的候选人他们口中的CAS基本就是Central Authentication Service。这类CAS在工程项目中通常是一个独立的服务端统一处理用户名密码校验、会话管理和票据签发。各个业务系统不再各自维护login页面而是遇到未登录请求时统一跳转到CAS服务端去登录。面试官在这个方向下关心的是TGT和ST是怎么签发的、客户端如何校验票据、service回调地址要怎么配置、登录状态如何共享、登出如何同步。1.3 当面试官只丢出“CAS”三个字母时怎么判断提问意图我在整理面试题库时发现一个现象很多候选人在听到CAS之后会下意识按自己最熟悉的一套回答。如果面试官场景是“你们项目登录是怎么做的”你就别从CPU指令开始讲如果场景是“高并发下如何保证线程计数准确”那就别扯TGT票据。有个比较实用的判断方法先看上下文里有没出现synchronized、Atomic、无锁、自旋这些词再看上下文里有没有出现登录、ticket、SSO、回调、session这些词。前者属于并发CAS后者属于认证CAS。如果面试官故意只说一句“说说你了解的CAS”那么在时间允许的前提下你大可以先明确句“CAS有两个常见的含义在并发场景里它是比较交换在单点登录场景里它是统一认证服务”然后等面试官确认方向。这种主动澄清本身就是加分项比闷头讲错方向要体面得多。2. 并发CAS的经典考法从底层原理一路问到JDK实现2.1 CAS的握手过程V、A、B三个操作数并发CAS可以用一句话解释需要一块内存位置、一个预期值和要写入的新值。具体到每次操作时先读取内存位置的当前值V拿它和预期值A比较。如果V等于A说明操作期间没有被人改过这时候可以把新值B写进去如果V不等于A说明别人已经改过了操作失败什么都不做。整个过程在硬件层面是原子的所以不需要加锁也能保证“比较写入”之间不会被其他线程打断。生活里可以把它理解成公告板上贴值班表你想把自己排进周五先看一眼板上还是“待定”两个字确认后赶紧贴上自己的名字。CAS保证了“确认”和“贴名字”之间不会有人插队修改。面试时如果能补充一句“CAS在x86平台底层使用cmpxchg指令在ARM平台依赖LL/SC机制”会明显比只记一个缩写有说服力。2.2 Java里的CAS藏在哪Unsafe和native方法Java应用层不能直接触发CPU的cmpxchg指令JDK把这条路封装在了sun.misc.Unsafe里。Unsafe类里最核心的是几个native方法比如compareAndSwapInt和compareAndSwapObject它们接收对象、内存偏移地址、期望值和新值返回布尔值表示是否更新成功。java.util.concurrent.atomic包下面的原子类底层调用的就是它们。JDK 9之后还引入了VarHandle提供了更标准、更受约束的访问方式可以操作对象字段的原子更新。不过日常面试涉及到的绝大多数案例还是围绕Unsafe和AtomicInteger展开。你不需要一字不差背源码但至少要清楚这条路AtomicInteger - Unsafe.compareAndSwapInt - native方法 - CPU原子指令2.3 一个自增案例看懂原子类的自旋更新AtomicInteger的incrementAndGet为什么是线程安全的很多人只记得结论却说不清过程。看JDK源码时间比较久远但思路稳定public final int getAndAddInt(Object o, long offset, int delta) { int v; do { // 1. 先读取当前值 v getIntVolatile(o, offset); // 2. CAS尝试更新失败就继续读、继续试 } while (!compareAndSwapInt(o, offset, v, v delta)); return v; }这段代码其实就是“先读、再试、失败了重读、再试”的循环。只有一个线程能成功把值从v改成vdelta失败线程不会进入阻塞状态而是回到读取当前值这个步骤重新竞争。所以CAS也被称为乐观锁默认假设冲突很少把精力放在不断尝试上。如果你在项目里写过类似这样的并发计数场景是很适合拿出来当案例的AtomicInteger counter new AtomicInteger(0); // 模拟10个线程每个线程累加1000次 CountDownLatch latch new CountDownLatch(10); for (int i 0; i 10; i) { new Thread(() - { for (int j 0; j 1000; j) { counter.incrementAndGet(); } latch.countDown(); }).start(); } latch.await(); System.out.println(最终计数: counter.get()); // 稳定输出10000换成普通int最终结果大概率会小于10000原因就在于“读旧值、加一、写回”这三步不是原子的。这个对比案例我在面试候选人时经常听到答对的人基本都能顺带讲出自旋原理。2.4 和synchronized放在一起时对比点是什么面试官一旦追问“CAS和synchronized到底谁更好”最忌讳的回答是“CAS快所以CAS好”。真实情况是看竞争等级。synchronized在JDK早期确实是重量级锁线程进入前会争抢monitor拿不到锁就可能被挂起并涉及操作系统层面的线程上下文切换。但JDK 6之后做了锁升级优化一个对象一开始是偏向锁竞争升级到轻量级锁轻量级锁失败的场景才升级为重量级锁。轻量级锁的实现里就借助了CAS思想所以两者并不是简单的替代关系。CAS的优点是失败不会导致线程挂起对短临界区场景很友好缺点是如果竞争特别激烈大量线程会陷入循环重试白白消耗CPU。这也是为什么不能绝对地说“无锁一定比有锁快”在核心热点高竞争场景下适合的锁策略反而更可靠。面试时能说出这段辩证关系会比只会背结论给人留下更深的印象。3. 并发CAS的深水区三个面试官最不肯放过的追问3.1 ABA问题值回到了原点但故事已经变了CAS只认“当前值是否等于期望值”它不关心变量在中间被修改了几次。假设线程甲读到值是A在它执行CAS之前线程乙把值从A改成B再改成A等线程甲回来时看到值还是A于是CAS成功。这个过程的本质问题是虽然最终值等于期望值但数据的历史已经变了原先的某些假设可能不再成立。一个经常用作例子的无锁栈场景线程甲要删除栈顶节点A先读取了A的地址和A的next节点。如果中途其他线程把A改成B又改回A栈关系可能已经变化线程甲再对栈顶执行CAS就可能错误地丢掉节点。ABA之所以有危害是因为它不只发生在整数加减法上还会发生在引用类型的结构变更上。解决ABA的通用思路是引入版本号。JDK提供了AtomicStampedReference它在维护目标引用时同时维护一个int类型的版本戳CAS时需要引用和版本号都相等才能成功AtomicStampedReferenceString ref new AtomicStampedReference(A, 0); int[] stampHolder new int[1]; String current ref.get(stampHolder); // 期望引用为A、期望版本为stampHolder[0]新引用为B新版本加1 boolean success ref.compareAndSet(current, B, stampHolder[0], stampHolder[0] 1);AtomicMarkableReference是简化版只用布尔值标记对象是否被修改过。如果候选人连版本号方案都答不出来面试官通常会在这一题上直接降低评价。3.2 自旋饥饿一直重试不一定等到锁CAS的致命弱点之一是它不阻塞失败线程就是不断循环尝试。如果多个线程同时争抢同一个热点变量且每个线程持锁时间又比较长那么这批线程不会让出CPU只是空转最终可能导致CPU使用率很高、整体吞吐反而下降。这种忙等策略在高并发场景下是有饥饿风险的。HotSpot虚拟机里也做了一部分优化会使用自适应自旋JVM根据前一次自旋获取锁的成功率来动态调整自旋次数减少无意义空转。但在纯应用层写循环CAS时这种自适应能力并不一定适用。因此实践上热点超级集中的计数器、需要频繁修改的共享状态更适合考虑分片手段或更成熟的同步器。面试官问到这里时如果你能主动提到AQS内部就是用CAS操作状态state配合阻塞队列来实现锁的那说明你对JUC的理解就不再停留在原子类层面。3.3 单变量操作限制想原子更新多个字段怎么办CAS还有一个限制它一次只能保证“一个变量的比较和交换”。可实际业务里经常需要同时更新两个字段例如账户余额和版本号或者坐标对象里的x和y。这个限制在面试里经常被转述为“CAS只能操作一个变量那我要同时更新多个变量该怎么办”第一个思路是把多个变量封装成一个POJO然后用AtomicReference去整体替换这个对象引用。比如一个类有name和age两个字段可以定义UserInfo对象然后每次更新都创建新对象再通过AtomicReferenceUserInfo做CAS。缺点也是显然的只要任何字段变化整个对象就要重建。第二个思路是改用锁在并发修改多个字段的场景用synchronized或ReentrantLock保护临界区把复合操作的原子性交给锁来保证。到这里你已经能向面试官解释为什么CAS适合简单状态更新而复杂复合操作还是离不开锁。3.4 LongAdder的思路把抢同一个位子改成分窗口排队如果你要回答“高并发计数到底怎么优化”LongAdder是一个很好的落点。它没有让所有线程死磕同一个base变量而是准备了一个Cell数组每个线程会尝试通过hash散列到某个Cell上做累加最终求和时把base和各Cell的值加起来。只有Cell需要扩容或竞争非常剧烈时才有线程会去竞争修改base。这个设计本质是降低单点竞争原本所有线程都在抢同一个内存地址现在拆成多个槽位如同一家只有一个窗口的银行排队改成按号分散到多个窗口。LongAdder在写多读少的统计场景里很有价值但它在真正需要精确读取总数的那一刻才汇总所以读一致性上不如AtomicInteger直观这也是 AtomicLong与LongAdder在各自场景里的取舍之一。4. 认证型CAS面试题不把TGT、ST、回调URL讲清过不了这关4.1 单点登录要解决什么问题如果你的简历里有“接入CAS”“单点登录”“统一认证”之类的描述那一整套认证型CAS面试题就会围绕一个真实的业务需求展开一个员工每天要打开OA、ERP、CRM等多个系统如果每个系统都要单独登录一次体验差、密码也多安全上也不好管理。最好的方式是一处登录、处处通行登录一次后在所有已接入系统里都能被信任这就是单点登录通常简称SSO。CAS就是实现这种模式的经典方案它有一个独立的认证服务端各业务系统作为客户端接入。服务端负责统一校验身份和签发凭证客户端负责把“匿名用户”引导到认证中心并把认证结果转换成本地会话。4.2 三个核心票据TGT、TGC、ST很多候选人讲不清认证流程不是因为不知道要重定向而是没有理顺票据间的使用关系。TGTTicket Granting Ticket。用户第一次在CAS登录页输入密码成功之后服务端会为用户创建一个TGT可以理解成服务端长期保存的“全局登录会话”它代表用户已经通过身份验证。TGCTicket Granting Cookie。服务端创建TGT后向浏览器下发一个名为TGC的Cookie。浏览器下次访问CAS服务端时靠这个Cookie找到用户对应的TGT从而知道“你已经登录过了”。STService Ticket。当已经登录的用户要访问某个具体业务系统时CAS服务端给这个“业务系统用户”的组合签发一张一次性服务票据ST。业务系统拿着ST去CAS服务端校验校验通过就知道用户确实是合法用户。三者之间一句话总结TGC是浏览器侧的身份凭据TGT是服务端保存的登录会话ST是每次访问业务系统时临时颁发的通行证。4.3 一次完整的CAS登录流程按文字步骤拆解假设用户首次访问系统A系统A地址是https://app1.example.comCAS服务端地址是https://sso.example.com/cas。我用文字按顺序拆解每一步面试时按这个顺序讲就不容易乱用户浏览器访问App1首页App1发现没有本地登录态比如session为空于是把请求重定向到CAS登录接口重定向URL带上service参数这个参数表示“登录成功后回跳到哪个地址”。典型形式是https://sso.example.com/cas/login?servicehttps://app1.example.com/callback。浏览器跳转到CAS登录页。此时浏览器如果没有TGCCAS就显示用户名密码表单。用户提交账号密码CAS校验通过后服务端创建TGT并向浏览器写入TGC然后把浏览器302重定向回service参数指向的地址同时在地址后面追加一个ticket参数https://app1.example.com/callback?ticketST-xxxxx。App1后端收到这个回调请求后取出ticket和service地址在服务端调用CAS的serviceValidate接口来校验https://sso.example.com/cas/serviceValidate?servicehttps://app1.example.com/callbackticketST-xxxxx。CAS服务端检查ST有效、且绑定的service与参数一致然后返回一段用户信息通常包含用户名和扩展属性。App1拿到用户信息后建立自己的本地会话后续请求就不再触发CAS跳转。此时再访问系统B时流程会缩短App2发现用户没有本地会话把请求重定向到CAS登录地址浏览器带着TGC访问CASCAS通过TGC找到TGT后发现用户已经登录于是不再要求输入密码直接生成一个绑定系统B的ST并302回跳到App2App2再次完成校验和本地会话创建。对用户来说他只输了一次账号密码。4.4 单点登出反过来通知所有业务系统单点登出是面试里很容易被忽略的考点。单点登录不是只在一个业务系统里点击退出然后只清空本地session那么简单。核心在于通知链条用户访问CAS的logout接口CAS服务端销毁全局TGT同时CAS服务端需要主动通知所有曾给这个用户发送过ST的业务系统让它们清理各自的本地会话。在客户端实现层面业务系统通常会预留一个logout入口给CAS服务端调用收到通知后执行本地session销毁这样才能保证用户在任何一个系统退出后再访问其他系统也需要重新登录。4.5 这一节的标准表述方式如果面试官让你“讲讲单点登录原理”我建议采用四步法组织口头答案先说SSO要解决的问题再说TGT、TGC、ST三种凭证分别是什么然后结合访问A系统和访问B系统两种场景把流程串起来最后补一句“ST是一次性的且和service地址绑定从而降低票据被滥用的风险”。这段话说下来大概两分钟逻辑完整比零散地讲“登录某系统会跳到一个认证中心”要专业很多。5. springbootshiro对接CAS的项目经历怎么讲得让面试官点头5.1 先交代技术选型为什么是CAS加Shiro而不是另造一套项目型问题的第一个高频点往往是技术选型。大家现在的登录方案很多有自己写session的有引入Spring Security的有用OAuth2/OIDC的为什么你们要选CAS加Shiro实际业务背景通常是这样系统数量多、技术栈杂、建设年代不同但又必须快速实现全公司统一登录。CAS服务端是现成的、社区成熟的统一认证服务改造成本低不需要自己从零设计票据系统。Shiro在业务系统中负责登录之后的权限控制、会话管理和注解式鉴权它比Spring Security轻量团队也熟悉过滤器链的概念。于是边界就很清晰CAS管“你是谁”Shiro管“你能干什么”。5.2 cas-client四个过滤器在业务系统里的职责SpringBoot项目接入CAS时绕过各种starter的封装直接看cas-client-core里几个过滤器会更利于理解SingleSignOutFilter处理CAS服务端发来的登出通知负责在收到全局登出时清理当前业务的session。AuthenticationFilter拦截需要登录的请求如果检测不到登录用户就把请求重定向到CAS登录页。TicketValidationFilter拦截携带ticket的回调URL调用CAS serviceValidate接口完成校验。校验通过后把用户信息放入session。HttpServletRequestWrapperFilter把用户名等信息包装到当前请求的属性里方便后端代码直接取用。这四个过滤器在过滤器链中的先后顺序也很重要。如果顺序错了很可能出现ticket还没被校验请求就被当成未登录重定向的问题。一般SingleSignOutFilter放在最外层保证先处理登出请求再进入认证判断。SpringBoot里可以用FilterRegistrationBean手动设置注册顺序。5.3 Shiro在这套体系里管什么、不管什么有不少候选人一说起“shiro集成cas”就以为要改一大堆认证逻辑其实Shiro在这个组合里负责的是本地会话和权限模型。CAS把认证做完后业务系统已经拿到了合法用户名接下来要做的通常是查本地用户表或用户中心得到用户角色、权限、组织等数据把这些数据形成Shiro的Subject主体关联到Realm中Shiro继续用自身的过滤器链保护资源比如判断某个URL是否需要登录、是否属于管理员权限范围。在集成时工程上会把携带ticket的回调路径配置成匿名可访问例如/login/cas然后把其余需要保护的资源路径统一走Shiro的认证过滤器。如果两个体系各自再进行一次登录判断没有做好衔接就容易出现登录成功后仍然被Shiro兜住的循环跳转。5.4 实际集成时最容易翻车的三个细节先聊service地址不匹配。这是我在现场见过最多的环境问题。本地调试用localhost访问测试环境用test.example.com访问但CAS服务端允许的service列表中只配了后者前端跳回时就会收到“未授权的service”报错。联调之前一定要把所有会用到的回调域名加进白名单而且URL编码前后的路径要完全一致不要以为多了个尾斜杠没事。再说重定向循环。现象是浏览器在业务系统和CAS登录页之间反复跳进不了登录页也进不了系统。常见原因是业务系统判断是否登录时读取不到cas-client写入的session或者登录成功后没有正确构建Shiro的Subject。排查时先确认回调是否确实走通了再打开浏览器开发者工具看响应头的Location别一上来瞎改配置。最后是证书和会话过期时间不一致。CAS服务端和业务系统之间如果要互相发起请求例如业务系统去serviceValidate、CAS服务端去通知登出走HTTPS时如果证书不是公共CA签发就可能有SSL握手失败。两端需要把对方证书正确导入信任库。会话过期问题更隐蔽CAS全局TGT通常几小时过期而业务系统的登录态可能半小时就过期两套时间不匹配会造成体验混乱要么让客户端本地session时长与CAS的Timeout策略尽量对齐要么通过单点登出机制强制
返回列表