`
denger
  • 浏览: 359025 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

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的解决方案可能大多数人都会提到两种方法:
   
  • 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
/**
 * Opens up the CAS web flow to allow external retrieval of a login ticket.
 * 
 * @author denger
 */
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");
	}
	
}
// 如果参数中包含 get-lt 参数,则返回 loginTicketRequested 执行流,并跳转至 loginTicket 生成页,否则 则跳过该flow,并按照原始login的流程来执行。

并且将该 action 声明在 cas-servlet.xml 中:
<bean id="provideLoginTicketAction" class="com.denger.sso.web.ProvideLoginTicketAction" />     



还需要定义 loginTicket 的生成页也就是当返回 loginTicketRequested 的 view:
viewRedirectToRequestor.jsp
<%@ 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 = "";
        // 需要输入 login-at 参数,当生成lt后或登录失败后则重新跳转至 原登录页,并传入参数 lt 和 error_message
	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 中:
### 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 代码:
public class CasUtility {

	/**
	 * Removes the previously attached GET parameters "lt" and "error_message"
	 * to be able to send new ones.
	 * 
	 * @param casUrl
	 * @return
	 */
	public static String resetUrl(String casUrl) {
		String cleanedUrl;
		String[] paramsToBeRemoved = new String[] { "lt", "error_message", "get-lt" };
		cleanedUrl = removeHttpGetParameters(casUrl, paramsToBeRemoved);
		return cleanedUrl;
	}

	/**
	 * Removes selected HTTP GET parameters from a given URL
	 * 
	 * @param casUrl
	 * @param paramsToBeRemoved
	 * @return
	 */
	public static String removeHttpGetParameters(String casUrl,
			String[] paramsToBeRemoved) {
		String cleanedUrl = casUrl;
		if (casUrl != null) {
			// check if there is any query string at all
			if (casUrl.indexOf("?") == -1) {
				return casUrl;
			} else {
				// determine the start and end position of the parameters to be
				// removed
				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();
						// remove that parameter, leaving the rest untouched
						cleanedUrl = cleanedUrl.substring(0, startPosition)
								+ cleanedUrl.substring(endPosition);
						containsOneOfTheUnwantedParams = true;
					}
				}

				// wenn nur noch das Fragezeichen vom query string übrig oder am
				// schluss ein "&", dann auch dieses entfernen
				if (cleanedUrl.endsWith("?") || cleanedUrl.endsWith("&")) {
					cleanedUrl = cleanedUrl.substring(0,
							cleanedUrl.length() - 1);
				}
				// parameter mehrfach angegeben wurde...
				if (!containsOneOfTheUnwantedParams)
					return casUrl;
				else
					cleanedUrl = removeHttpGetParameters(cleanedUrl,
							paramsToBeRemoved);
			}
		}
		return cleanedUrl;
	}


还有一处需要调整的地方就是当用户名和密码验证失败后,应该重新返回至子系统登录页,也就是  login-at 参数值,此时同样需要重新生成 login ticket。 于是找到 cas 登录验证处理 action :org.jasig.cas.web.flow.AuthenticationViaFormAction  修改 submit方法 中代码下如:
try {
            WebUtils.putTicketGrantingTicketInRequestScope(context, this.centralAuthenticationService.createTicketGrantingTicket(credentials));
            putWarnCookieIfRequestParameterPresent(context);
            return "success";
        } catch (final TicketException e) {
            populateErrorsInstance(e, messageContext);
            // 当验证失败后,判断参数中是否获否 login-at 参数,如果包含的话则跳转至 login ticket 获取页
            String referer = context.getRequestParameters().get("login-at");
            if (!org.apache.commons.lang.StringUtils.isBlank(referer)) {
                return "errorForRemoteRequestor";
            }
            return "error";
        }




接下来要做的就是将该action 的处理加入到 login-webflow.xml 请求流中:
<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 , 当验证失败之后重新获取login ticket -->
		<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;
	query	= 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  如有不足之处,欢迎指正~
  • 大小: 37.3 KB
  • 大小: 50.4 KB
  • 大小: 31.5 KB
16
1
分享到:
评论
5 楼 波波补丁 2011-06-21  
您好,我想请问一下com.denger.sso.web.ProvideLoginTicketAction
这个类是放在哪里呢?
4 楼 denger 2011-05-31  
melin 写道
可以考虑用jsonp的方式来获取it

是的,我们目前就是这么做的。sina的sso也是这么做的。
3 楼 melin 2011-05-31  
可以考虑用jsonp的方式来获取it
2 楼 denger 2010-11-11  
xuwenkeke 写道
其实不用那么复杂, CAS 提供了restful-API形式的login方式.

谢谢,它REST API这个我知道,不过CAS 的跨域 SSO 主要是依赖于 cookie 来实现~  它所提供的 REST 只是用于获取ST和 TGT,目的并非用于登录.
1 楼 xuwenkeke 2010-11-11  
其实不用那么复杂, CAS 提供了restful-API形式的login方式.

相关推荐

    cas修改登录页

    【标题】"CAS修改登录页"是一个针对中央认证服务(Central Authentication Service,简称CAS)的定制化实践。在单点登录(Single Sign-On,SSO)系统中,CAS扮演着核心角色,它提供了统一的身份验证服务,使得用户只...

    CAS单点登录(SSO)服务端自定义认证+CAS客户端配置+CAS完整使用文档+CAS4.2.7 cas-serv服务端源码 cas-client客户端源码

    CAS(Central Authentication Service)是Java开发的一个开源的单点登录...通过学习和实践,你可以掌握CAS的核心概念,实现自定义认证策略,以及优化客户端集成,从而为你的项目构建一个强大而安全的单点登录系统。

    让CAS支持客户端自定义登陆页面——客户端篇

    标题中的“让CAS支持客户端自定义登陆页面——客户端篇”表明了本文主要探讨的是如何在中央认证服务(Central Authentication Service, CAS)系统中,允许客户端应用程序实现自定义登录页面的配置与实现。CAS是一个...

    cas源码修改-登录页面

    9. **API扩展**:如果需要在登录页面添加自定义功能,如集成第三方服务,可以通过实现 CAS 提供的 REST API 或者 WebService API 来实现。 10. **文档更新**:任何代码更改后,确保更新相关的开发者文档,以便团队...

    CAS实现不同应用展示不同的登录页

    总结,实现CAS系统下不同应用展示不同登录页的关键在于理解CAS的工作机制,并对服务器端进行适当的配置和扩展。通过修改登录视图、动态选择页面和定义应用标识,我们可以为用户提供更加个性化和友好的登录体验。在...

    CAS-3.2.1自定义客户端登录界面----完整篇

    由于实际内容没有给出,我将基于常见的CAS自定义登录流程进行讲解。 首先,理解CAS的工作原理至关重要。当用户尝试访问一个受CAS保护的应用时,会被重定向到CAS服务器的登录页面。在这里,用户输入凭证,如用户名和...

    自定义客户端登录CAS服务器-iframe实现

    这通常涉及向CAS服务器发送请求,获取认证URL(即CAS登录页),并在用户登录成功后,处理CAS返回的服务票证。 4. iframe技术:使用HTML的iframe元素,将CAS的登录页面嵌入到你的应用中,使得用户可以在不离开当前...

    让CAS支持客户端自定义登陆页面----服务器篇-.pdf

    本文主要介绍如何让CAS(Central Authentication Service)支持客户端自定义登录页面,主要集中在服务器端的修改。CAS是一种开源的身份验证框架,它提供单点登录(SSO)功能,通常用于集中管理多个应用系统的用户...

    让CAS支持客户端自定义登陆页面----服务器篇--.doc

    本文将详细介绍如何在CAS服务器端进行配置,以便支持客户端自定义登录页面。 首先,我们要明确修改的目标是不影响CAS原有的统一登录界面功能,同时尽可能简化客户端的实现,并确保原有功能的安全性。但要注意的是,...

    CAS单点登录Demo

    在本文中,我们将深入探讨CAS单点登录的基本原理、工作流程以及如何通过提供的Demo进行实践操作。 **CAS基本原理** CAS的核心思想是用户只需在一个应用系统中验证身份,之后访问其他所有支持CAS的应用系统时都不再...

    CAS单点登录配置

    可以使用CAS提供的工具或者开发自定义的测试脚本来模拟用户登录和应用间的跳转。 在提供的PDF文件`cas单点登录(一).pdf`和`cas单点登录(二).pdf`中,应该详细涵盖了这些步骤,以及更深入的技术细节,包括可能...

    CAS单点登录配置大全

    **CAS单点登录配置大全** CAS(Central Authentication Service,中央认证服务)是一种广泛使用的开源单点登录(Single Sign-On,SSO)协议。它允许用户通过一个统一的认证系统访问多个应用系统,而无需在每个系统...

    基于Cas的单点登录实现

    **基于Cas的单点登录实现** 单点登录(Single Sign-On,简称SSO)是一种在多个应用系统中,...通过学习和实践这个示例应用,开发者可以深入理解Cas和Shiro的结合使用,以及SSO在实际项目中的应用,提升系统安全性。

    cas单点登录4.0

    CAS(Central Authentication Service)是...总之,"cas单点登录4.0"的资源对于学习和实践CAS单点登录系统非常重要。无论是部署和配置CAS服务器,还是深入研究其源代码,都能帮助我们更全面地理解和掌握单点登录技术。

    利用CAS实现单点登录的完整实例

    总结,通过学习和实践这个"利用CAS实现单点登录的完整实例",你将掌握如何使用Jasig CAS构建一个高效的单点登录系统,从而提升用户体验,简化身份验证管理,并加强系统的安全性。记得深入理解每个步骤,并根据实际...

    CAS单点登录示例

    在这个" CAS单点登录示例 "中,你可以通过实践来理解这些概念,探索如何设置CAS服务器,配置客户端,以及如何进行用户认证。通过这个示例,你将更深入地了解SSO的工作原理,为实际项目中的身份验证解决方案打下坚实...

    cas-demo.rar_.net cas demo_CAS_CAS点登录配置文档_DEMO_cas .net

    6. **定制化**:根据应用需求,可能需要自定义CAS客户端的行为,如自定义登录页面、错误处理或者用户信息的解析。 7. **安全考虑**:集成CAS后,应确保网络通信的安全,通常采用HTTPS协议来加密传输数据。同时,对...

    SSO之CAS单点登录实例演示

    通过"SSO之CAS单点登录实例演示",我们可以实践这些步骤,了解如何设置和运行一个基本的CAS环境,进一步理解SSO的工作原理和实际应用。这个实例将帮助我们更好地掌握如何为自己的应用实现SSO功能,提升系统的安全性...

    落雨博客基于CAS框架的单点登录技术讲解(ppt+code实例+doc)配套资料

    [置顶] SSO单点登录系列3:cas-server端配置认证方式实践(数据源+自定义java类认证) http://blog.csdn.net/ae6623/article/details/8851801 [置顶] SSO单点登录系列2:cas客户端和cas服务端交互原理动画图解,cas...

Global site tag (gtag.js) - Google Analytics