ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

SpringBoot前后端分离项目接入CAS单点登录实战

SpringBoot前后端分离项目接入CAS单点登录实战

SpringBoot 前后端分离项目接入 CAS 单点登录,看起来是个老生常谈的需求,但真正把前后端分离架构和 CAS 经典的“重定向 + Cookie + 服务端 Session”模式揉到一起时,到处是坑。我最近刚好帮某个内部系统做完这套对接,从票据校验到跨域 Cookie,再到登出回调,踩了不少雷,也整理出一套比较稳定的方案。这篇文章就把完整思路、关键代码和排查实录写出来,给准备接 CAS 的后端和前端同学做个参考。

这套方案适合两类人:一类是后端工程师,需要在自己项目里快速集成 CAS 协议但又不想被老旧的 Filter 客户端拖累;另一类是前端开发,需要搞清楚登录跳转、ticket 捕获和跨域携带 Cookie 这三个环节到底是怎么协同的。我会先用大白话拆清楚 CAS 的原理和前后端分离场景下的冲突点,再给出一套“前端拿票、后端换票”的实现链路,最后把 CORS、SameSite、Session 这些跨域配置一次讲透。

1. 先把问题看清楚:前后端分离项目接 CAS,到底难在哪

1.1 前后端分离架构下的身份认证痛点

传统服务端渲染项目接 CAS 非常简单:后端加 Filter,用户访问受保护资源时,服务端直接重定向到 CAS 登录页,登录成功后回调地址也是后端接口,整个链路里服务端始终能拿到 Session。

但前后端分离项目把这条路切断了。前端是一个独立部署的静态站点,后端是纯 API,用户直接访问前端页面时根本没有服务端会话这一说。CAS 的 SSO 会话是建立在浏览器与 CAS 服务端之间的,靠的是 CAS 域名下的 Cookie(TGT),而业务系统的登录态是建立在前端页面跟后端 API 之间的。两个会话之间唯一的桥梁就是 CAS 协议里的一次性票据(ST)。

所以你首先得接受一件事:即便接入了 CAS,你也不可能直接复用 CAS 服务端的登录态。你的后端仍然需要建立自己的会话机制,CAS 只是帮你完成“这个用户是谁”的认证,而不是帮你管理整个业务系统的授权。

1.2 CAS 登录流程拆解:TGT、ST、service 回调的关系

在写代码之前,一定要把 CAS 协议的三张牌搞清楚:TGT、ST、service。

TGT 是 Ticket Granting Ticket,用户输入账号密码成功之后,CAS 服务端在自己的域名下种下的“登录凭证”。它代表了用户在 CAS 服务端的认证状态,TGT 存在 CAS 服务端的 Session 里,浏览器里只保留 CAS 域下的 Cookie。这就是为什么你在一个系统里登录过 CAS,再访问另一个接入了同一 CAS 服务端的系统时不用再次输入密码——浏览器带着 CAS 域下的 Cookie,CAS 服务端看到 TGT 还没过期,直接签发新票据。

ST 是 Service Ticket,一次性票据。当 CAS 确认用户已登录后,会重定向到业务系统的回调地址并在 URL 上附带这个 ticket 参数。业务系统拿着这个 ST 去 CAS 服务端的票据校验接口换用户信息。ST 只能换一次,换完立刻失效,而且有时效性(通常五分钟)。

service 参数是这段流程里业务系统与 CAS 服务端之间的“对账凭证”。它告诉 CAS 服务端“我是谁”、“登录成功后你要把我送回哪个地址”。CAS 服务端注册了哪些 service 是合法的,就只能重定向到这些地址,再额外校验回调地址与请求时的 service 完全一致,包括协议、域名、端口、路径。

放到前后端分离场景里,整个过程是这样的:

  1. 用户访问前端受保护页面,前端判断本地无登录态。
  2. 前端把页面重定向到 CAS 登录页,地址形如cas.example.com/login?service=frontend.example.com/cas/callback。
  3. 用户登录成功后,CAS 重定向回frontend.example.com/cas/callback?ticket=ST-xxx。
  4. 前端拿到 URL 上的 ticket,调用自己后端的/api/auth/cas/validate?ticket=ST-xxx。
  5. 后端收到 ticket 后,在服务端发起请求到 CAS 的/serviceValidate接口换用户信息。
  6. 校验成功,后端建立自己的会话,返回前端用户信息和登录状态。

这里面最容易出错的地方有两个:service 参数必须与 CAS 服务端注册的回调地址精确匹配;ticket 只能通过一次,千万别在前端就傻乎乎地消耗掉,或者让多台服务器同时去校验。

1.3 跨域问题的两个层面:浏览器同源策略 vs Cookie 归属域

前后端分离项目里,跨域从来都不是一个概念,而是两层问题,好多人把它们搅在一起,导致排查时越搞越乱。

第一层是 CORS。这是浏览器的同源策略,意思是当你前端页面域是frontend.example.com:8081,后端 API 域是backend.example.com:8080时,浏览器跨域请求默认会被拦截。要解决它,后端必须返回Access-Control-Allow-Origin、Access-Control-Allow-Methods等响应头。这套机制针对的是“能不能跨域发请求并读取返回”的问题。

第二层是 Cookie 归属域。就算 CORS 通过了,请求能发出去了,浏览器也默认不会在跨域请求里自动带上目标域的 Cookie,同时跨域响应里的Set-Cookie也可能被浏览器忽略。想让 Cookie 在跨域场景下正常工作,得同时满足三个条件:请求方设置withCredentials: true、服务端 CORS 配置allowCredentials(true)、Cookie 自身的Domain和SameSite属性允许被写入和携带。

很多项目里出现“接口能通,但登录态就是保不住”的怪现象,其实不是 CORS 没配好,而是 Cookie 这一层没处理干净。后面我会单独用一个章节讲透这两个层面的配置组合。

2. 方案选择与技术架构

2.1 核心认证链路设计:前端拿票、后端换票

我最终定的方案叫“前端拿票、后端换票”。这个字面意思就把整个链路概括了:前端负责跳转 CAS 登录页、接收回调地址上的 ticket;后端负责把前端传过来的 ticket 换成熟知的用户身份,并建立业务系统的会话。

为什么不让后端直接作为 service 回调地址呢?你也完全可以做成这样:用户访问前端页面,后端接口发现有未登录用户,直接后端返回 302 到 CAS 服务端,CAS 登录完再回调后端接口,后端建立完会话再重定向回前端页面。但这条路在前后端分离下非常别扭。前端是单页应用,它期望的是接口返回 JSON,不是 302。你还要在后端维护一个重定向 URL 的状态流转,调试起来异常痛苦。更关键的是,现在很多前端用的是 Vue、React 独立部署,后端网关跟前端部署域根本不是同一个,后端做 service 回调会让地址非常难管理。

让前端拿票就一定安全吗?很多人担心,ticket 在 URL 里传来传去,会不会被偷?确实有泄露风险,但 CAS 协议本身就是这样设计的,ST 是一次性的,而且必须配合 service 参数一起在后端校验,就算有人拿到了 ticket 但 service 对不上,校验一样失败。再加上我们在后端对回调来源地址做了白名单校验,这种风险是可控的。既然 CAS 协议要求这样,我们就按协议来,不要硬把 ST 塞进 Session,那反而会制造出不必要的问题。

核心链路确定后,前后端职责边界就非常清晰了。前端只管三件事:判断登录态、拼跳转链接、取 URL 参数里的 ticket 调后端;后端只管两件事:验票换身份、管理业务系统自己的会话。

2.2 工程结构落地:模块划分与边界

我习惯把 CAS 对接代码收敛成一个独立的后端模块,不要到处写。后端按这个结构拆:

  • auth模块:负责票据校验、会话创建、当前用户信息获取。
  • user模块:负责业务用户体系,CAS 换回来的用户名只是“认证身份”,还需要在本地用户表里映射成业务用户。
  • common模块:放统一返回结构、异常处理、跨域配置等基础设施。

这样做的好处是,以后如果同一个后端集群有多个服务需要接 CAS,可以直接复用auth模块,而不是每个服务里复制一遍登录逻辑。别忘了分布式环境下 Session 的存储问题,如果后端是多实例部署,Session 建议直接放到 Redis 里,否则用户一次请求打到实例 A、下一次打到实例 B,Session 就找不到了。CAS 只解决“你是谁”,不解决“你每次请求打到那台机器还能认识你”,这一点是很多小团队第一次上多实例就翻车的地方。

对于前端,我建议封装一个authService,暴露三个方法:initAuth()、redirectToCasLogin()、handleCallback()。组件里不要直接操作window.location或者拼 URL,关键逻辑集中起来,避免每个页面写一套。登录回调页也只是一个壳,真正的工作是取 ticket、发验证请求、存登录态、跳转到目标路由。

2.3 为什么不建议直接抄老项目的 Filter 客户端

很多人一搜 CAS 接入,第一反应就是用重量级客户端库,配置一堆 Filter。你在早期服务端渲染项目里用没问题,但在前后端分离项目里,我真的不建议再走这条路。

原因有三。第一,老客户端的设计目标是把登录重定向逻辑放在服务端 Filter 里拦截所有受保护请求,这在前后端分离项目里完全不适用。一个单页应用前端页面,访问/login静态资源本身你怎么拦截?拦截了之后返回 302,SPA 还怎么渲染?第二,这类客户端的默认行为会劫持整个 Servlet 链,做大量 exclude 配置才能保证你的 API 和静态资源不被它拦截,配置复杂度和踩坑概率远超预期。第三,我们真的只需要 CAS 协议里的一个接口——票据校验,自己写一个 HTTP 客户端调它,输出一个用户信息,剩下的会话与权限管理本来就应该由 Spring Security 或自己的拦截器负责,引入库反而把职责搞混了。

所以这个方案从实际落地上来说,就是“轻协议客户端 + 自建会话 + 清晰的前后端分工”。你少依赖一个库,以后升级维护少一份麻烦。

3. 完整实现步骤与关键配置

3.1 后端 API:票据验证接口的实现

后端验证接口是整个对接的核心。它的逻辑不复杂,但细节很多。

接口入参只有两个:ticket 字符串,以及可选的 service 参数。service 参数在前后端分离场景下尤其关键,因为前端回调的可能是一个固定地址,而 CAS 服务端注册的也是这个地址,后端必须校验最终使用的 service 是白名单内的。如果这个 service 可以随便传,那么攻击者可以用你的后端接口去消耗别人的 ST,甚至利用你的服务做开放重定向。

看一个完整的 Controller 实现:

@RestController @RequestMapping("/api/auth") public class CasAuthController { @Autowired private CasService casService; @Autowired private UserService userService; @GetMapping("/cas/validate") public Result<LoginResponse> validate( @RequestParam("ticket") String ticket, @RequestParam(value = "service", required = false) String service, HttpServletRequest request) { String validService = casService.resolveServiceUrl(service); // 白名单校验,不匹配直接抛异常 if (!casService.isAllowedService(validService)) { throw new BizException("非法的服务来源"); } // 后端服务端调用 CAS serviceValidate 接口 CasUserInfo userInfo = casService.validateTicket(ticket, validService); // 从本地用户库映射出业务用户,不存在则做自动创建或提示 SysUser sysUser = userService.getOrCreate(userInfo.getUsername()); // 建立业务系统自己的会话 HttpSession session = request.getSession(true); session.setAttribute("userId", sysUser.getId()); session.setAttribute("username", sysUser.getUsername()); session.setAttribute("casUser", userInfo.getUsername()); LoginResponse resp = new LoginResponse(); resp.setUserId(sysUser.getId()); resp.setUsername(userInfo.getUsername()); resp.setDisplayName(userInfo.getDisplayName()); return Result.ok(resp); } }

再看CasService里最核心的票据校验方法。CAS 协议 2.0/3.0 的验票接口就是访问一个带参数的服务端地址,返回 XML:

public CasUserInfo validateTicket(String ticket, String service) { String rawUrl = casServerPrefix + "/serviceValidate?service=" + URLEncoder.encode(service, StandardCharsets.UTF_8) + "&ticket=" + URLEncoder.encode(ticket, StandardCharsets.UTF_8); // 用带超时的 HTTP 客户端发起 GET 请求 String xml = httpClient.get(rawUrl); return parseCasXml(xml); }

解析 XML 时建议用 XPath 而不是字符串截取,因为 CAS 服务端版本不同,XML 结构可能有细微差异。相关文件在cas:serviceResponse命名空间下,认证成功时/cas:serviceResponse/cas:authenticationSuccess/cas:user就是用户名,失败时则是cas:authenticationFailure节点,里面有 code 和描述。

private CasUserInfo parseCasXml(String xml) { DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); factory.setNamespaceAware(true); // 关键:CAS XML 带命名空间 Document doc = factory.newDocumentBuilder() .parse(new ByteArrayInputStream(xml.getBytes(StandardCharsets.UTF_8))); XPath xPath = XPathFactory.newInstance().newXPath(); // 取认证失败信息 String failure = xPath.evaluate("/cas:serviceResponse/cas:authenticationFailure/@code", doc); if (StringUtils.hasText(failure)) { String desc = xPath.evaluate("/cas:serviceResponse/cas:authenticationFailure", doc); throw new CasAuthException(failure, desc); } String username = xPath.evaluate("/cas:serviceResponse/cas:authenticationSuccess/cas:user", doc); // 可以继续取 nicename、email 等属性 return new CasUserInfo(username); }

这里要特别提醒 namespace 的问题。CAS 的 XML 里节点都带cas前缀,如果你用普通的方式解析,或者只写//user,大概率解析不出来。属性节点还要注意区分命名空间和本地名。

还有一个容易被忽略的点:验票必须放到后端,并且要设置 HTTP 超时。CAS 服务端如果出现瞬时抖动,你的接口不能跟着挂,超时建议 3 到 5 秒,失败返回明确错误码,让前端提示“登录服务暂不可用,请稍后重试”,而不是白屏或者无限转圈。

3.2 前端实现:登录跳转、ticket 捕获与后续请求携带

前端实现我以 Vue 为例,React 思路完全一样。首先配置常量:

const CAS_LOGIN_URL = 'https://cas.example.com/login' const SERVICE_URL = 'https://app.example.com/cas/callback'

注意:这里的SERVICE_URL是固定写死的一个前端回调地址,不要动态传window.location.href,虽然这样看起来更灵活,但很容易出现http与https混用、末尾多一个斜杠之类的问题,导致 CAS 服务端校验不通过。固定地址最好维护。

路由守卫里判断登录态:

router.beforeEach((to, from, next) => { if (to.meta.public) { next() return } if (!authService.isLoggedIn()) { // 保存用户想访问的路径 sessionStorage.setItem('redirect_uri', to.fullPath) // 跳转 CAS 登录 window.location.href = `${CAS_LOGIN_URL}?service=${encodeURIComponent(SERVICE_URL)}` return } next() })

需要一个回调路由/cas/callback,这个页面从 URL 里取 ticket,然后调后端接口:

async function handleCallback() { const query = router.currentRoute.value.query const ticket = query.ticket if (!ticket) { // 没有 ticket 说明登录未完成或 CAS 异常 window.location.href = '/login' return } try { const resp = await authApi.validateTicket(ticket) authService.setUser(resp.data) const redirectUri = sessionStorage.getItem('redirect_uri') || '/dashboard' sessionStorage.removeItem('redirect_uri') router.replace(redirectUri) } catch (e) { // 校验失败,跳回登录页或展示错误 router.replace('/login?auth=failed') } }

这里有两个坑是很多人踩过的。第一个,前端路由必须用 history 模式而不是 hash 模式。hash 模式下 URL 是app.example.com/#/cas/callback?ticket=ST-xxx,整个#后面的内容不会发到服务器,但关键是你在 JS 里通过window.location.href跳转时,CAS 服务端重定向回来的ticket参数如果落在了 hash 后面,解析时极易出错。第二个,取完 ticket 后不要让页面刷新,否则同一张 ST 会被二次使用,直接失效。

axios 实例的配置也必须提前做好:

const http = axios.create({ baseURL: '/api', timeout: 8000, withCredentials: true // 跨域携带 Cookie 的关键 })

3.3 CORS、Cookie、Session 的跨域统一配置

跨域这块是最容易写崩的部分,我决定用一张表格把常见部署方式下的配置组合列清楚。

部署架构示例跨域级别Cookie 是否自动带推荐配置
同域名不同端口app.example.com:8081→api.example.com:8080cross-origin,但不跨站浏览器默认会带 Cookie(同站)只配 CORS,Cookie 走默认 SameSite=Lax 即可
同主域不同子域www.example.com→api.example.com跨域且跨站Cookie 默认不带后端设Domain=example.com,配置 CORS
完全不同域名front.com→api.com跨域且跨站Cookie 默认不带SameSite=None; Secure+ CORS,或干脆反代同源

后端的基础 CORS 配置如下:

@Configuration public class CorsConfig { @Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); // 千万不要用 setAllowedOrigins("*"),配合 allowCredentials 会直接报错 config.setAllowedOriginPatterns(Collections.singletonList("*")); config.setAllowCredentials(true); config.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS")); config.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type", "X-Requested-With")); config.setExposedHeaders(Collections.singletonList("Set-Cookie")); config.setMaxAge(3600L); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); } }

记得处理预检请求 OPTIONS。如果你用了 Spring Security,要在配置中开.cors()并保证 CorsFilter 排在认证过滤器前面,否则 OPTIONS 请求会被 401 拦下,浏览器就报“preflight request doesn’t pass access control check”。排查这个问题时看 Network 面板,如果 OPTIONS 请求返回的是 401,八成就是过滤器顺序问题。

再到 Cookie 层面。如果你选择最省事的方式——前端通过 Nginx 反向代理把/api路径转发到后端服务,让浏览器请求的始终是同一个域,那么 Cookie 的 SameSite 就不用动,也不需要Secure。我强烈建议能用反代就用反代,生产环境的同域方案比跨域方案稳定得多。

如果是同主域不同子域,就要在后端配置 Cookie 的 Domain:

server: servlet: session: cookie: name: BIZ_SESSION domain: .example.com http-only: true

如果前后端确实要跨到完全不同的域名,那么SameSite=None和Secure一个都不能少:

server: servlet: session: cookie: same-site: none secure: true

SameSite=None强制要求Secure,这意味着你的站点必须跑在 HTTPS 下。很多人本地联调时把同 site 配成 none,然后在 HTTP 环境下发现 Cookie 完全种不上,就是这个原因。本地联调最稳妥的方式是改前端 host 映射,把app.example.com和api.example.com都指向127.0.0.1,再按同主域方案配置。

3.4 单点登出与多端会话清理

CAS 单点登录做完之后,登出这步很多人都做得粗糙,导致用户明明在业务系统点了退出,刷新一下居然还保持着登录态。要么是把业务系统 Session 清了但 CAS 服务端还有登录态,再访问别的系统照样能自动登录;要么是业务系统 Session 没清理干净,前端登出了,后端接口还能被调用。

主动登出逻辑应该是这样的:

  1. 前端调用/api/auth/logout,后端清掉自己的 Session。
  2. 前端再跳转到 CAS 登出地址:cas.example.com/logout?service=https://app.example.com/login。
  3. CAS 清掉 SSO 会话,重定向回service参数指定的地址,前端回到登录页。

这步不难,难的是被动单点登出。用户在应用 A 点了登出,CAS 服务端会向所有注册了登出回调的接入系统发送通知。后端需要暴露一个匿名可访问的接口接收这个通知,并根据通知里的 session 标识把业务系统 Session 销毁。CAS 规则里,这种通知是服务端发起的 HTTP 回调,不是在浏览器里发生的,所以要单独处理安全校验,防止别人伪造登出请求。最简单的校验方式是验证来源 IP 固定配置为 CAS 服务端地址,再配合一个内部共享密钥做签名头校验。

如果你们的业务系统有多个客户端同时使用同一个用户账号,单点登出时要特别注意“主会话”的概念。用户在业务系统里点登出,清的是他自己的会话,CAS 服务端登出后,其他系统可能正让同一个用户继续工作,全部被动登出反而未必符合业务预期。我只建议对“高安全级别系统”的登录做全局登出,普通内部系统保持轻量处理即可。

4. 常见问题与排查实录

4.1 ticket 校验一直失败:service 参数从未被正确对待

我观察下来,ticket 校验失败的大头永远是 service 不匹配。常见表现是 CAS 返回的 XML 里有一行authenticationFailure,code 是INVALID_SERVICE。排查方法分三步:

第一步,对比 CAS 服务端配置里的 service 注册地址和你在代码里定义的前端回调地址是否完全一致。这里说的完全一致,包括https还是http、有没有端口、路径大小写、末尾有没有斜杠。多一个斜杠就失败。第二步,检查重定向回来时浏览器地址栏里的 URL 是否符合预期,如果前端用了路由嵌套或者代理,实际重定向地址很容易被改掉。第三步,在 CAS 服务端添加管理员日志检查,一般会有明文显示它要求匹配的 service 是什么。

还有一个小坑是 ticket 只能认证一次。如果你后端接口因为重试机制连续调了两次serviceValidate,第一次成功,第二次就返回INVALID_TICKET。前端如果做了自动重试,要格外小心。

4.2 前端请求没有带上 Cookie:明明后端 set-cookie 了

这个问题的典型表现是,Network 面板里能看到后端接口返回了Set-Cookie,但后续请求的 Request Headers 里没有Cookie。我总结的原因是“三层缺一不可”:

  • axios 实例没有设withCredentials: true,或 fetch 没加credentials: 'include'。
  • 后端 CORS 没有allowCredentials(true),而且allowedOrigins误用了通配符。
  • Cookie 本身不满足浏览器的接收条件,比如SameSite=None但没Secure,或者Domain配置不对。

排查这个问题的建议是用 curl 先测一下后端接口能否正常 set-cookie,排除后端问题。然后在浏览器里直接输入后端地址访问一次,看会不会种 Cookie。如果会,那问题就在跨域配置上;如果不会,就是 Domain 和 SameSite 的问题。

4.3 登录成功刷新页面马上又掉线

登录成功之后用户一刷新就回到登录页,多数情况下不是 CAS 的问题,而是业务系统会话没有正确建立或者前端存不住登录态。

先确认后端 Session 是否真的创建成功了。如果你用的是分布式 Session,检查 Redis 里有没有对应 key;如果是单机 Session,检查 Set-Cookie 里有没有 JSESSIONID 或者其他业务 Session 名字。如果没有,最常见的原因是你登录成功后的响应里没有真正走request.getSession(true),或者 CORS 配置里exposedHeaders没有暴露Set-Cookie。

还有一个隐蔽问题:前端刷新时,携带 Cookie 的请求是否发到了你 Session 所在的服务实例。多实例部署时,如果 Session 没放 Redis,刷新后请求被负载均衡打到另一个实例,就发现 Session 不存在,用户直接被弹回登录页。这种情况配置spring.session.store-type=redis就能解决。

4.4 开放重定向与 ST 复用:安全红线

最后讲讲安全。前后端分离项目里,service 参数的控制权在前端,你可能觉得这是“自家前端”,不值得防范,但实际业务系统里回调地址很容易被人拼参数或构造畸形链接。

后端必须维护一个允许跳转的 service 白名单,凡是匹配不到的地址一律拒绝。同时 ST 校验成功之后要在会话或缓存里标记该 ticket 已使用,同一张 ST 第二次校验直接拒绝。这个和 CAS 协议本身的一致性逻辑是互补的,可以防住一部分中间人重放。还要提醒前端:不要在浏览器 URL 上明文打印 ticket,也不要把它存进 localStorage,票据是一次性的,用完就应该从地址栏清掉。

还有一点容易被忽略,后端日志里往往会记录请求参数。如果日志框架把 query 原样打出来,ticket 就会留在日志文件里。建议对包含 ticket 参数的数据做脱敏,线上日志级别也别开 DEBUG。

5. 写在最后的个人体会

整套方案落地之后,我最深的感受是:CAS 本身不难,难的是“前后端分离”这四个字带来的会话模型变化。只要是前后端分离,你早晚会遇到 Cookie 跨域、Filter 拦截冲突、SPA 路由跟服务端重定向打架这些问题。与其去搜各种玄学方案,不如回过头把协议本身吃透,再去配置 CORS 和 Cookie。

如果你现在可以直接控制部署拓扑,我强烈建议首选 Nginx 反向代理同源方案,把跨域问题从根上消灭掉。如果你要做一个多端(Web、小程序、App)都能用的系统,那就别用 Session 存登录态了,可以考虑 CAS 认证成功之后换发一个 JWT,后续所有接口都走无状态 Token 认证,这样跨域 Cookie 的整个复杂度都会消失。这个方案我在另一个项目里验证过,整体接入体验比 Session 方案顺滑得多,但要注意 JWT 的续期和注销策略要单独设计。

如果你正在接 CAS,希望这篇文章能帮你少走弯路。配置上如果有不懂的地方,照着后面的排查表一项一项对,比盲目改配置高效得多。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进