CAS 5.x环境下Shiro+Pac4j Ajax跨客户端访问返回401问题求助
这个问题我之前在CAS集成的项目里踩过坑,太懂这种头疼的感觉了!核心原因就是Ajax请求没法像浏览器页面跳转那样处理CAS的重定向逻辑——当CAS检测到请求没有有效票据时,会返回302跳转到登录页,但Ajax会直接把这个重定向当成失败请求,最终返回401。下面给你几个实用的解决思路:
这是最贴合CAS SSO流程的方案,思路是让CAS Server判断请求是否为Ajax,若是则返回401+JSON提示,而非302重定向,再由前端处理跳转逻辑。
CAS Server端配置:
自定义一个AuthenticationEntryPoint或者修改Webflow配置,检测请求头里的X-Requested-With: XMLHttpRequest。如果是Ajax请求,就返回401 Unauthorized状态码,并附带JSON格式的响应体,比如:{"status": "unauthorized", "redirectUrl": "https://your-cas-server/login?service=xxx"}具体实现可以通过CAS的扩展机制,比如在Spring环境下,重写
CasAuthenticationEntryPoint的commence方法,增加请求头判断逻辑。前端Ajax全局拦截:
在client-1的前端代码里添加全局Ajax错误拦截,捕获401响应后手动跳转到CAS登录页。用jQuery的话可以这么写:$(document).ajaxError(function(event, xhr, settings) { if (xhr.status === 401) { // 可以从响应体里取redirectUrl,或者直接拼接CAS登录地址和当前请求的service参数 const loginUrl = "https://your-cas-server/login?service=" + encodeURIComponent(settings.url); window.location.href = loginUrl; } });这样用户登录成功后,会自动回到原请求的资源页面,体验更流畅。
如果不想修改CAS Server的配置,可以通过CAS的REST接口提前获取ST(Service Ticket),然后在Ajax请求里带上这个票据。
步骤说明:
- 先调用CAS的
/v1/tickets接口获取TGT(Ticket Granting Ticket),需要带上用户名和密码。 - 再用TGT调用
/v1/tickets/{TGT}接口获取针对client-2的ST。 - 在Ajax请求的URL后拼接
?ticket=ST-xxxxxx,或者把ST放在请求头里。
- 先调用CAS的
修改后的Ajax代码示例:
function test() { // 假设已经通过REST接口获取到了有效的ST const serviceTicket = "ST-123456-abcdef-your-cas-server"; jQuery.ajax({ url: `http://192.168.0.14:8445/client-2/user/userInfo?ticket=${serviceTicket}`, headers: {'X-Requested-With': 'XMLHttpRequest'}, success: function(res) { // 处理返回结果 } }); }注意:这种方式需要处理ST的有效期,另外如果client-1和client-2跨域,要确保CAS允许跨域请求,或者通过后端代理来避免跨域问题。
如果client-1和client-2是同域,或者已经配置了共享会话(比如用Redis共享Session),可以让Ajax请求携带Cookie,让CAS客户端识别已登录状态。
修改Ajax代码:
添加withCredentials: true和crossDomain: true配置:function test() { jQuery.ajax({ url: "http://192.168.0.14:8445/client-2/user/userInfo", headers: {'X-Requested-With': 'XMLHttpRequest'}, xhrFields: { withCredentials: true }, crossDomain: true, success: function(res) { // 处理返回结果 } }); }client-2端配置CORS:
要确保client-2允许来自client-1的跨域请求并支持携带凭证,比如在Spring Boot项目里可以配置CorsFilter:@Bean public CorsFilter corsFilter() { CorsConfiguration config = new CorsConfiguration(); // 允许client-1的域名 config.setAllowedOrigins(Arrays.asList("http://your-client-1-domain")); config.setAllowCredentials(true); config.addAllowedMethod("*"); config.addAllowedHeader("*"); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return new CorsFilter(source); }
个人最推荐方案1,因为它完全适配CAS的SSO逻辑,用户体验也更自然,不需要额外处理票据的生命周期问题。
内容的提问来源于stack exchange,提问作者frank

