【SSO單點系列】(2):CAS4.0 之 跨域 Ajax 登陸實踐

CAS4.0 之 跨域 Ajax 登陸實踐

1、問題描述

CAS實現單點 實現一處登陸 可訪問多個應用 。 可是原登陸是CAS默認登陸頁面和登出頁面是沒法重定向到自定義頁面的   此處使用Ajax+Iframe 的方法來實現自定義頁面跨域提交登陸。javascript

 

2、問題分析

     CAS在登陸認證時主要參數說明:
              service         [OPTIONAL] 登陸成功後重定向的URL地址;
              username    [REQUIRED] 登陸用戶名;
              password    [REQUIRED] 登陸密碼;
              lt                    [REQUIRED] 登陸令牌;
       主要有四個參數,其中的三個參數倒好說,最關鍵的就是 lt , 據官方說明該參數是login ticket id, 主要是在登陸前產生的一個惟一的「登陸門票」,而後提交登陸後會先取得"門票",肯定其有效性後才進行用戶名和密碼的校驗,不然直接重定向至 cas/login 頁。
       因而,便打開CAS-Server的登陸頁,發現其每次刷新都會產生一個 lt, 其實就是 Spring WebFlow 中的 flowExecutionKey值。 那麼問題的關鍵就在於在子系統中如何獲取 lt 也就是登陸的ticket?html

3、 可能的解決方案

 通常對於獲取登陸ticket的解決方案可能大多數人都會提到兩種方法:
    java

AJAX:  熟悉 Ajax 的可能都知道,它的請求方式是嚴格按照沙箱安全模型機制的,嚴格狀況下會存在跨域安全問題。 web

IFrames: 這也是早期的 ajax 實現方式,在頁面中嵌入一個隱藏的IFrame,而後經過表單提交到該iframe來實現不刷新提交,不過使用這種方式一樣會帶來兩個問題: ajax

登陸成功以後如何擺脫登陸後的IFrame呢?若是成功登陸可能會致使整個頁面重定向,固然你能在form中使用屬性 target="_parent",使之彈出,那麼你如何在父頁面顯示錯誤信息呢?
 b.  你可能會受到佈局的限止(不容許或不支持iframe)  對於以上兩種方案,並不是說不能實現,只是說對於一個靈活的登陸系統來講仍然仍是會存在必定的侷限性的,咱們堅信能有更好的方案來解決這個問題。spring

4、 經過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 的操做。express

 

5、實現 跨域

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

 

二、客戶端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 個部分:

一、  請示頁面獲取lt登陸頁面的密鑰

二、  發送用戶名密碼

三、  返回服務端發來的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
相關文章
相關標籤/搜索