1. 程式人生 > >單點登入(八)cas支援客戶端登入——客戶端

單點登入(八)cas支援客戶端登入——客戶端

客戶端即指使用CAS中央認證伺服器的應用程式,而不是指使用者瀏覽器

客戶端實現目標
客戶端實現主要需要滿足5個case:
·          1. 使用者未在中央認證伺服器登陸,訪問客戶端受保護資源時,客戶端重定向到中央認證伺服器請求TGT認證,認證失敗,轉回客戶端登陸頁面,保證受保護資源URL資訊不丟失
·          2. 使用者未在中央認證伺服器登陸,訪問客戶端登陸頁面時,客戶端重定向到中央認證伺服器請求TGT認證,認證失敗,轉回客戶端登陸頁面,此次登入頁面不再受保護,允許訪問
·          3. 使用者已在中央認證伺服器登陸,訪問客戶端受保護資源時,客戶端重定向到中央認證伺服器請求TGT認證,認證成功,直接轉回受保護資源
·          4. 使用者在客戶端登陸頁面提交使用者名稱密碼,客戶端將使用者名稱密碼資訊提交給伺服器端,認證失敗,轉回客戶端登陸頁面,攜帶失敗資訊並保證轉到登陸頁面前受保護資源URL資訊不丟失
·          5. 使用者在客戶端登陸頁面提交使用者名稱密碼,客戶端將使用者名稱密碼資訊提交給伺服器端,認證成功,轉回轉到登陸頁面前受保護資源
        對於case  1和case  3,普通的CAS客戶端即可滿足需求,但對於case  4和case  5,則需要我們定製自己的登陸頁面。對於case 2,主要是需要滿足部分登陸頁面希望在使用者未登陸狀態顯示登陸框,在已登陸狀態顯示使用者歡迎資訊的需求,實現這個需求我們是通過讓CAS客戶端認證器滿足一個排除約定,即當用戶請求路徑為登陸頁面且帶有validated=true的引數時,即不進行重定向TGT認證請求

客戶端修改方案

        根據伺服器流程修改方案,我們可以知道,所有的遠端請求都必須攜帶有loginUrl引數資訊以使得伺服器端知道在認證失敗後轉向客戶端登陸頁面。而在CAS客戶端上,上一節的case  4和case  5,我們主要通過提交表單的方式傳遞loginUrl,而case  1,  case  3則是依靠org.jasig.cas.client.authentication.AuthenticationFilter類進行的轉向,但使用AuthenticationFilter轉向時,是沒有loginUrl資訊的,因此我們首先需要重新實現一個自己的認證過濾器,以下是我們自己的認證過濾器的程式碼:

/** 
* @author xiaoxiao 
* 日期:2013-8-7 上午11:23:02 
* The default character set is UTF-8.
*/ 
package td.sso.filter;

import java.io.IOException;
import java.net.URL;
import java.net.URLEncoder;

import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletRequest;
import javax.servlet.ServletResponse;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.jasig.cas.client.authentication.DefaultGatewayResolverImpl;
import org.jasig.cas.client.authentication.GatewayResolver;
import org.jasig.cas.client.util.AbstractCasFilter;
import org.jasig.cas.client.util.CommonUtils;
import org.jasig.cas.client.validation.Assertion;

/**
 * 遠端認證過濾器.
 * 由於AuthenticationFilter的doFilter方法被宣告為final,
 * 只好重新實現一個認證過濾器,支援localLoginUrl設定.
 */
public class RemoteAuthenticationFilter extends AbstractCasFilter {
    
    public static final String CONST_CAS_GATEWAY = "_const_cas_gateway_";

    /**
     * 本地登陸頁面URL.
     */
    private String localLoginUrl;
    
    /**
     * The URL to the CAS Server login.
     */
    private String casServerLoginUrl;

    /**
     * Whether to send the renew request or not.
     */
    private boolean renew = false;

    /**
     * Whether to send the gateway request or not.
     */
    private boolean gateway = false;
    

    protected void initInternal(final FilterConfig filterConfig) throws ServletException {
        super.initInternal(filterConfig);
        setCasServerLoginUrl(getPropertyFromInitParams(filterConfig, "casServerLoginUrl", null));
        log.trace("Loaded CasServerLoginUrl parameter: " + this.casServerLoginUrl);
        setLocalLoginUrl(getPropertyFromInitParams(filterConfig, "localLoginUrl", null));
        log.trace("Loaded LocalLoginUrl parameter: " + this.localLoginUrl);
        setRenew(Boolean.parseBoolean(getPropertyFromInitParams(filterConfig, "renew", "false")));
        log.trace("Loaded renew parameter: " + this.renew);
        setGateway(Boolean.parseBoolean(getPropertyFromInitParams(filterConfig, "gateway", "false")));
        log.trace("Loaded gateway parameter: " + this.gateway);
        
    }

    public void init() {
        super.init();
        CommonUtils.assertNotNull(this.localLoginUrl, "localLoginUrl cannot be null.");
        CommonUtils.assertNotNull(this.casServerLoginUrl, "casServerLoginUrl cannot be null.");
    }

    public final void doFilter(final ServletRequest servletRequest,
            final ServletResponse servletResponse, final FilterChain filterChain)
            throws IOException, ServletException {
    	System.out.println("呼叫RemoteAuthenticationFilter--->");
        final HttpServletRequest request = (HttpServletRequest) servletRequest;
        final HttpServletResponse response = (HttpServletResponse) servletResponse;
        final HttpSession session = request.getSession(false);
        final String ticket = request.getParameter(getArtifactParameterName());
        final Assertion assertion = session != null ? (Assertion) session.getAttribute(CONST_CAS_ASSERTION) : null;
//        final String serviceUrl = constructServiceUrl(request, response);
        final boolean wasGatewayed = session != null && session.getAttribute(CONST_CAS_GATEWAY) != null;
        

        // 如果訪問路徑為localLoginUrl且帶有validated引數則跳過
        URL url = new URL(localLoginUrl);
        final boolean isValidatedLocalLoginUrl = request.getRequestURI().endsWith(url.getPath()) &&
            CommonUtils.isNotBlank(request.getParameter("validated"));
System.out.println(!isValidatedLocalLoginUrl && CommonUtils.isBlank(ticket) && assertion == null && !wasGatewayed); if (!isValidatedLocalLoginUrl && CommonUtils.isBlank(ticket) && assertion == null && !wasGatewayed) { log.debug("no ticket and no assertion found"); if (this.gateway) { log.debug("setting gateway attribute in session"); request.getSession(true).setAttribute(CONST_CAS_GATEWAY, "yes"); } final String serviceUrl = constructServiceUrl(request, response); System.out.println(serviceUrl); if (log.isDebugEnabled()) { log.debug("Constructed service url: " + serviceUrl); } String urlToRedirectTo = CommonUtils.constructRedirectUrl( this.casServerLoginUrl, getServiceParameterName(), serviceUrl, this.renew, this.gateway); System.out.println("RemoteAuthenticationFilter urlToRedirectTo\t"+urlToRedirectTo+",loginUrl\t"+localLoginUrl); // 加入localLoginUrl urlToRedirectTo += (urlToRedirectTo.contains("?") ? "&" : "?") + "loginUrl=" + URLEncoder.encode(localLoginUrl, "utf-8"); if (log.isDebugEnabled()) { log.debug("redirecting to \"" + urlToRedirectTo + "\""); } response.sendRedirect(urlToRedirectTo); return; } if (session != null) { log.debug("removing gateway attribute from session"); session.setAttribute(CONST_CAS_GATEWAY, null); } filterChain.doFilter(request, response); } public final void setRenew(final boolean renew) { this.renew = renew; } public final void setGateway(final boolean gateway) { this.gateway = gateway; } public final void setCasServerLoginUrl(final String casServerLoginUrl) { this.casServerLoginUrl = casServerLoginUrl; } public final void setLocalLoginUrl(String localLoginUrl) { this.localLoginUrl = localLoginUrl; } }
以上黃色背景程式碼為修改部分,其餘程式碼均拷貝自org.jasig.cas.client.authentication.AuthenticationFilter,可以看到我們為原有的認證過濾器增加了一個引數localLoginUrl。在

applicationContext-cas.xml,將authenticationFilter指向的類換成我們的RemoteAuthenticationFilter,並新增localLoginUrl 引數

applicationContext-cas.xml:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       ">
	<context:property-placeholder location="classpath:cas-client.properties" />
	
	<!-- 負責使用者的認證 -->
	<bean name="authenticationFilter"
		class="com.talkingdatawebsite.sso.filter.RemoteAuthenticationFilter">
		<property name="localLoginUrl" value="${local.loginUrl}"></property>
		<property name="casServerLoginUrl" value="${cas.server.loginUrl}" />
		<property name="renew" value="${cas.server.renew}" />
		<property name="gateway" value="${cas.server.gateway}" />
		<property name="service" value="${cas.client.serverName}" />
	</bean>

	<!-- 對認證ticket進行校驗 -->
	<bean name="ticketValidationFilter"
		class="org.jasig.cas.client.validation.Cas20ProxyReceivingTicketValidationFilter">
		<property name="service" value="${cas.client.serverName}" />
		<property name="ticketValidator">
			<bean class="org.jasig.cas.client.validation.Cas10TicketValidator">
				<constructor-arg index="0" value="${cas.server.url}" />
			</bean>
		</property>
	</bean>

</beans>

cas-client.properties:
cas.server.url=https://www.talkingdata.net:8446/
cas.server.loginUrl=https://www.talkingdata.net:8446/remoteLogin
local.loginUrl=http://localhost:8080/index.jsp
cas.client.serverName=http://localhost:8080/index.jsp
cas.server.renew=false 
cas.server.gateway=false 

最後來看看我們的登入頁面login.jsp:
<%@ page language="java" contentType="text/html; charset=utf-8"
	pageEncoding="utf-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>遠端CAS客戶端登陸頁面</title>
<link rel="stylesheet" type="text/css"
	href="<%=request.getContextPath()%>/styles/main.css" />
<script type="text/javascript">
	function getParam(name) {
		var queryString = window.location.search;
		var param = queryString.substring(1, queryString.length).split("&");
		for ( var i = 0; i < param.length; i++) {
			var keyValue = param[i].split("=");
			if (keyValue[0] == name){
				return keyValue[1];
			}
		}
		return null;
	}
	function init() {
		// 顯示異常資訊
		var error = getParam("errorMessage");
		if (error) {
			document.getElementById("errorMessage").innerHTML = decodeURIComponent(error);
		}
		// 注入service
		var service = getParam("service");
		if (service)
			document.getElementById("service").value = decodeURIComponent(service);
// 		document.getElementById("service").value = service;
		else
			document.getElementById("service").value = location.href;
	}
</script>
</head>
<body >
	<h1>遠端CAS客戶端client1.1登陸頁面</h1>
	<%
		if (request.getRemoteUser() == null) {
	%>
	<div id="errorMessage"></div>
	<form id="myLoginForm" action="https://www.talkingdata.net:8446/remoteLogin"
		method="post">
		<input type="hidden" id="service" name="service" value="">
		<input type="hidden" name="loginUrl" value="http://localhost:8082/login.jsp">
		<input type="hidden" name="loginsubmit" value="true" />
		<table>
			<tr>
				<td>使用者名稱:</td>
				<td><input type="text" id="username" name="username"></td>
			</tr>
			<tr>
				<td>密  碼:</td>
				<td><input type="password" id="password" name="password"></td>
			</tr>
			<tr>
				<td colspan="2"><input type="submit" value="登陸" /></td>
			</tr>
		</table>
	</form>
	<script type="text/javascript">
		init()
	</script>
	<%
		} else {
	%>
	<div class="welcome">
		您好:<%=request.getRemoteUser()%></div>
	<div id="logout">
			<a href="https://www.talkingdata.net:8446/logout?service=http://localhost:8082/">退出</a>    
			<a href="http://localhost:8081">我要去client1</a>
	</div>
	<%
		}
	%>
</body>
</html>

以上黃色背景字中,我們首先將表單action指向伺服器端remoteLogin,然後在裡面設定了兩個重要的hidden域以傳遞  loginUrl和submit引數,前者用於告訴伺服器失敗後轉向何處,後者告訴伺服器端webflow現在要進行提交而不是TGT認證請求。

這樣我們的自定義客戶端遠端登陸頁面就完成了,現在趕快測試一下吧~。