CAS实现单点 实现一处登陆 可访问多个应用 。 可是原登陆是CAS默认登陆页面和登出页面是没法重定向到自定义页面的 此处使用Ajax+Iframe 的方法来实现自定义页面跨域提交登陆。javascript
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
通常对于获取登陆ticket的解决方案可能大多数人都会提到两种方法:
java
AJAX: 熟悉 Ajax 的可能都知道,它的请求方式是严格按照沙箱安全模型机制的,严格状况下会存在跨域安全问题。 web
IFrames: 这也是早期的 ajax 实现方式,在页面中嵌入一个隐藏的IFrame,而后经过表单提交到该iframe来实现不刷新提交,不过使用这种方式一样会带来两个问题: ajax
登陆成功以后如何摆脱登陆后的IFrame呢?若是成功登陆可能会致使整个页面重定向,固然你能在form中使用属性 target="_parent",使之弹出,那么你如何在父页面显示错误信息呢?
b. 你可能会受到布局的限止(不容许或不支持iframe) 对于以上两种方案,并不是说不能实现,只是说对于一个灵活的登陆系统来讲仍然仍是会存在必定的局限性的,咱们坚信能有更好的方案来解决这个问题。spring
当第一次进入子系统的登陆页时,经过 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> </em> <input type="checkbox" name="rememberMe" id="rememberMe" value="true" /> 自动登陆 <a href="/retrieve">忘记密码?</a></li> <li><em> </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>
二、客户端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>
三、服务端
向服务端发送请求为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> <!-- 添加结束处-->
添加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 }
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 %>
服务端在获取请求时会进入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>
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 }
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>