《SSO CAS单点系列》之 支持Web应用跨域登录CAS
前面我们介绍的SSO,无论是CAS还是我们自主开发的Nebula,都有一个共同的特点,就是应用系统需要登录时,都先重定向到认证服务器进行登录。也就是说系统需要从一个应用先跳到另一个应用,我们看阿里的单点登录就是这么做的。
但有时候,我们想进一步增加用户体验,并不希望用户离开原应用页面,在原有页面基础上进行登录,让用户感受不到认证中心的存在,能不能做到呢?回答是肯定的,大家看下新浪的单点登录方式,就是这么做的。
在原有应用系统页面进行登录认证中心,如不发生跳转,我们需要使用Ajax方式。而最常用的XMLHttpRequest Ajax方式调用,存在一个跨域问题,即为了安全,Ajax本身是不允许跨域调用的。这也就是为什么单点登录常规做法是重定向到认证中心去登录,然后再重定向回系统应用的原因。(而且为了安全,CAS本身也不提倡跨域远程登录)
在应用页面中,如何达到远程登录CAS的效果?摆在我们面前有两道坎儿需要克服:
首先是远程获取lt和execution参数值问题。前面我们介绍过,CAS登录的form提交不仅有username和password两个参数,还包括lt和execution,lt防止重复提交,execution保证走的同一个webflow流程。在进行远程提交时,我们需要远程得到CAS动态产生的这两个参数,从而保证能够向CAS进行正确form提交。
XMLHttpRequest Ajax不能使用,可以采用另外一种方式,即JSONP。JSONP使用了script标签可以跨域访问其它网站资源的特性,巧妙地返回一段js回调方法代码,通过执行这个回调方法,达到了传递跨域调用数据的目的。
第二个坎儿是如何在本页面跨域提交form请求。我们能不能也用JSONP方法呢?很遗憾,不行!JSONP提供的是get方式,而我们提交的form是post方式。我们可以使用另外一种ajax技术来解决,iframe。iframe可以加载和操作其它域的资源,根据用户提交的username和password,以及前面获取的lt和execution,在iframe中提交登录form参数,完成登录。
主页面如何获取iframe提交返回的信息?可以修改CAS的登录流程,让其在远程登录的情况下,将出错信息以参数的方式重定向回应用系统服务端,应用系统再以调用父页面js函数方法,将出错信息通过参数传递给父页面。
从上面思路可以看出,我们并没有让CAS增加远程登录的功能,CAS登录,还是需要在CAS所在域下登录。我们只是利用iframe方法,让应用系统达到和远程登录一样的用户体验效果。而实现这一效果的关键,是应用登录页对lt和execution动态参数以及CAS登录反馈信息的捕获。
下面我们就按照上面思路介绍具体开发方法:
1.改造login-webflow.xml,增加支持跨域远程登录处理流程分支。
前面我们已经了解,登录流程的控制是在login-webflow.xml中,我们对它进行改造。改造原则是不修改原代码,在原有登录处理流程的基础上,增加一种新情况的处理,即支持跨域远程登录处理。
在流程初始化处理完成后,我们增加一个新的节点mode,它首先来检查登录请求中是否包含一个变量mode,并且变量的值为rlogin。如果没有,就继续走原常规流程。如果有,说明是跨域远程登录情况。<on-start> 后加入如下分支流程定义:
<action-state id="mode">
<evaluate expression="modeCheckAction.check(flowRequestContext)"/>
<transition on="rlogin" to="serviceAuthorizationCheckR" />
<transition on="normal" to="ticketGrantingTicketCheck" />
</action-state>
<action-state id="serviceAuthorizationCheckR">
<evaluate expression="serviceAuthorizationCheck"/>
<transition to="generateLoginTicketR"/>
</action-state>
<action-state id="generateLoginTicketR">
<evaluate expression="generateLoginTicketAction.generate
(flowRequestContext)" />
<transition on="generated" to="rLoginTicket" />
</action-state>
<view-state id="rLoginTicket" view="rLoginTicket" model="credential">
<binder>
<binding property="username" required="true" />
<binding property="password" required="true"/>
</binder>
<on-entry>
<set name="viewScope.commandName" value="'credential'" />
</on-entry>
<transition on="submit" bind="true" validate="true"
to="realSubmitWithRLogin">
<evaluate expression="authenticationViaRFormAction.doBind
(flowRequestContext, flowScope.credential)" />
</transition>
</view-state>
<action-state id="realSubmitWithRLogin">
<evaluate expression="authenticationViaRFormAction.submit(flowRequestContext,
flowScope.credential, messageContext)" />
<transition on="success" to="sendTicketGrantingTicketR" />
</action-state>
<action-state id="sendTicketGrantingTicketR">
<evaluate expression="sendTicketGrantingTicketAction" />
<transition on="success" to="rLoginRes" />
</action-state>
<end-state id="rLoginRes"