版权所有,转载请注明来源http://gogo1217.iteye.com,违者必究!
最早接触 CAS 大概是 2011 年,现在发展到 5.x 了。相比较那时候的版本。比较有意思的特性有:
服务端:
1. 使用 spring-boot 开发
2. 推荐使用 overlay 的方式对服务端进行修改,这个大大的赞。虽然我接触 CAS 的第二年就是这么干的 ...
客户端 3.5.0 版本:
1、默认强制获取 session。
2、增加了 authenticationRedirectStrategyClass ,使得能自定义没有登录时选择重定向还是返回地址交给浏览器去决定。
CAS 单点登录的原理不再赘述,这里重点阐述下 AJAX 请求 CAS Client 资源发生重定向的解决方案。
问题描述
假定,我们存在以下服务:
1、CAS 服务端 Server ;
2、CAS 客户端 Client-A、Client-B,集成了 CAS 客户端;
假定 Client-A 有个页面 Page-A,使用 AJAX 调用了 Client-B 的接口 API-B。当用户访问 Page-A 时,分为以下 3 种情况:
一):没有登录过单点服务
1、API-B 发现没有登录,通知浏览器重定向到 Server
3、由于 AJAX 不能重定向,因此请求无法继续
二):Server端已登陆,但 Client-B 还未登录
1、API-B 发现没有登录,通知浏览器重定向到 Server
3、由于 AJAX 不能重定向,因此请求无法继续
三):Client-B 登录过单点服务
1、API-B 发现已经登录,返回正确的内容
解决思路
想要解决这个问题,必须拦截所有的 AJAX 请求,根据返回判断,手动触发 CAS 的重定向。
1、首先对 Client-B (CAS 的客户端)进行改造。
如果是 AJAX 请求,则返回一个重定向的目标地址给客户端,由客户端完成URL的重定向,这里直接使用 client 3.5.0 版本,已经具备了这种接口。
authenticationRedirectStrategyClass org.jasig.cas.client.authentication.FacesCompatibleAuthenticationRedirectStrategy
这是它默认的实现,使用的是特殊参数,我们一般改为通过HTTP请求头判定比较合适。
public final class FacesCompatibleAuthenticationRedirectStrategy implements AuthenticationRedirectStrategy { private static final String FACES_PARTIAL_AJAX_PARAMETER = "javax.faces.partial.ajax"; public FacesCompatibleAuthenticationRedirectStrategy() { } public void redirect(HttpServletRequest request, HttpServletResponse response, String potentialRedirectUrl) throws IOException { String xRequestedWith = request.getHeader("X-Requested-With"); if ((CommonUtils.isNotBlank(xRequestedWith) && "XMLHttpRequest".equals(xRequestedWith.trim())) || CommonUtils.isNotBlank(request.getParameter(FACES_PARTIAL_AJAX_PARAMETER))) { response.setContentType("application/json"); response.setStatus(200); PrintWriter writer = response.getWriter(); writer.write("{\"casError\":\"403\",\"redirect\":\"" + potentialRedirectUrl + "\"}"); } else { response.sendRedirect(potentialRedirectUrl); } } }
2、对 Server (CAS 的服务端)进行改造。
a. 支持 AJAX 跨域调用:
修改 application.yml ,增加以下配置,使得 CAS 服务端支持跨域调用。
注意,如果要对CAS默认的属性进行配置,建议修改 application.yml 。
cas: httpWebRequest: cors: # cors 跨域 enabled: true allowCredentials: true allowOrigins: ["*"] allowMethods: ["*"] allowHeaders: ["*"] maxAge: 3600
b. 当 CAS 服务端已登录,而客户端没有登录,返回 ST 内容,而不是重定向。
参考:https://apereo.github.io/cas/5.0.x/protocol/CAS-Protocol-Specification.html
Use POST responses instead of redirects: https://cas.example.org/cas/login?method=POST&service=http%3A%2F%2Fwww.example.org%2Fservice
在请求中增加参数 method=POST ,CAS 服务端将以 Form 表单的方式返回,而不是直接服务器重定向。
实现步骤
有了上面的准备,我们重新梳理下 AJAX请求,需要做什么处理。
一):没有登录过单点服务
1、API-B 发现没有登录,返回一个重定向到 Server 的地址( AJAX 特殊处理,客户端改造)。
2、AJAX 获得这个地址后在 URL 后追加参数 method=POST,重新发起 AJAX 请求,请求 Server(服务端跨域请求处理)。
3、由于 Server 没有登录,服务端返回登录页面的内容。
4、AJAX 获得返回内容,发现是登录页面,提醒用户登录。
5、用户登录后,重新发起AJAX请求。
二):Server 端已登陆,但 Client-B 还未登录
1、API-B 发现没有登录,返回一个重定向到 Server 的地址( AJAX 特殊处理,客户端改造)。
2、AJAX 获得这个地址后在 URL 后追加参数 method=POST,重新发起 AJAX 请求,请求 Server(服务端跨域请求处理)。
3、由于 Server 已经登录,服务端以表单的方式返回 ST 凭证(参数 method=POST,服务端处理)。
4、AJAX 获得ST后,在最初的请求上附加 ST 凭证,重新请求 API-B。
5、Client-B 根据 ST 去 Server 获取用户信息,完成登录,并返回正确内容。
在上述环节中,如果 Server 端已经登录过,我们是可以做到完全静默请求,用户无感知的。
通过对 AJAX 的统一拦截处理,使得开发人员在开发过程中,无感知 cas。以下是 axios 的默认实现。
(function () { if (!axios) { return; } axios.interceptors.request.use((config) => { if (!config.headers['X-Requested-With']) { config.headers['X-Requested-With'] = 'XMLHttpRequest'; } return config; }); var oldRequest = axios.Axios.prototype.request; axios.Axios.prototype.request = function request(config) { var self = this; return new Promise(function (resolve, reject) { oldRequest.call(self, config).then( function (response) { if (response.data && response.data.casError == '403' && response.data.redirect) { console.log('原始请求- 发现 CAS 客户端未登录'); let url = response.data.redirect + "&method=POST"; axios.get(url, { withCredentials: true, responseType: 'document', }).then(function (ssoResponse) { var form = ssoResponse.data.getElementsByTagName('form'); if (form) { form = form[0]; if (form.getAttribute('id') === 'fm1') { console.log('获取ticket 失败 ,CAS 服务端未登录'); ssoResponse.message = '获取ticket 失败 ,CAS 服务端未登录'; reject(ssoResponse); alert('请在弹出窗口完成登录后,再进行操作'); window.open(response.data.redirect.substring(0, response.data.redirect.indexOf('?'))); } else if (form.getAttribute('name') === 'acsForm' && form.getElementsByTagName('input')) { var join = config.url.indexOf('?') > 0 ? '&' : '?'; config.url = config.url + join + 'ticket=' + form.getElementsByTagName('input')[0].value; axios.request(config).then(function (withLoginResponse) { console.log('获取ticket 成功,再次请求数据'); resolve(withLoginResponse); }).catch(function (err) { reject(err); }); } } }) } else { resolve(response); } }, function (error) { reject(error); } ); }); }; })();
完整代码:
https://github.com/gogo1217/sso-parent