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
- public class ProvideLoginTicketAction extends AbstractAction{
-
- @Override
- protected Event doExecute(RequestContext context) throws Exception {
- final HttpServletRequest request = WebUtils.getHttpServletRequest(context);
-
- if (request.getParameter("get-lt") != null && request.getParameter("get-lt").equalsIgnoreCase("true")) {
- return result("loginTicketRequested");
- }
- return result("continue");
- }
-
- }
而且將該 action 聲明在 cas-servlet.xml 中: java
- <bean id="provideLoginTicketAction" class="com.denger.sso.web.ProvideLoginTicketAction" />
還須要定義 loginTicket 的生成頁也就是當返回 loginTicketRequested 的 view:
viewRedirectToRequestor.jsp web
- <%@ page contentType="text/html; charset=UTF-8"%>
- <%@ page import="com.denger.sso.util.CasUtility"%>
- <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
- <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
- <%
- String separator = "";
-
- String referer = request.getParameter("login-at");
-
- referer = CasUtility.resetUrl(referer);
- if (referer != null && referer.length() > 0) {
- separator = (referer.indexOf("?") > -1) ? "&" : "?";
- %>
- <html>
- <title>cas get login ticket</title>
- <head>
- <META http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <script>
- var redirectURL = "<%=referer + separator%>lt=${flowExecutionKey}";
- <spring:hasBindErrors name="credentials">
- var errorMsg = '<c:forEach var="error" items="${errors.allErrors}"><spring:message code="${error.code}" text="${error.defaultMessage}" /></c:forEach>';
- redirectURL += '&error_message=' + encodeURIComponent (errorMsg);
- </spring:hasBindErrors>
- window.location.href = redirectURL;
- </script>
- </head>
- <body></body>
- </html>
- <%
- } else {
- %>
- <script>window.location.href = "/member/login";</script>
- <%
- }
- %>
而且須要將該 jsp 聲明在 default._views.properites 中: ajax
- ### Redirect with login ticket view
- casRedirectToRequestorView.(class)=org.springframework.web.servlet.view.JstlView
- casRedirectToRequestorView.url=/WEB-INF/view/jsp/default/ui/viewRedirectToRequestor.jsp
相關 com.denger.sso.util.CasUtility 代碼: spring
- public class CasUtility {
-
-
- public static String resetUrl(String casUrl) {
- String cleanedUrl;
- String[] paramsToBeRemoved = new String[] { "lt", "error_message", "get-lt" };
- cleanedUrl = removeHttpGetParameters(casUrl, paramsToBeRemoved);
- return cleanedUrl;
- }
-
-
- public static String removeHttpGetParameters(String casUrl,
- String[] paramsToBeRemoved) {
- String cleanedUrl = casUrl;
- if (casUrl != null) {
-
- if (casUrl.indexOf("?") == -1) {
- return casUrl;
- } else {
-
-
- int startPosition, endPosition;
- boolean containsOneOfTheUnwantedParams = false;
- for (String paramToBeErased : paramsToBeRemoved) {
- startPosition = -1;
- endPosition = -1;
- if (cleanedUrl.indexOf("?" + paramToBeErased + "=") > -1) {
- startPosition = cleanedUrl.indexOf("?"
- + paramToBeErased + "=") + 1;
- } else if (cleanedUrl.indexOf("&" + paramToBeErased + "=") > -1) {
- startPosition = cleanedUrl.indexOf("&"
- + paramToBeErased + "=") + 1;
- }
- if (startPosition > -1) {
- int temp = cleanedUrl.indexOf("&", startPosition);
- endPosition = (temp > -1) ? temp + 1 : cleanedUrl
- .length();
-
- cleanedUrl = cleanedUrl.substring(0, startPosition)
- + cleanedUrl.substring(endPosition);
- containsOneOfTheUnwantedParams = true;
- }
- }
-
-
-
- if (cleanedUrl.endsWith("?") || cleanedUrl.endsWith("&")) {
- cleanedUrl = cleanedUrl.substring(0,
- cleanedUrl.length() - 1);
- }
-
- if (!containsOneOfTheUnwantedParams)
- return casUrl;
- else
- cleanedUrl = removeHttpGetParameters(cleanedUrl,
- paramsToBeRemoved);
- }
- }
- return cleanedUrl;
- }
還有一處須要調整的地方就是當用戶名和密碼驗證失敗後,應該從新返回至子系統登陸頁,也就是 login-at 參數值,此時一樣須要從新生成 login ticket。 因而找到 cas 登陸驗證處理 action :org.jasig.cas.web.flow.AuthenticationViaFormAction 修改 submit方法 中代碼下如: express
- try {
- WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));
- putWarnCookieIfRequestParameterPresent(context);
- return "success";
- } catch (final TicketException e) {
- populateErrorsInstance(e, messageContext);
-
- String referer = context.getRequestParameters().get("login-at");
- if (!org.apache.commons.lang.StringUtils.isBlank(referer)) {
- return "errorForRemoteRequestor";
- }
- return "error";
- }
接下來要作的就是將該action 的處理加入到 login-webflow.xml 請求流中: apache
- <on-start>
- <evaluate expression="initialFlowSetupAction" />
- </on-start>
-
- <action-state id="provideLoginTicket">
- <evaluate expression="provideLoginTicketAction"/>
- <transition on="loginTicketRequested" to ="viewRedirectToRequestor" />
- <transition on="continue" to="ticketGrantingTicketExistsCheck" />
- </action-state>
-
- <view-state id="viewRedirectToRequestor" view="casRedirectToRequestorView" model="credentials">
- <var name="credentials" class="org.jasig.cas.authentication.principal.UsernamePasswordCredentials" />
- <binder>
- <binding property="username" />
- <binding property="password" />
- </binder>
- <on-entry>
- <set name="viewScope.commandName" value="'credentials'" />
- </on-entry>
- <transition on="submit" bind="true" validate="true" to="realSubmit">
- <set name="flowScope.credentials" value="credentials" />
- <evaluate expression="authenticationViaFormAction.doBind(flowRequestContext, flowScope.credentials)" />
- </transition>
- </view-state>
-
- <decision-state id="ticketGrantingTicketExistsCheck">
- <if test="flowScope.ticketGrantingTicketId neq null" then="hasServiceCheck" else="gatewayRequestCheck" />
- </decision-state>
-
-
-
- <action-state id="realSubmit">
- <evaluate expression="authenticationViaFormAction.submit(flowRequestContext, flowScope.credentials, messageContext)" />
- <transition on="warn" to="warn" />
- <transition on="success" to="sendTicketGrantingTicket" />
- <transition on="error" to="viewLoginForm" />
- <transition on="errorForRemoteRequestor" to="viewRedirectToRequestor" />
- </action-state>
好了,至此,對server端的調整基本上已經大功告成了,如今開始寫一個測試遠程登陸的 html:
跨域
- <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
- <html>
- <head>
- <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
- <title>Test remote Login using JS</title>
- <script type="text/javascript">
- function prepareLoginForm() {
- $('myLoginForm').action = casLoginURL;
- $("lt").value = loginTicket;
- }
-
- function checkForLoginTicket() {
- var loginTicketProvided = false;
- var query = '';
- casLoginURL = 'http://192.168.6.1:8080/member/login';
- thisPageURL = 'http://192.168.6.1:8080/member/test-login.html';
- casLoginURL += '?login-at=' + encodeURIComponent (thisPageURL);
-
- query = window.location.search;
- queryquery = query.substr (1);
-
-
- var param = new Array();
- //var value = 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];
- loginTicketProvided = true;
- }
- if (temp[0] == 'error_message') {
- error = temp[1];
- }
- i++;
- }
- // 判斷是否已經獲取到 lt 參數,若是未獲取到則跳轉至 cas/login 頁,而且帶上請求參數 get-lt=true。 第一次進該頁面時會進行一次跳轉
- if (!loginTicketProvided) {
- location.href = casLoginURL + '&get-lt=true';
- }
- }
-
- var $ = function(id){
- return document.getElementById(id);
- }
-
-
- checkForLoginTicket();
- onload = prepareLoginForm;
- </script>
- </head>
- <body>
- <h2>Test remote Login using JS</h2>
- <form id="myLoginForm" action="" method="post">
- <input type="hidden" name="_eventId" value="submit" />
- <table>
- <tr>
- <td id="txt_error" colspan="2">
-
- <script type="text/javascript" language="javascript">
- <!--
- if ( error ) {
-
- error = decodeURIComponent (error);
-
- document.write (error);
- }
- //-->
- </script>
-
- </td>
- </tr>
- <tr>
- <td>Username:</td>
- <td><input type="text" value="" name="username" ></td>
- </tr>
- <tr>
- <td>Password:</td>
- <td><input type="text" value="" name="password" ></td>
- </tr>
- <tr>
- <td>Login Ticket:</td>
- <td><input type="text" name="lt" id="lt" value=""></td>
- </tr>
- <tr>
- <td>Service:</td>
- <td><input type="text" name="service" value="http://www.google.com.hk"></td>
- </tr>
- <tr>
- <td align="right" colspan="2"><input type="submit" /></td>
- </tr>
- </table>
- </form>
- </body>
- </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