cas源碼解讀

1. 動機 
      用過 CAS 的人都知道 CAS-Server端是單獨部署的,做爲一個純粹的認證中心。在用戶每次登陸時,都須要進入CAS-Server的登陸頁填寫用戶名和密碼登陸,可是若是存在多個子應用系統時,它們可能都有相應風格的登陸頁面,咱們但願直接在子系統中登陸成功,而不是每次都要跳轉到CAS的登陸頁去登陸。 

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

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

    • AJAX:  熟悉 Ajax 的可能都知道,它的請求方式是嚴格按照沙箱安全模型機制的,嚴格狀況下會存在跨域安全問題。
    • IFrames: 這也是早期的 ajax 實現方式,在頁面中嵌入一個隱藏的IFrame,而後經過表單提交到該iframe來實現不刷新提交,不過使用這種方式一樣會帶來兩個問題:
    •                    a.  登陸成功以後如何擺脫登陸後的IFrame呢?若是成功登陸可能會致使整個頁面重定向,固然你能在form中使  

 

                              用屬性target="_parent",使之彈出,那麼你如何在父頁面顯示錯誤信息呢? 

 

                         b.  你可能會受到佈局的限止(不容許或不支持iframe) 


        對於以上兩種方案,並不是說不能實現,只是說對於一個靈活的登陸系統來講仍然仍是會存在必定的侷限性的,咱們堅信能有更好的方案來解決這個問題。 

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 的操做。 
       
5. 開始實踐 
       首先,在咱們的子系統中應該有一個登陸頁面,經過輸入用戶名和密碼提交至cas認證中心。不過前提是先要獲取到  login tickt id. 也就是說當用戶第一次進入子系統的登陸頁面時,在該頁面中會經過js跳轉到 cas/login 中的獲取login ticket. 在 cas/login 的 flow 中先會判斷請求的參數中是否包含了 get-lt 的參數。 
      在cas的 login flow 中加入 ProvideLoginTicketAction 的流,主要用於判斷該請求是不是來獲取 lt,在cas-server端聲明獲取 login ticket action 類: 
com.denger.sso.web.ProvideLoginTicketAction html

Java代碼   收藏代碼
  1. /** 
  2.  * Opens up the CAS web flow to allow external retrieval of a login ticket. 
  3.  *  
  4.  * @author denger 
  5.  */  
  6. public class ProvideLoginTicketAction extends AbstractAction{  
  7.   
  8.     @Override  
  9.     protected Event doExecute(RequestContext context) throws Exception {  
  10.         final HttpServletRequest request = WebUtils.getHttpServletRequest(context);  
  11.   
  12.         if (request.getParameter("get-lt") != null && request.getParameter("get-lt").equalsIgnoreCase("true")) {  
  13.             return result("loginTicketRequested");  
  14.         }  
  15.         return result("continue");  
  16.     }  
  17.       
  18. }  
  19. // 若是參數中包含 get-lt 參數,則返回 loginTicketRequested 執行流,並跳轉至 loginTicket 生成頁,不然 則跳過該flow,並按照原始login的流程來執行。  


而且將該 action 聲明在 cas-servlet.xml 中: java

Xml代碼   收藏代碼
  1. <bean id="provideLoginTicketAction" class="com.denger.sso.web.ProvideLoginTicketAction" />       




還須要定義 loginTicket 的生成頁也就是當返回 loginTicketRequested 的 view: 
viewRedirectToRequestor.jsp web

Java代碼   收藏代碼
  1. <%@ page contentType="text/html; charset=UTF-8"%>  
  2. <%@ page import="com.denger.sso.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=${flowExecutionKey}";  
  20.         <spring:hasBindErrors name="credentials">  
  21.             var errorMsg = '<c:forEach var="error" items="${errors.allErrors}"><spring:message code="${error.code}" text="${error.defaultMessage}" /></c:forEach>';  
  22.             redirectURL += '&error_message=' + encodeURIComponent (errorMsg);  
  23.         </spring:hasBindErrors>  
  24.          window.location.href = redirectURL;  
  25.        </script>  
  26.     </head>  
  27.     <body></body>  
  28. </html>  
  29. <%  
  30.     } else {  
  31. %>         
  32.         <script>window.location.href = "/member/login";</script>  
  33. <%         
  34.     }  
  35. %>  


而且須要將該 jsp 聲明在 default._views.properites 中: ajax

Config代碼   收藏代碼
  1. ### Redirect with login ticket view  
  2. casRedirectToRequestorView.(class)=org.springframework.web.servlet.view.JstlView  
  3. casRedirectToRequestorView.url=/WEB-INF/view/jsp/default/ui/viewRedirectToRequestor.jsp  



相關 com.denger.sso.util.CasUtility 代碼: spring

Java代碼   收藏代碼
  1. public class CasUtility {  
  2.   
  3.     /** 
  4.      * Removes the previously attached GET parameters "lt" and "error_message" 
  5.      * to be able to send new ones. 
  6.      *  
  7.      * @param casUrl 
  8.      * @return 
  9.      */  
  10.     public static String resetUrl(String casUrl) {  
  11.         String cleanedUrl;  
  12.         String[] paramsToBeRemoved = new String[] { "lt", "error_message", "get-lt" };  
  13.         cleanedUrl = removeHttpGetParameters(casUrl, paramsToBeRemoved);  
  14.         return cleanedUrl;  
  15.     }  
  16.   
  17.     /** 
  18.      * Removes selected HTTP GET parameters from a given URL 
  19.      *  
  20.      * @param casUrl 
  21.      * @param paramsToBeRemoved 
  22.      * @return 
  23.      */  
  24.     public static String removeHttpGetParameters(String casUrl,  
  25.             String[] paramsToBeRemoved) {  
  26.         String cleanedUrl = casUrl;  
  27.         if (casUrl != null) {  
  28.             // check if there is any query string at all  
  29.             if (casUrl.indexOf("?") == -1) {  
  30.                 return casUrl;  
  31.             } else {  
  32.                 // determine the start and end position of the parameters to be  
  33.                 // removed  
  34.                 int startPosition, endPosition;  
  35.                 boolean containsOneOfTheUnwantedParams = false;  
  36.                 for (String paramToBeErased : paramsToBeRemoved) {  
  37.                     startPosition = -1;  
  38.                     endPosition = -1;  
  39.                     if (cleanedUrl.indexOf("?" + paramToBeErased + "=") > -1) {  
  40.                         startPosition = cleanedUrl.indexOf("?"  
  41.                                 + paramToBeErased + "=") + 1;  
  42.                     } else if (cleanedUrl.indexOf("&" + paramToBeErased + "=") > -1) {  
  43.                         startPosition = cleanedUrl.indexOf("&"  
  44.                                 + paramToBeErased + "=") + 1;  
  45.                     }  
  46.                     if (startPosition > -1) {  
  47.                         int temp = cleanedUrl.indexOf("&", startPosition);  
  48.                         endPosition = (temp > -1) ? temp + 1 : cleanedUrl  
  49.                                 .length();  
  50.                         // remove that parameter, leaving the rest untouched  
  51.                         cleanedUrl = cleanedUrl.substring(0, startPosition)  
  52.                                 + cleanedUrl.substring(endPosition);  
  53.                         containsOneOfTheUnwantedParams = true;  
  54.                     }  
  55.                 }  
  56.   
  57.                 // wenn nur noch das Fragezeichen vom query string √ºbrig oder am  
  58.                 // schluss ein "&", dann auch dieses entfernen  
  59.                 if (cleanedUrl.endsWith("?") || cleanedUrl.endsWith("&")) {  
  60.                     cleanedUrl = cleanedUrl.substring(0,  
  61.                             cleanedUrl.length() - 1);  
  62.                 }  
  63.                 // parameter mehrfach angegeben wurde...  
  64.                 if (!containsOneOfTheUnwantedParams)  
  65.                     return casUrl;  
  66.                 else  
  67.                     cleanedUrl = removeHttpGetParameters(cleanedUrl,  
  68.                             paramsToBeRemoved);  
  69.             }  
  70.         }  
  71.         return cleanedUrl;  
  72.     }  



還有一處須要調整的地方就是當用戶名和密碼驗證失敗後,應該從新返回至子系統登陸頁,也就是  login-at 參數值,此時一樣須要從新生成 login ticket。 因而找到 cas 登陸驗證處理 action :org.jasig.cas.web.flow.AuthenticationViaFormAction  修改 submit方法 中代碼下如: express

Java代碼   收藏代碼
  1. try {  
  2.             WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));  
  3.             putWarnCookieIfRequestParameterPresent(context);  
  4.             return "success";  
  5.         } catch (final TicketException e) {  
  6.             populateErrorsInstance(e, messageContext);  
  7.             // 當驗證失敗後,判斷參數中是否獲否 login-at 參數,若是包含的話則跳轉至 login ticket 獲取頁  
  8.             String referer = context.getRequestParameters().get("login-at");  
  9.             if (!org.apache.commons.lang.StringUtils.isBlank(referer)) {  
  10.                 return "errorForRemoteRequestor";  
  11.             }  
  12.             return "error";  
  13.         }  





接下來要作的就是將該action 的處理加入到 login-webflow.xml 請求流中: apache

Xml代碼   收藏代碼
  1. <on-start>  
  2.         <evaluate expression="initialFlowSetupAction" />  
  3.     </on-start>  
  4.    <!-- 添加以下配置 :-->  
  5.     <action-state id="provideLoginTicket">  
  6.         <evaluate expression="provideLoginTicketAction"/>  
  7.         <transition on="loginTicketRequested" to ="viewRedirectToRequestor" />  
  8.         <transition on="continue" to="ticketGrantingTicketExistsCheck" />  
  9.     </action-state>  
  10.   
  11.     <view-state id="viewRedirectToRequestor" view="casRedirectToRequestorView" model="credentials">  
  12.         <var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />  
  13.         <binder>  
  14.             <binding property="username" />  
  15.             <binding property="password" />  
  16.         </binder>  
  17.         <on-entry>  
  18.             <set name="viewScope.commandName" value="'credentials'" />  
  19.         </on-entry>  
  20.         <transition on="submit" bind="true" validate="true" to="realSubmit">  
  21.             <set name="flowScope.credentials" value="credentials" />  
  22.             <evaluate expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />  
  23.         </transition>  
  24.     </view-state>  
  25.        <!---添加結束處 --->  
  26.     <decision-state id="ticketGrantingTicketExistsCheck">  
  27.         <if test="flowScope.ticketGrantingTicketId neq null" then="hasServiceCheck" else="gatewayRequestCheck" />  
  28.     </decision-state>  
  29.   
  30.       <!-- ..... 省略中間代碼 ...-->  
  31.   
  32. <action-state id="realSubmit">  
  33.         <evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" />  
  34.         <transition on="warn" to="warn" />  
  35.         <transition on="success" to="sendTicketGrantingTicket" />  
  36.         <transition on="error" to="viewLoginForm" />  
  37. <!--加入該transition , 當驗證失敗以後從新獲取login ticket -->  
  38.         <transition on="errorForRemoteRequestor" to="viewRedirectToRequestor" />  
  39.     </action-state>  



好了,至此,對server端的調整基本上已經大功告成了,如今開始寫一個測試遠程登陸的 html: 

跨域

Html代碼   收藏代碼
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">  
  2. <html>  
  3. <head>  
  4. <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">  
  5. <title>Test remote Login using JS</title>  
  6. <script type="text/javascript">  
  7. function prepareLoginForm() {  
  8.     $('myLoginForm').action = casLoginURL;  
  9.     $("lt").value = loginTicket;  
  10. }  
  11.   
  12. function checkForLoginTicket() {  
  13.     var loginTicketProvided = false;  
  14.     var query   = '';  
  15.    casLoginURL = 'http://192.168.6.1:8080/member/login';  
  16.    thisPageURL = 'http://192.168.6.1:8080/member/test-login.html';  
  17.    casLoginURL += '?login-at=' + encodeURIComponent (thisPageURL);  
  18.   
  19.     query   = window.location.search;  
  20.     queryquery   = query.substr (1);  
  21.   
  22.   
  23.     var param   = new Array();  
  24.     //var value = new Array();  
  25.     var temp    = new Array();  
  26.     param   = query.split ('&');  
  27.   
  28.     i = 0;  
  29.         // 開始獲取當前 url 的參數,獲到 lt 和 error_message。  
  30.     while (param[i]) {  
  31.         temp        = param[i].split ('=');  
  32.         if (temp[0] == 'lt') {  
  33.             loginTicket = temp[1];  
  34.             loginTicketProvided = true;  
  35.         }  
  36.         if (temp[0] == 'error_message') {  
  37.                 error = temp[1];  
  38.             }  
  39.         i++;  
  40.     }  
  41.         // 判斷是否已經獲取到 lt 參數,若是未獲取到則跳轉至 cas/login 頁,而且帶上請求參數  get-lt=true。 第一次進該頁面時會進行一次跳轉  
  42.     if (!loginTicketProvided) {  
  43.         location.href = casLoginURL + '&get-lt=true';  
  44.     }  
  45. }  
  46.   
  47. var $ = function(id){  
  48.     return document.getElementById(id);  
  49. }  
  50.   
  51.   
  52. checkForLoginTicket();  
  53. onload = prepareLoginForm;  
  54. </script>  
  55. </head>  
  56. <body>  
  57. <h2>Test remote Login using JS</h2>  
  58. <form id="myLoginForm" action="" method="post">  
  59. <input type="hidden" name="_eventId" value="submit" />  
  60. <table>  
  61. <tr>  
  62.     <td id="txt_error" colspan="2">  
  63.   
  64.     <script type="text/javascript" language="javascript">  
  65.     <!--  
  66.     if ( error ) {  
  67.       
  68.         error = decodeURIComponent (error);  
  69.           
  70.         document.write (error);  
  71.     }  
  72.     //-->  
  73.     </script>  
  74.   
  75.     </td>  
  76. </tr>  
  77. <tr>  
  78.     <td>Username:</td>  
  79.     <td><input type="text" value="" name="username" ></td>  
  80. </tr>  
  81. <tr>  
  82.     <td>Password:</td>  
  83.     <td><input type="text" value="" name="password" ></td>  
  84. </tr>  
  85. <tr>  
  86.     <td>Login Ticket:</td>  
  87.     <td><input type="text" name="lt" id="lt" value=""></td>  
  88. </tr>  
  89. <tr>  
  90.     <td>Service:</td>  
  91.     <td><input type="text" name="service" value="http://www.google.com.hk"></td>  
  92. </tr>  
  93. <tr>  
  94.     <td align="right" colspan="2"><input type="submit" /></td>  
  95. </tr>  
  96. </table>  
  97. </form>  
  98. </body>  
  99. </html>  



開始測試,直接訪問:http://192.168.6.1:8080/member/test-login.html  發現進行了二次重定向,進入該頁面 js 未發現 lt 參數,因而重定向到 http://192.168.6.1:8080/member/login?login-at=http://192.168.6.1:8080/member/test-login.html &get-lt=true ,而後又從該頁重定向到 http://192.168.6.1:8080/member/test-login.html?lt=e1s1 ,能夠發現,其中的  lt 就是咱們所須要的 login ticket參數。 


6. 不足之處 
       1. 能夠發現,每次用戶訪問 登陸頁面時都要進行兩次重定向的操做,雖然很快,可是在有些狀況仍然能看到登陸頁面閃了一下。 固然這也是有辦法能夠解決的! 
       2. 能夠發現,當登陸失敗以後,會將錯誤信息以參數的方式進行傳遞,看上去這並不是專業作法。能夠定義一些錯誤標識,好比 1 是用戶名或密碼錯誤之類的。 

PS:參考:https://wiki.jasig.org/display/CAS/Using+CAS+without+the+Login+Screen  若有不足之處,歡迎指正安全

spring boot 集成cas

http://blog.csdn.net/jw314947712/article/details/54236216

http://blog.163.com/software_warrior/blog/static/169719837201701693329537/

http://blog.csdn.net/cl_andywin/article/details/53998986

http://blog.csdn.net/jw314947712/article/details/54236216

http://www.cnblogs.com/question-sky/p/7061522.html

http://blog.csdn.net/ONROAD0612/article/details/72846247

http://blog.csdn.net/liuchuanhong1/article/details/73176603

http://tonyaction.blog.51cto.com/227462/898173

http://www.cnblogs.com/question-sky/p/7061522.html

相關文章
相關標籤/搜索