文档章节

CAS 之自定义登录页实践

引鸩怼孑
 引鸩怼孑
发布于 2015/06/30 17:14
字数 2393
阅读 254
收藏 11
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的解决方案可能大多数人都会提到两种方法: 
   
  • 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  
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 中: 
Xml代码   收藏代码
  1. <bean id="provideLoginTicketAction" class="com.denger.sso.web.ProvideLoginTicketAction" />       



还需要定义 loginTicket 的生成页也就是当返回 loginTicketRequested 的 view: 
viewRedirectToRequestor.jsp  
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 中: 
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  代码: 
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方法 中代码下如: 
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 请求流中: 
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 是用户名或密码错误之类的。

本文转载自:http://denger.iteye.com/blog/809170

引鸩怼孑
粉丝 45
博文 206
码字总数 16947
作品 0
南京
项目经理
私信 提问
CAS单点登录系统如何在没有登录的情况下不自动跳转到登录页

最近公司项目需要实现单点登录,我选择使用CAS来实现,在进行的过程中,遇到一个问题。 在使用CAS单点登录的时候,如果当前没有登录的话,会跳转到登录页去登陆。 但现在比如说 ,我有一个项...

窗边冷月光
2016/12/12
877
5
关于登录 页面的 和数据库的问题

不知道 oschina 有没有用 cas 做单点登录, 不过看样子是没有的 1. CAS 那个登录页面是不是 可以修改的啊, 我的意思是 可以用每个 自己的登录页面, 而不是修改它 原来的页面, 有没有一个 http...

panmingguang
2014/04/21
117
0
java cas求解惑

最近接触到一个cas单点登录的项目,在网上查看了很多资料,总是感觉自己没有理解。所以跑过来请教下。 疑问1:CAS必须要单独的配置一个服务器(tomcat)吗?如果放子系统中的某个集成可以吗?比...

wtintslt
2017/10/09
163
6
单点登录CAS使用记:关于服务器超时以及客户端超时的分析

我的预想情况 一般情况下,当用户登录一个站点后,如果长时间没有发生任何动作,当用户再次点击时,会被强制登出并且跳转到登录页面, 提醒用户重新登录。现在我已经为站点整合了CAS,并且已...

Zzzz_WP
2018/09/04
0
0
a466350665/smart

Smart QQ交流群:454343484(提供开发工具和文档下载) 简述 Smart定位用当下最流行的SSM(SpringMVC + Spring + Mybatis)技术,为您构建一个易理解、高可用、高扩展性的单点登录权限管理应用...

a466350665
2017/08/09
0
0

没有更多内容

加载失败,请刷新页面

加载更多

任务调度-单体应用定时任务解决方案

1. 应用场景: 单体应用(并发少、就公司内部使用)、业务比较简单、单一、稳定,传统行业首选,项目初期。 2. 主要方式: Spring XML配置方式,timer。 <bean id="cycleBonusTimer" class="...

秋日芒草
10分钟前
0
0
EditText中singleLine过期替代方法

android:lines="1" android:inputType="text"

球球
24分钟前
0
0
删除 Tomcat-webapps 目录自带项目

本文将 %CATALINA_HOME% 目录称为“tomcat”目录。 1.webapps目录中的项目 在 Tomcat 8.0 的 tomcat/webapps 目录中,含有 5 个 Tomcat 自带的 Web 项目,如下所示: docs 有关于 Tomcat 的介...

Airship
28分钟前
2
0
好文:华杉:我等用功,不求日增,但求日减。减一分人欲,则增一分天理,这是何等简易!何等洒脱!

#写在前面1.怎么理解“减一分人欲,则增一分天理,这是何等简易!”?1)华杉提倡 “一劳永逸” 排除浪费,少干活,多赚钱,一战而定,降低作业成本。2)华杉提倡学海无涯,回头是岸...

阿锋zxf
38分钟前
3
0
vue 的bus总线

bus声明 global.bus = new Vue() 事件发送 controlTabbar () {global.bus.$emit('pickUp', 'ddd')}, 事件接收 global.bus.$on('pickUp', (res) => {this.isFocus = true})......

Js_Mei
43分钟前
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部