【SSO单点系列】(2):CAS4.0 之 跨域 Ajax 登录实践

CAS4.0 之 跨域 Ajax 登录实践

一、问题描述

CAS实现单点 实现一处登录 可访问多个应用 。 但是原登录是CAS默认登录页面和登出页面是无法重定向到自定义页面的   此处使用Ajax+Iframe 的方法来实现自定义页面跨域提交登录。

 

二、问题分析

     CAS在登录认证时主要参数说明:
              service         [OPTIONAL] 登录成功后重定向的URL地址;
              username    [REQUIRED] 登录用户名;
              password    [REQUIRED] 登录密码;
              lt                    [REQUIRED] 登录令牌;
       主要有四个参数,其中的三个参数倒好说,最关键的就是 lt , 据官方说明该参数是login ticket id, 主要是在登录前产生的一个唯一的“登录门票”,然后提交登录后会先取得"门票",确定其有效性后才进行用户名和密码的校验,否则直接重定向至 cas/login 页。
       于是,便打开CAS-Server的登录页,发现其每次刷新都会产生一个 lt, 其实就是 Spring WebFlow 中的 flowExecutionKey值。 那么问题的关键就在于在子系统中如何获取 lt 也就是登录的ticket?

三、 可能的解决方案

 一般对于获取登录ticket的解决方案可能大多数人都会提到两种方法:
   

AJAX:  熟悉 Ajax 的可能都知道,它的请求方式是严格按照沙箱安全模型机制的,严格情况下会存在跨域安全问题。

IFrames: 这也是早期的 ajax 实现方式,在页面中嵌入一个隐藏的IFrame,然后通过表单提交到该iframe来实现不刷新提交,不过使用这种方式同样会带来两个问题:

登录成功之后如何摆脱登录后的IFrame呢?如果成功登录可能会导致整个页面重定向,当然你能在form中使用属性 target="_parent",使之弹出,那么你如何在父页面显示错误信息呢?
 b.  你可能会受到布局的限止(不允许或不支持iframe)  对于以上两种方案,并非说不能实现,只是说对于一个灵活的登录系统来说仍然还是会存在一定的局限性的,我们坚信能有更好的方案来解决这个问题。

四、 通过JS重定向来获取login ticket (lt)

当第一次进入子系统的登录页时,通过 JS 进行redirect到cas/login?get-lt=true获取login ticket,然后在该login中的 flow 中检查是否包含get-lt=true的参数,如果是的话则跳转到lt生成页,生成后,并将lt作为该redirect url 中的参数连接,如 remote-login.html?lt=e1s1,然后子系统再通过JS解析当前URL并从参数中取得该lt的值放置登录表单中,即完成 lt 的获取工作。其中进行了两次 redirect 的操作。

 

五、实现

1 、客户端iframe提交代码

<form action="http://www.myCas.com:18080/login" method="post"
   onsubmit="return loginValidate();" target="ssoLoginFrame">
   <ul>
      <span class="red" style="height:12px;" id="J_ErrorMsg"></span>

      <li><em>用户名:</em> <input name="username" id="J_Username" value="2"
         type="text" autocomplete="off" class="line" style="width: 180px" />
      </li>
      <li><em>密 码:</em> <input name="password" type="password" value="2"
         id="J_Password" class="line" style="width: 180px" /></li>

      <li class="mai"><em>&nbsp;</em> <input type="checkbox"
         name="rememberMe" id="rememberMe" value="true" /> &nbsp;自动登录 <a
         href="/retrieve">忘记密码?</a></li>
      <li><em>&nbsp;</em> 
      isajax:<input type="text" name="isajax" value="true" /> 
      isframe:<input type="text" name="isframe" value="true" />
         lt:<input type="text" name="lt" value="" id="J_LoginTicket">
       execution: <input type="text" name="execution" id="execution"  value="">
         _eventId:<input type="text" name="_eventId" value="submit" />
          <input name="" type="submit" value="登录" class="loginbanner" /> 
          ticket:<input type="text" name="ticket" value="" id="ticket">
           <input type="hidden" name="loginUrl" value="http://www.myApp1.com:8080/test.jsp" />
          </li>
   </ul>
   
</form>
 <a href="javascript:void(0)" class="easyui-linkbutton" onClick="checkForLoginTicket()">单点登录</a> 
</div>
   <script>


   $(document).ready(function() {  
      checkForLoginTicket();
   });
    var myCas = 'http://www.myCas.com:18080';
        var myApp1 = 'http://ciat.padx.cn:8080';

   
       var loginTicket;
   function checkForLoginTicket() {
      var loginTicketProvided = false;
      var query = '';
      casLoginURL = myCas+'/login';
      thisPageURL = myApp1+'/test.jsp?&n='
               + new Date().getTime(); 
      thisPageURL2 = myApp1+'/user-center.action'    ;
      
      casLoginURL += '?login-at=' + encodeURIComponent(thisPageURL)+'&service=' + encodeURIComponent(thisPageURL2);
      query = window.location.search;
      queryquery = query.substr(1);

      var param = new Array(); 
      var temp = new Array();
      param = query.split('&');

      i = 0;
      // 开始获取当前 url 的参数,获到 lt 和 error_message。  
      while (param[i]) { 
         temp = param[i].split('=');
          
         if (temp[0] == 'lt') {
            loginTicket = temp[1];
             $('#J_LoginTicket').val(loginTicket); 
            loginTicketProvided = true;
         }
          if (temp[0] == '?ticket') { 
            loginTicketProvided = true;
            $('#ticket').val(temp[1] ); 
         }
          if (temp[0] == 'execution') {  
            $('#execution').val(temp[1] ); 
         }
          
         if (temp[0] == 'error_message') {
            error = temp[1];
         }
         i++;
      } 
      // 判断是否已经获取到 lt 参数,如果未获取到则跳转至 cas/login 页,并且带上请求参数  get-lt=true。 第一次进该页面时会进行一次跳转  
      if (!loginTicketProvided) {
          location.href = casLoginURL + '&get-lt=true';
      }
   }

   //--------------------

   // 登录验证函数, 由 onsubmit 事件触发  
   var loginValidate = function() {
      var msg;
      if ($.trim($('#J_Username').val()).length == 0) {
         msg = "用户名不能为空。";
      } else if ($.trim($('#J_Password').val()).length == 0) {
         msg = "密码不能为空。";
      }
      if (msg && msg.length > 0) {
         $('#J_ErrorMsg').fadeOut().text(msg).fadeIn();
         return false;
         // Can't request the login ticket.  
      } else if ($('#J_LoginTicket').val().length == 0) {
      // $('#J_ErrorMsg').text('服务器正忙,请稍后再试..');
      // return false;
      } else {
         // 验证成功后,动态创建用于提交登录的 iframe  
         $('body').append($('<iframe/>').attr({
            style : "display:none;width:0;height:0",
            id : "ssoLoginFrame",
            name : "ssoLoginFrame",
            src : "javascript:false;"
         }));
         return true;
      }
   } 
</script> 
View Code

 

2、客户端web.xml

!--单点退出配置-->
<!--用于单点退出,该过滤器用于实现单点登出功能,可选配置 -->
<listener>
    <listener-class>org.jasig.cas.client.session.SingleSignOutHttpSessionListener</listener-class>
</listener>

<!--该过滤器用于实现单点登出功能,可选配置。 -->
<filter>
    <filter-name>CASSingle Sign OutFilter</filter-name>
    <filter-class>org.jasig.cas.client.session.SingleSignOutFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CASSingle Sign OutFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<filter>
    <filter-name>CASFilter</filter-name>
    <filter-class>org.jasig.cas.client.authentication.AuthenticationFilter</filter-class>
    <init-param>
        <param-name>casServerLoginUrl</param-name>
        <param-value>http://www.myCas.com:18080/login</param-value>   
    </init-param>
    <init-param>
        <param-name>serverName</param-name>
        <param-value>http://ciat.padx.cn:8080</param-value>
    </init-param>
</filter>

<filter-mapping>
    <filter-name>CASFilter</filter-name>
    <url-pattern>/user-center.action</url-pattern>
    <url-pattern>/user-center!validate2.action</url-pattern>
</filter-mapping>


<!--该过滤器负责对Ticket的校验工作,必须启用它 -->
<filter>
    <filter-name>CASValidationFilter</filter-name>
    <filter-class>
        org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter
    </filter-class>
    <init-param>
        <param-name>casServerUrlPrefix</param-name>
        <param-value>http://www.myCas.com:18080</param-value>
    </init-param>
    <init-param>
        <param-name>serverName</param-name>
        <param-value>http://ciat.padx.cn:8080</param-value>
    </init-param>
</filter>
<filter-mapping>
    <filter-name>CASValidationFilter</filter-name>
    <url-pattern>/user-center.action</url-pattern>
</filter-mapping>


<!-- 该过滤器负责实现HttpServletRequest请求的包裹, 比如允许开发者通过HttpServletRequest的getRemoteUser()方法获得SSO登录用户的登录名,可选配置。 -->
<filter>
    <filter-name>CASHttpServletRequest WrapperFilter</filter-name>
    <filter-class>
        org.jasig.cas.client.util.HttpServletRequestWrapperFilter
    </filter-class>
</filter>
<filter-mapping>
    <filter-name>CASHttpServletRequest WrapperFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
<!-- 该过滤器使得开发者可以通过org.jasig.cas.client.util.AssertionHolder来获取用户的登录名。 比如AssertionHolder.getAssertion().getPrincipal().getName()。 -->
<filter>
    <filter-name>CASAssertion Thread LocalFilter</filter-name>
    <filter-class>org.jasig.cas.client.util.AssertionThreadLocalFilter</filter-class>
</filter>
<filter-mapping>
    <filter-name>CASAssertion Thread LocalFilter</filter-name>
    <url-pattern>/*</url-pattern>
</filter-mapping>
View Code

 

3、服务端

向服务端发送请求为3 个部分:

1、  请示页面获取lt登录页面的密钥

2、  发送用户名密码

3、  返回服务端发来的ST在确认是否成功登录

服务端修改login-webflow.xml(添加)

<!-- 添加如下配置 :-->
<action-state id="provideLoginTicket">
    <evaluate expression="provideLoginTicketAction"/>
    <transition on="loginTicketRequested" to ="ajaxgenerateLoginTicket" />
    <transition on="continue" to="generateLoginTicket" />
    <transition on="newapp" to="generateServiceTicket" />
</action-state> 
<view-state id="viewRedirectToRequestor" view="casRedirectToRequestorView" model="credential">
    <binder>
        <binding property="username" />
        <binding property="password" />
    </binder>
    <on-entry>
        <set name="viewScope.commandName" value="'credential'" />
    </on-entry>
    <transition on="submit" bind="true" validate="true" to="realSubmit">
        <evaluate expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credential)" />
    </transition>
</view-state>

<action-state id="ajaxgenerateLoginTicket">
    <evaluate expression="generateLoginTicketAction.generate(flowRequestContext)" />
    <transition on="generated" to="viewRedirectToRequestor" />
</action-state>
<!--  添加结束处-->
View Code

 

添加ProvideLoginTicketAction.java

 1 public class ProvideLoginTicketAction  extends AbstractAction {
 2 
 3     @Override
 4     protected Event doExecute(RequestContext context) throws Exception {
 5         final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
 6         final Service service = WebUtils.getService(context);
 7         final String ticketGrantingTicket = WebUtils.getTicketGrantingTicketId(context); 
 8         if(ticketGrantingTicket!=null){
 9             return result("newapp");
10         }
11         if (request.getParameter("get-lt") != null && request.getParameter("get-lt").equalsIgnoreCase("true")) {
12             return result("loginTicketRequested");
13         }
14         return result("continue");
15     }
16 
17 }
View Code

default_views.properties

casRedirectToRequestorView.(class)=org.springframework.web.servlet.view.JstlView
casRedirectToRequestorView.url=/WEB-INF/view/jsp/default/ui/viewRedirectToRequestor.jsp

添加viewRedirectToRequestor.jsp

 1 <%@ page contentType="text/html; charset=UTF-8"%>
 2 <%@ page import="org.jasig.cas.util.CasUtility"%>
 3 <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
 4 <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
 5 <%
 6   String separator = "";
 7   // 需要输入 login-at 参数,当生成lt后或登录失败后则重新跳转至 原登录页,并传入参数 lt 和 error_message
 8   String referer = request.getParameter("login-at");
 9 
10   referer = CasUtility.resetUrl(referer);
11   if (referer != null && referer.length() > 0) {
12     separator = (referer.indexOf("?") > -1) ? "&" : "?";
13 %>
14 <html>
15 <title>cas get login ticket</title>
16 <head>
17   <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
18   <script>
19     var redirectURL = "<%=referer + separator%>lt=${loginTicket}&execution=${flowExecutionKey}";
20     window.location.href = redirectURL;
21   </script>
22 </head>
23 <body></body>
24 </html>
25 <%
26 } else {
27 %>
28 <%
29   }
30 %>
View Code

服务端在获取请求时会进入ProvideLoginTicketAction.java进行判断传入参数get-lt

值为false不是ajax在请求获取lt值,那么按原流程走

值为true 生成lt 并 进入自定义返回页面viewRedirectToRequestor.jsp 回传

 

以上代码虽已可以成功登录但是客户端只有iframe里的内容显示已成功 iframe外需要刷新页面才可以,下面实现自动刷新

 1 <action-state id="generateServiceTicket">
 2        <evaluate expression="generateServiceTicketAction" />
 3    <!--<transition on="success" to ="warn" /> -->
 4        <transition on="success" to="loginResponse" />
 5        <transition on="authenticationFailure" to="handleAuthenticationFailure" />
 6        <transition on="error" to="generateLoginTicket" />
 7    <transition on="gateway" to="gatewayServicesManagementCheck" />
 8 </action-state>
 9 
10    <action-state id="loginResponse">
11        <evaluate expression="ajaxLoginServiceTicketAction" />
12        <!--非ajax/iframe方式登录,采取原流程处理 -->
13        <transition on="success" to="warn" />
14        <transition on="error" to="generateLoginTicket" />
15        <!-- 反之,则进入 viewAjaxLoginView 页面 -->
16        <transition on="local" to="viewAjaxLoginView" />
17    </action-state>
View Code

generateServiceTicket内的返回success修改为loginResponse 并 新增loginResponse内容

添加AjaxLoginServiceTicketAction.java

 1 public class AjaxLoginServiceTicketAction extends AbstractAction {
 2 
 3     // The default call back function name.
 4     protected static final String J_CALLBACK = "feedBackUrlCallBack";
 5 
 6     protected Event doExecute(final RequestContext context) {
 7         HttpServletRequest request = WebUtils.getHttpServletRequest(context);
 8         Event event = context.getCurrentEvent();
 9         boolean isAjax = BooleanUtils.toBoolean(request.getParameter("isajax"));
10 
11         if (!isAjax){  // 非 ajax/iframe 方式登录,返回当前 event.
12             return event;
13         }
14         boolean isLoginSuccess;
15         // Login Successful.
16         if ("success".equals(event.getId())){ //是否登录成功
17             final Service service = WebUtils.getService(context);
18             final String serviceTicket = WebUtils.getServiceTicketFromRequestScope(context);
19             if (service != null){  //设置登录成功之后 跳转的地址
20                 request.setAttribute("service", service.getId());
21             }
22             request.setAttribute("ticket", serviceTicket);
23             isLoginSuccess = true;
24         } else { // Login Fails..
25             isLoginSuccess = false;
26         }
27 
28         boolean isFrame = BooleanUtils.toBoolean(request.getParameter("isframe"));
29         String callback = request.getParameter("callback");
30         if(StringUtils.isEmpty(callback)){ // 如果未转入 callback 参数,则采用默认 callback 函数名
31             callback = J_CALLBACK;
32         }
33         if(isFrame){ // 如果采用了 iframe ,则 concat 其 parent 。
34             callback = "parent.".concat(callback);
35         }
36         request.setAttribute("isFrame", isFrame);
37         request.setAttribute("callback", callback);
38         request.setAttribute("isLogin", isLoginSuccess);
39 
40         return new Event(this, "local"); // 转入 ajaxLogin.jsp 页面
41     }
42 
43 }
View Code

default_views.properties

1 viewAjaxLoginView.(class)=org.springframework.web.servlet.view.JstlView
2 viewAjaxLoginView.url=/WEB-INF/view/jsp/default/ui/ajaxLogin.jsp

新增ajaxLogin.jsp

 1 <%@ page contentType="text/html; charset=UTF-8"%>
 2 <html>
 3 <head>
 4   <title>正在登录....</title>
 5 </head>
 6 <body>
 7 <script type="text/javascript">
 8   <%
 9       Boolean isFrame = (Boolean)request.getAttribute("isFrame");
10       Boolean isLogin = (Boolean)request.getAttribute("isLogin");
11       // 登录成功
12       if(isLogin){
13           if(isFrame){%>
14   parent.location.replace('${service}?ticket=${ticket}')
15   <%} else{%>
16   location.replace('${service}?ticket=${ticket}')
17           <%}
18       }
19   %>
20     // 回调
21           ${callback}({'login':${isLogin ? '"success"': '"fails"'}, 'msg': ${isLogin ? '""': '"用户名或密码错误!"'}})
22 </script>
23 </body>
24 </html>
View Code

 

posted @ 2016-04-21 17:08  打不死的小强hedgehog  阅读(4717)  评论(0编辑  收藏  举报