單點登錄之CAS SSO從入門到精通(第三天)

開場白

各位新年好,上海的新年好冷,冷到我手發抖。

做好準備全身心投入到新的學習和工作中去了嗎?因爲今天開始的教程很“變態”啊,我們要完成下面幾件事:

  1. 自定義CAS SSO登錄界面
  2. 在CAS SSO登錄界面增加我們自定義的登錄用元素
  3. 使用LDAP帶出登錄用戶在LDAP內存儲的更多的信息
  4. 實現CAS SSO支持多租戶登錄的功能
好,開始正文!

正文

上次我們說到了CAS SSO的一些基本用法如:連數據庫怎麼用,連LDAP怎麼用,這次我們要來講一個網上幾乎沒有人去過多涉及到的一個問題即:在多租戶的環境下我們的cas sso如何去更好的支持,即cas sso multi tentant 的問題,這個問題在很多國外的一些網站包括CAS的官網也很少有人得到解決,在此呢我們把它給徹底的解決掉吧,呵呵。

多租戶環境下的單點登錄

什麼是多租戶環境呢?舉個例子吧:


我們知道,在有一些雲平臺或者是電商中的B2B中,經常會存在這樣的情況:


在同一個域名下如taobao.com下會有多個商鋪(就是租戶)好比:

  • taobao.com/company_101/張飛
  • taobao.com/company_102/張飛
  • taobao.com/company_103/趙雲
看張飛這個名字,看!!!

不同的company(租戶)下有着相同的用戶,但其實這是兩個不用的用戶,中國同名同姓的人多了去了,對吧,這時company_101的張飛登錄是因該只看到它所屬的company_101這個租戶下所有的數據和信息吧,而不能跑到company_102中看到別人家的信息,對吧?

國外很多解決方案說是在我們的CAS SSO的配置文件裏在綁定LDAP的context時寫上多條這樣的東西:

<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"
					p:filter="uid=%u" p:searchBase="o=101,o=company,dc=sky,dc=org"
					p:contextSource-ref="contextSource" />

<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"
					p:filter="uid=%u" p:searchBase="o=102,o=company,dc=sky,dc=org"
					p:contextSource-ref="contextSource" />

<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"
					p:filter="uid=%u" p:searchBase="o=103,o=company,dc=sky,dc=org"
					p:contextSource-ref="contextSource" />

可是,我們想想:

  1. 我們的租戶在我們的後臺系統中是自動“開戶”的,companyid是一個自動增加的,我從companyid_101現在增加到了companyid_110時,你是不是每次用戶一開戶,你就要去手動改這個CAS SSO中的配置文件呢?
  2. 如果你不嫌煩,好好好,你夠狠,你就手工改吧!但是當你每次在配置文件中新增一條配置語句時,你的CAS SSO是不是要斷服務重啓啊?那你還怎麼做到24*7的這種不間斷服務啊疑問
一般來說,我們的開戶是用程序自動寫入LDAP中去的,即LDAP中的company_101, company_102, company_103是由程序自動生成的,那我們的程序就需要能夠讓用戶在登錄後臺B2B系統時自動可以根據用戶選擇的租戶來爲用戶正確登錄的這麼一種自動識別功能,就好比下面這樣的一個登錄界面:



看到這個界面了嗎?

對的,這個就是CAS SSO的主登錄界面,我把它都給改了,還加入了支持多租戶登錄的功能,我們今天就要來講這個功能是怎麼做出來的,包括如何去定製自己的CAS SSO的登錄界面。

再來看看用於今天練習的我們在LDAP中的組織結構是怎麼樣的吧。


看到上面這張圖了吧,這就是我說的“多租戶”的概念,大家應該記得我們在CAS SSO第二天中怎麼去拿CAS SSO綁定LDAP中的一條UserDN然後去搜索的吧?

<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"
					p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org"
					p:contextSource-ref="contextSource" />
對吧!!!


現在我們要做到的就是:

p:searchBase="xxx.xxx.xx"

這條要做成動態的,比如說:


  • 用戶是company_id=101的,這時這個p:searchBase就應該變爲:“p:searchBase="uid=sky,o=101,o=company,dc=sky,dc=org"
  • 用戶是company_id=102的,這時這個p:searchBase就應該變爲:“p:searchBase="uid=jason,o=102,o=company,dc=sky,dc=org"

前面我們提到過,這些配置是放在XML文件中的,因此每次增加一個”租戶“我們要手工在XML配置文件中新增一條,這個不現實,它是實現不了我們的24*7的這種服務的要求的,我們要做的是可以讓這個p:searchBase能夠動態的去組建這個userDN,所以重點是要解決這個問題。


該問題在國外的YALE CAS論壇上有兩種解決方案:

  • 一種是直接通過CAS的登錄界面然後在輸入用戶名時要求用戶以這種形式“uid=sky,o=101"去輸入它的用戶名,這種做法先不去說會造成用戶登錄時的困擾,而且CAS SSO的登錄界面也不支持這樣格式的用戶名輸入。
  • 一種就是很笨的在CAS SSO的配置文件中綁定多個p:searchBase,這個方法已經被我們否掉了。
因此,筆者在這邊要提的將是獨創的可以做到全動態的去根據用戶名,密碼和所該用戶所屬組織自動在後臺創建p:searchBase的最完美的解決方案,下面我們就開始吧。

創建工程

我們這次是要在CAS SSO這個產品上做擴展了,爲此,我們不能再像我們第一天和第二天中那樣直接拿個文本編輯器去改CAS SSO裏的配置文件了,我們需要創建一個eclipse工程,來看我們的eclipse工程。



look,今天我們把這個cas-server放到了eclipse工程中去了,然後在eclipse裏隨改隨測試,現在我們就來講述如何創建這個工程以使得cas server可以運行在我們的eclipse的工程中。
因此我們在eclipse中新建一個java工程-是java工程你可千萬不要建成j2ee工程啊,然後按照上圖建立相應的目錄。

CAS SERVER工程的組建

導入所有的配置文件

這是我們在第一天,第二天中佈署在tomcat下的cas server工程的目錄:

D:\tomcat\webapps\cas-server\WEB-INF\classes

把這個目錄下所有的內容,除去以下2個目錄:
  • org
  • META-INF
外所有的東西統統拷貝入eclipse中的cas-server工程中的src/main/resources目錄下

構建WEB-INF目錄

將D:\tomcat\webapps\cas-server\WEB-INF目錄下這幾個目錄放入cas-server工程的src/main/webapp/WEB-INF目錄下



構建cas-server基本源碼

解壓開我們下載的”cas-server-3.5.2-release"包,內含源碼,它位於這樣的一個目錄cas-server-3.5.2\cas-server-webapp\src\main\java“

將這個目錄下所有的文件置於cas-server工程的src/main/java目錄下


並在eclipse工程中做如下設置

此處需要注意的是我們把:
  • src/main/java
  • src/main/resources
這兩個目錄做成編譯路徑,而src/main/webapp不作爲編譯路徑


別忘了把所有的src/main/webapp/WEB-INF/lib目錄下的jar加到cas-server工程的Libraries中去。



構建webapp目錄

將我們在第一天、第二天中佈署在tomcat中的case-server中以下這些目錄

拷貝到eclipse的cas-server工程中的src/main/webapp目錄

CAS SSO在jboss/weblogic下的bug的修正

由於我們的eclipse中的cas-server將和我們的cas-sample-site1以及cas-sample-site2啓動在jboss下,因此cas sso在jboss或者是在weblogic下有兩個小問題,在此需要修正。

  1. META-INF文件內的persistence.xml中報HSQLDialect錯誤
  2. 報log4jConfiguration.xml文件在啓動時找不到的錯誤
下面我們來看如何修正這兩個小BUG。

修正CAS SSO的persistence.xml文件中的HSQLDialect錯誤

這是原始的/META-INF/persistence.xml文件的內容:
        <class>org.jasig.cas.services.AbstractRegisteredService</class>
        <class>org.jasig.cas.services.RegexRegisteredService</class>
        <class>org.jasig.cas.services.RegisteredServiceImpl</class>
        <class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class>
        <class>org.jasig.cas.ticket.ServiceTicketImpl</class>
        <class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class>

我們在文件最後加入以下配置代碼
        <class>org.jasig.cas.services.AbstractRegisteredService</class>
        <class>org.jasig.cas.services.RegexRegisteredService</class>
        <class>org.jasig.cas.services.RegisteredServiceImpl</class>
        <class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class>
        <class>org.jasig.cas.ticket.ServiceTicketImpl</class>
        <class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class>
        <properties>
	      <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
	</properties>
這是完整的改完後的persistence.xml文件的內容:
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"
        version="2.0">

	<persistence-unit name="CasPersistence" transaction-type="RESOURCE_LOCAL">
        <class>org.jasig.cas.services.AbstractRegisteredService</class>
        <class>org.jasig.cas.services.RegexRegisteredService</class>
        <class>org.jasig.cas.services.RegisteredServiceImpl</class>
        <class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class>
        <class>org.jasig.cas.ticket.ServiceTicketImpl</class>
        <class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class>
        <properties>
		<property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />
	</properties>
	</persistence-unit>
</persistence>

改完後請保存。

修正CAS SSO中log4jConfiguration.xml文件在啓動時找不到的錯誤

找到eclipse的cas-server工程中WEB-INF/spring-configuration/log4jConfiguration.xml文件,將這段內容註釋掉
     <bean id="log4jInitialization" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
         <property name="targetClass" value="org.springframework.util.Log4jConfigurer"/>
         <property name="targetMethod" value="initLogging"/>
         <property name="arguments">
             <list>
                 <value>${log4j.config.location:classpath:log4j.xml}</value>
                 <value>${log4j.refresh.interval:60000}</value>
             </list>
         </property>
    </bean>

整個log4jConfiguration.xml文件修改後是這個樣子的:
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:p="http://www.springframework.org/schema/p"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

	
	<!-- 
     <bean id="log4jInitialization" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
         <property name="targetClass" value="org.springframework.util.Log4jConfigurer"/>
         <property name="targetMethod" value="initLogging"/>
         <property name="arguments">
             <list>
                 <value>${log4j.config.location:classpath:log4j.xml}</value>
                 <value>${log4j.refresh.interval:60000}</value>
             </list>
         </property>
    </bean>
	-->
</beans>

改完後請保存。

將CAS-SERVER從eclipse java工程改爲j2ee工程

右鍵單擊cas-sso工程,選擇project properties,然後選擇project facet,按照如下截圖來做選擇。


組裝可在eclipse中啓動的cas-server web工程

右鍵單擊cas-sso工程,選擇project properties,然後選擇Deployment Assembly,這個基本功我已經在 通向架構師的道路(第二十天)萬能框架spring(二)maven結合spring與ibatis中詳細講述過這個Deployment Assembly是幹什麼用的了。


在eclipse中啓動cas-server工程

一切無誤後請在eclipse中啓動cas-sso吧。


開始修改源碼

如何讓cas-server支持動態的p:searchBase呢

我們的p:searchBase從這一層o=company,dc=sky,dc=org開始要進行動態組裝,因此我們將在deployConfiguration.xml文件中將我們的ldap的p:searchBase的綁定改成如下:

p:searchBase="o=company,dc=sky,dc=org",然後我們使用程序動態組建o=company,dc=sky,dc=org之前的內容到底是該“o=101”呢還是因該是“o=102” 這樣的串。

爲cas server的登錄增加一個項


原來的cas sso的登錄項只有兩個屬性:
  • username
  • password
我們需要增加一個companyid,用於判斷當前登錄的用戶是屬於哪個租戶的。

新建CASCredential類

public class CASCredential extends RememberMeUsernamePasswordCredentials {
	private static final long serialVersionUID = 1L;

	private Map<String, Object> param;


	private String companyid;

	/**
	 * @return the companyid
	 */
	public String getCompanyid() {
		return companyid;
	}

	/**
	 * @param companyid the companyid to set
	 */
	public void setCompanyid(String companyid) {
		this.companyid = companyid;
	}

	public Map<String, Object> getParam() {
		return param;
	}

	public void setParam(Map<String, Object> param) {
		this.param = param;
	}
}

這就是我們擴展的CASCredential類,該類除了擁有原來CAS SSO基本credential中的username和password兩個屬性外還有一個叫companyid的屬性。

將新增的companyid綁定至cas sso的登錄頁面

修改src/main/webapp/WEB-INF/login-webflow.xml文件,找到以下這段:
<view-state id="viewLoginForm" view="casLoginView" model="credentials">
        <binder>
            <binding property="username" />
            <binding property="password" />
        </binder>

將其改成:
<view-state id="viewLoginForm" view="casLoginView" model="credentials">
        <binder>
            <binding property="username" />
            <binding property="password" />
            <binding property="companyid"/>
        </binder>

擴展CAS SSO登錄頁面的submit行爲以支持我們在頁面中新增的companyid屬性可以被提交到CAS SSO的後臺

新建一個類CASAuthenticationViaFormAction,內容如下:
package org.sky.cas.auth;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.validation.constraints.NotNull;

import org.jasig.cas.CentralAuthenticationService;
import org.jasig.cas.authentication.handler.AuthenticationException;
import org.jasig.cas.authentication.principal.Credentials;
import org.jasig.cas.authentication.principal.Service;
import org.jasig.cas.ticket.TicketException;
import org.jasig.cas.web.bind.CredentialsBinder;
import org.jasig.cas.web.support.WebUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.binding.message.MessageBuilder;
import org.springframework.binding.message.MessageContext;
import org.springframework.util.StringUtils;
import org.springframework.web.util.CookieGenerator;
import org.springframework.webflow.core.collection.MutableAttributeMap;
import org.springframework.webflow.execution.RequestContext;

@SuppressWarnings("deprecation")
public class CASAuthenticationViaFormAction {
	/**
	 * Binder that allows additional binding of form object beyond Spring
	 * defaults.
	 */
	private CredentialsBinder credentialsBinder;

	/** Core we delegate to for handling all ticket related tasks. */
	@NotNull
	private CentralAuthenticationService centralAuthenticationService;

	@NotNull
	private CookieGenerator warnCookieGenerator;

	protected Logger logger = LoggerFactory.getLogger(getClass());

	public final void doBind(final RequestContext context, final Credentials credentials) throws Exception {
		final HttpServletRequest request = WebUtils.getHttpServletRequest(context);

		if (this.credentialsBinder != null && this.credentialsBinder.supports(credentials.getClass())) {
			this.credentialsBinder.bind(request, credentials);
		}
	}

	public final String submit(final RequestContext context, final Credentials credentials, final MessageContext messageContext)
			throws Exception {
		String companyid = "";
		// Validate login ticket
		final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context);
		final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context);
		if (credentials instanceof CASCredential) {
			String companyCode = "compnayid";
			CASCredential rmupc = (CASCredential) credentials;
			companyid = rmupc.getCompanyid();

		}

		if (!authoritativeLoginTicket.equals(providedLoginTicket)) {
			this.logger.warn("Invalid login ticket " + providedLoginTicket);
			final String code = "INVALID_TICKET";
			messageContext.addMessage(new MessageBuilder().error().code(code).arg(providedLoginTicket).defaultText(code).build());
			return "error";
		}

		final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);
		final Service service = WebUtils.getService(context);
		if (StringUtils.hasText(context.getRequestParameters().get("renew")) && ticketGrantingTicketId != null && service != null) {

			try {
				final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId,
						service, credentials);
				WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);
				putWarnCookieIfRequestParameterPresent(context);
				return "warn";
			} catch (final TicketException e) {
				if (isCauseAuthenticationException(e)) {
					populateErrorsInstance(e, messageContext);
					return getAuthenticationExceptionEventId(e);
				}

				this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);
				if (logger.isDebugEnabled()) {
					logger.debug("Attempted to generate a ServiceTicket using renew=true with different credentials", e);
				}
			}
		}

		try {

			CASCredential rmupc = (CASCredential) credentials;
			WebUtils.putTicketGrantingTicketInRequestScope(context,
					centralAuthenticationService.createTicketGrantingTicket(rmupc));
			putWarnCookieIfRequestParameterPresent(context);

			return "success";
		} catch (final TicketException e) {
			populateErrorsInstance(e, messageContext);
			if (isCauseAuthenticationException(e))
				return getAuthenticationExceptionEventId(e);
			return "error";
		}
	}

	private void populateErrorsInstance(final TicketException e, final MessageContext messageContext) {

		try {
			messageContext.addMessage(new MessageBuilder().error().code(e.getCode()).defaultText(e.getCode()).build());
		} catch (final Exception fe) {
			logger.error(fe.getMessage(), fe);
		}
	}

	private void putWarnCookieIfRequestParameterPresent(final RequestContext context) {
		final HttpServletResponse response = WebUtils.getHttpServletResponse(context);

		if (StringUtils.hasText(context.getExternalContext().getRequestParameterMap().get("warn"))) {
			this.warnCookieGenerator.addCookie(response, "true");
		} else {
			this.warnCookieGenerator.removeCookie(response);
		}
	}

	private AuthenticationException getAuthenticationExceptionAsCause(final TicketException e) {
		return (AuthenticationException) e.getCause();
	}

	private String getAuthenticationExceptionEventId(final TicketException e) {
		final AuthenticationException authEx = getAuthenticationExceptionAsCause(e);

		if (this.logger.isDebugEnabled())
			this.logger.debug("An authentication error has occurred. Returning the event id " + authEx.getType());

		return authEx.getType();
	}

	private boolean isCauseAuthenticationException(final TicketException e) {
		return e.getCause() != null && AuthenticationException.class.isAssignableFrom(e.getCause().getClass());
	}

	public final void setCentralAuthenticationService(final CentralAuthenticationService centralAuthenticationService) {
		this.centralAuthenticationService = centralAuthenticationService;
	}

	/**
	 * Set a CredentialsBinder for additional binding of the HttpServletRequest
	 * to the Credentials instance, beyond our default binding of the
	 * Credentials as a Form Object in Spring WebMVC parlance. By the time we
	 * invoke this CredentialsBinder, we have already engaged in default binding
	 * such that for each HttpServletRequest parameter, if there was a JavaBean
	 * property of the Credentials implementation of the same name, we have set
	 * that property to be the value of the corresponding request parameter.
	 * This CredentialsBinder plugin point exists to allow consideration of
	 * things other than HttpServletRequest parameters in populating the
	 * Credentials (or more sophisticated consideration of the
	 * HttpServletRequest parameters).
	 *
	 * @param credentialsBinder the credentials binder to set.
	 */
	public final void setCredentialsBinder(final CredentialsBinder credentialsBinder) {
		this.credentialsBinder = credentialsBinder;
	}

	public final void setWarnCookieGenerator(final CookieGenerator warnCookieGenerator) {
		this.warnCookieGenerator = warnCookieGenerator;
	}
}

這個類很簡單,主要是第59行到第64行的:
if (credentials instanceof CASCredential) {
			String companyCode = "compnayid";
			CASCredential rmupc = (CASCredential) credentials;
			companyid = rmupc.getCompanyid();

}

以及第98行到第100行的:
			CASCredential rmupc = (CASCredential) credentials;
			WebUtils.putTicketGrantingTicketInRequestScope(context,
					centralAuthenticationService.createTicketGrantingTicket(rmupc));
它告訴了CAS SSO使用我們自定義的CASCredential來驗證用戶在CAS SSO中的登錄信息,而不是原來CAS SSO默認的UsernameAndPasswordCredential。

把”CASAuthenticationViaFormAction“類註冊給CAS SSO,告訴CAS SSO在登錄頁面點擊”登錄“按鈕後能夠使用這個我們自定義的submit action:

修改配置文件:src/main/webapp/WEB-INF/cas-servlet.xml

找到以下這行:
<bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
        p:centralAuthenticationService-ref="centralAuthenticationService"
        p:warnCookieGenerator-ref="warnCookieGenerator"/>
把它註釋掉改成:
  <!-- <bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"
        p:centralAuthenticationService-ref="centralAuthenticationService"
        p:warnCookieGenerator-ref="warnCookieGenerator"/>
 -->
 
  <bean id="authenticationViaFormAction"
		class="org.sky.cas.auth.CASAuthenticationViaFormAction"
		p:centralAuthenticationService-ref="centralAuthenticationService"
		p:warnCookieGenerator-ref="warnCookieGenerator" />

此時,CAS SSO的登錄界面在用戶點擊submit按鈕時,就會使用我們自定義的這個CASAuthenticationViaFormAction類了。

增加p:searchBase使得CAS SSO的LDAP可以根據不同的companyid動態搜索用戶的功能

新增一個類CASLDAPAuthenticationHandler,代碼如下:

package org.sky.cas.auth;

import org.jasig.cas.adaptors.ldap.AbstractLdapUsernamePasswordAuthenticationHandler;
import org.jasig.cas.authentication.handler.AuthenticationException;
import org.jasig.cas.authentication.principal.UsernamePasswordCredentials;
import org.jasig.cas.util.LdapUtils;
import org.springframework.ldap.NamingSecurityException;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.NameClassPairCallbackHandler;
import org.springframework.ldap.core.SearchExecutor;

import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import java.util.ArrayList;
import java.util.List;

public class CASLDAPAuthenticationHandler extends AbstractLdapUsernamePasswordAuthenticationHandler {
	/** The default maximum number of results to return. */
	private static final int DEFAULT_MAX_NUMBER_OF_RESULTS = 1000;

	/** The default timeout. */
	private static final int DEFAULT_TIMEOUT = 1000;

	/** The search base to find the user under. */
	private String searchBase;

	/** The scope. */
	@Min(0)
	@Max(2)
	private int scope = SearchControls.ONELEVEL_SCOPE;

	/** The maximum number of results to return. */
	private int maxNumberResults = DEFAULT_MAX_NUMBER_OF_RESULTS;

	/** The amount of time to wait. */
	private int timeout = DEFAULT_TIMEOUT;

	/** Boolean of whether multiple accounts are allowed. */
	private boolean allowMultipleAccounts;

	protected final boolean authenticateUsernamePasswordInternal(final UsernamePasswordCredentials credentials)
			throws AuthenticationException {
		CASCredential rmupc = (CASCredential) credentials;
		final String companyid = rmupc.getCompanyid();
		final List<String> cns = new ArrayList<String>();

		final SearchControls searchControls = getSearchControls();

		final String transformedUsername = getPrincipalNameTransformer().transform(credentials.getUsername());
		final String filter = LdapUtils.getFilterWithValues(getFilter(), transformedUsername);
		try {
			this.getLdapTemplate().search(new SearchExecutor() {
				public NamingEnumeration executeSearch(final DirContext context) throws NamingException {
					String baseDN = "";
					if (companyid != null && companyid.trim().length() > 0) {
						baseDN = "o=" + companyid + "," + searchBase;
					} else {
						baseDN = searchBase;
					}
					//System.out.println("searchBase=====" + baseDN);
					return context.search(baseDN, filter, searchControls);
				}
			}, new NameClassPairCallbackHandler() {

				public void handleNameClassPair(final NameClassPair nameClassPair) {
					cns.add(nameClassPair.getNameInNamespace());
				}
			});
		} catch (Exception e) {
			log.error("search ldap error casue: " + e.getMessage(), e);
			return false;
		}
		if (cns.isEmpty()) {
			log.debug("Search for " + filter + " returned 0 results.");
			return false;
		}
		if (cns.size() > 1 && !this.allowMultipleAccounts) {
			log.warn("Search for " + filter + " returned multiple results, which is not allowed.");
			return false;
		}

		for (final String dn : cns) {
			DirContext test = null;
			String finalDn = composeCompleteDnToCheck(dn, credentials);
			try {
				this.log.debug("Performing LDAP bind with credential: " + dn);
				test = this.getContextSource().getContext(finalDn, getPasswordEncoder().encode(credentials.getPassword()));

				if (test != null) {
					return true;
				}
			} catch (final NamingSecurityException e) {
				log.debug("Failed to authenticate user {} with error {}", credentials.getUsername(), e.getMessage());
				return false;
			} catch (final Exception e) {
				this.log.error(e.getMessage(), e);
				return false;
			} finally {
				LdapUtils.closeContext(test);
			}
		}

		return false;
	}

	protected String composeCompleteDnToCheck(final String dn, final UsernamePasswordCredentials credentials) {
		return dn;
	}

	private SearchControls getSearchControls() {
		final SearchControls constraints = new SearchControls();
		constraints.setSearchScope(this.scope);
		constraints.setReturningAttributes(new String[0]);
		constraints.setTimeLimit(this.timeout);
		constraints.setCountLimit(this.maxNumberResults);

		return constraints;
	}

	/**
	 * Method to return whether multiple accounts are allowed.
	 * @return true if multiple accounts are allowed, false otherwise.
	 */
	protected boolean isAllowMultipleAccounts() {
		return this.allowMultipleAccounts;
	}

	/**
	 * Method to return the max number of results allowed.
	 * @return the maximum number of results.
	 */
	protected int getMaxNumberResults() {
		return this.maxNumberResults;
	}

	/**
	 * Method to return the scope.
	 * @return the scope
	 */
	protected int getScope() {
		return this.scope;
	}

	/**
	 * Method to return the search base.
	 * @return the search base.
	 */
	protected String getSearchBase() {
		return this.searchBase;
	}

	/**
	 * Method to return the timeout. 
	 * @return the timeout.
	 */
	protected int getTimeout() {
		return this.timeout;
	}

	public final void setScope(final int scope) {
		this.scope = scope;
	}

	/**
	 * @param allowMultipleAccounts The allowMultipleAccounts to set.
	 */
	public void setAllowMultipleAccounts(final boolean allowMultipleAccounts) {
		this.allowMultipleAccounts = allowMultipleAccounts;
	}

	/**
	 * @param maxNumberResults The maxNumberResults to set.
	 */
	public final void setMaxNumberResults(final int maxNumberResults) {
		this.maxNumberResults = maxNumberResults;
	}

	/**
	 * @param searchBase The searchBase to set.
	 */
	public final void setSearchBase(final String searchBase) {
		this.searchBase = searchBase;
	}

	/**
	 * @param timeout The timeout to set.
	 */
	public final void setTimeout(final int timeout) {
		this.timeout = timeout;
	}

	/**
	 * Sets the context source for LDAP searches.  This method may be used to
	 * support use cases like the following:
	 * <ul>
	 * <li>Pooling of LDAP connections used for searching (e.g. via instance
	 * of {@link org.springframework.ldap.pool.factory.PoolingContextSource}).</li>
	 * <li>Searching with client certificate credentials.</li>
	 * </ul>
	 * <p>
	 * If this is not defined, the context source defined by
	 * {@link #setContextSource(ContextSource)} is used.
	 *
	 * @param contextSource LDAP context source.
	 */
	public final void setSearchContextSource(final ContextSource contextSource) {
		setLdapTemplate(new LdapTemplate(contextSource));
	}

}

這個類的作用就是給src/main/webapp/WEB-INF/deployerConfiguration.xml中以下這段用的:
<property name="authenticationHandlers">
			<list>

				<bean
					class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
					p:httpClient-ref="httpClient" />

				<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"
					p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org"
					p:contextSource-ref="contextSource" />
			</list>
</property>

請注意代碼50行處:
final String companyid = rmupc.getCompanyid();

以及59行到69行處:
public NamingEnumeration executeSearch(final DirContext context) throws NamingException {
					String baseDN = "";
					if (companyid != null && companyid.trim().length() > 0) {
						baseDN = "o=" + companyid + "," + searchBase;
					} else {
						baseDN = searchBase;
					}
					//System.out.println("searchBase=====" + baseDN);
					return context.search(baseDN, filter, searchControls);
				}
}, new NameClassPairCallbackHandler() {

這就是在根據用戶在登錄界面中選擇的companyid不同,而動態的去重組這個searchBase,以使得這個searchBase可以是o=101,o=company,dc=sky,dc=org, 也可以是o=102,o=company,dc=sky,dc=org同時它也可以變成o=103,o=company,dc=sky,dc=org。

有了這個類我們要修改我們的src/main/webapp/WEB-INF/deployerConfiguration.xml文件了,注意這個bean中的寫法 ,已經被我修改掉了
	<bean id="authenticationManager" class="org.jasig.cas.authentication.AuthenticationManagerImpl">


		<property name="credentialsToPrincipalResolvers">
			<list>
				<bean class="org.sky.cas.auth.CASCredentialsToPrincipalResolver">
					<property name="attributeRepository" ref="attributeRepository" />
				</bean>
			</list>
		</property>


		<property name="authenticationHandlers">
			<list>

				<bean
					class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
					p:httpClient-ref="httpClient" />

				<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"
					p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org"
					p:contextSource-ref="contextSource" />
			</list>
		</property>
	</bean>

看到這個CASLDAPAuthenticationHandler類在這邊的作用了吧。

將LDAP中登錄用戶的其它信息也帶入到客戶端登錄成功後跳轉的頁面中去

我們知道,CAS SSO可以把username(uid)帶入到客戶端登錄成功後的頁面中去,可是一個uid在LDAP中還關聯着許多其它有用的信息如:email。 還有就是我們剛纔新增的companyid,我們也想把這些信息同時帶到客戶端登錄成功的畫面中去呢?

這邊就需要使用到CAS SSO中的一個特殊的屬性,它叫attributeRepository。

attributeRepository的作用

attributeRepository關聯着一個dao和一個resolver,它們的作用如下:
  • attributeDAO是用於根據searchBase在LDAP中定位到一條數據,然後把該條數據所有的屬性取出來用的一個工具類
  • credentialsToPrincipalResolvers,該類用於向客戶端(就是我們的cas-samples-site1/site2)返回用戶在CAS SSO中登錄畫面中輸入的登錄相關信息用的一個工具類
先來說attributeDAO的作用吧。

CASLdapPersonAttributeDao

比如說我們這邊想要把ldap中某個uid的mail屬性也帶給到客戶端中去

我們就要按照下面這段代碼來書寫CASLdapPersonAttributeDao類,該類擴展自”AbstractQueryPersonAttributeDao“類,它被置於”package org.jasig.services.persondir.support.ldap“包中,因爲該包中還有其它相關的此類需要”引用"的工具類,我們不想到處import來import去了,因此直接把這個我們自定義的attributeDao類就直接放置於該包中了。

但是,嘿嘿嘿,在package org.jasig.services.persondir.support.ldap包中沒有其它這個類需要引用的那些外部類,如下圖所示:



怎麼辦?

很簡單,直接找到cas-server 3.5.2的源碼,將這兩個外部類置於我們自定義的CASLdapPersonAttributeDao同一層的包路徑下即可,我會在本文結束後直接給出完整的eclipse中可運行的cas-server的全部源碼。

/**
 * Licensed to Jasig under one or more contributor license
 * agreements. See the NOTICE file distributed with this work
 * for additional information regarding copyright ownership.
 * Jasig licenses this file to you under the Apache License,
 * Version 2.0 (the "License"); you may not use this file
 * except in compliance with the License. You may obtain a
 * copy of the License at:
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on
 * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied. See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

package org.jasig.services.persondir.support.ldap;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import javax.naming.directory.SearchControls;

import org.apache.commons.lang.StringUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.cas.util.CASCredentialHelper;
import org.jasig.services.persondir.IPersonAttributes;
import org.jasig.services.persondir.support.AbstractQueryPersonAttributeDao;
import org.jasig.services.persondir.support.CaseInsensitiveAttributeNamedPersonImpl;
import org.jasig.services.persondir.support.CaseInsensitiveNamedPersonImpl;
import org.jasig.services.persondir.support.QueryType;
import org.sky.cas.auth.LdapPersonInfoBean;
import org.springframework.beans.factory.BeanCreationException;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.ldap.core.AttributesMapper;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.filter.EqualsFilter;
import org.springframework.ldap.filter.Filter;
import org.springframework.ldap.filter.LikeFilter;
import org.springframework.util.Assert;

/**
 * LDAP implementation of {@link org.jasig.services.persondir.IPersonAttributeDao}.
 * 
 * In the case of multi valued attributes a {@link java.util.List} is set as the value.
 * 
 * <br>
 * <br>
 * Configuration:
 * <table border="1">
 *     <tr>
 *         <th align="left">Property</th>
 *         <th align="left">Description</th>
 *         <th align="left">Required</th>
 *         <th align="left">Default</th>
 *     </tr>
 *     <tr>
 *         <td align="right" valign="top">searchControls</td>
 *         <td>
 *             Set the {@link SearchControls} used for executing the LDAP query.
 *         </td>
 *         <td valign="top">No</td>
 *         <td valign="top">Default instance with SUBTREE scope.</td>
 *     </tr>
 *     <tr>
 *         <td align="right" valign="top">baseDN</td>
 *         <td>
 *             The base DistinguishedName to use when executing the query filter.
 *         </td>
 *         <td valign="top">No</td>
 *         <td valign="top">""</td>
 *     </tr>
 *     <tr>
 *         <td align="right" valign="top">contextSource</td>
 *         <td>
 *             A {@link ContextSource} from the Spring-LDAP framework. Provides a DataSource
 *             style object that this DAO can retrieve LDAP connections from.
 *         </td>
 *         <td valign="top">Yes</td>
 *         <td valign="top">null</td>
 *     </tr>
 *     <tr>
 *         <td align="right" valign="top">setReturningAttributes</td>
 *         <td>
 *             If the ldap attributes set in the ldapAttributesToPortalAttributes Map should be copied
 *             into the {@link SearchControls#setReturningAttributes(String[])}. Setting this helps reduce
 *             wire traffic of ldap queries.
 *         </td>
 *         <td valign="top">No</td>
 *         <td valign="top">true</td>
 *     </tr>
 *     <tr>
 *         <td align="right" valign="top">queryType</td>
 *         <td>
 *             How multiple attributes in a query should be concatenated together. The other option is OR.
 *         </td>
 *         <td valign="top">No</td>
 *         <td valign="top">AND</td>
 *     </tr>
 * </table>
 * 
 * @author [email protected]
 * @author Eric Dalquist
 * @version $Revision$ $Date$
 * @since uPortal 2.5
 */
public class CASLdapPersonAttributeDao extends AbstractQueryPersonAttributeDao<LogicalFilterWrapper> implements InitializingBean {
	private static final Pattern QUERY_PLACEHOLDER = Pattern.compile("\\{0\\}");
	private final static AttributesMapper MAPPER = new AttributeMapAttributesMapper();
	protected final Log logger = LogFactory.getLog(getClass());
	/**
	 * The LdapTemplate to use to execute queries on the DirContext
	 */
	private LdapTemplate ldapTemplate = null;

	private String baseDN = "";
	private String queryTemplate = null;
	private ContextSource contextSource = null;
	private SearchControls searchControls = new SearchControls();
	private boolean setReturningAttributes = true;
	private QueryType queryType = QueryType.AND;

	public CASLdapPersonAttributeDao() {
		this.searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);
		this.searchControls.setReturningObjFlag(false);
	}

	/* (non-Javadoc)
	 * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()
	 */
	public void afterPropertiesSet() throws Exception {
		final Map<String, Set<String>> resultAttributeMapping = this.getResultAttributeMapping();
		if (this.setReturningAttributes && resultAttributeMapping != null) {
			this.searchControls.setReturningAttributes(resultAttributeMapping.keySet().toArray(
					new String[resultAttributeMapping.size()]));
		}

		if (this.contextSource == null) {
			throw new BeanCreationException("contextSource must be set");
		}
	}

	/* (non-Javadoc)
	 * @see org.jasig.services.persondir.support.AbstractQueryPersonAttributeDao#appendAttributeToQuery(java.lang.Object, java.lang.String, java.util.List)
	 */
	@Override
	protected LogicalFilterWrapper appendAttributeToQuery(LogicalFilterWrapper queryBuilder, String dataAttribute,
			List<Object> queryValues) {
		if (queryBuilder == null) {
			queryBuilder = new LogicalFilterWrapper(this.queryType);
		}

		for (final Object queryValue : queryValues) {
			String queryValueString = queryValue == null ? null : queryValue.toString();

			LdapPersonInfoBean person = new LdapPersonInfoBean();
			//person = CASCredentialHelper.getPersoninfoFromCredential(queryValueString);
			//queryValueString = person.getUsername();
			person = CASCredentialHelper.getPersoninfoFromCredential(queryValueString);
			queryValueString=person.getUsername();
			if (StringUtils.isNotBlank(queryValueString)) {
				final Filter filter;
				if (!queryValueString.contains("*")) {
					filter = new EqualsFilter(dataAttribute, queryValueString);
				} else {
					filter = new LikeFilter(dataAttribute, queryValueString);
				}
				queryBuilder.append(filter);
			}
		}

		return queryBuilder;
	}

	/* (non-Javadoc)
	 * @see org.jasig.services.persondir.support.AbstractQueryPersonAttributeDao#getPeopleForQuery(java.lang.Object, java.lang.String)
	 */

	@Override
	protected List<IPersonAttributes> getPeopleForQuery(LogicalFilterWrapper queryBuilder, String queryUserName) {
		LdapPersonInfoBean ldapPerson = new LdapPersonInfoBean();
		ldapPerson = CASCredentialHelper.getPersoninfoFromCredential(queryUserName);
		final String generatedLdapQuery = queryBuilder.encode();
		//If no query is generated return null since the query cannot be run
		if (StringUtils.isBlank(generatedLdapQuery)) {
			return null;
		}

		//Insert the generated query into the template if it is configured
		final String ldapQuery;
		if (this.queryTemplate == null) {
			ldapQuery = generatedLdapQuery;
		} else {
			final Matcher queryMatcher = QUERY_PLACEHOLDER.matcher(this.queryTemplate);
			ldapQuery = queryMatcher.replaceAll(generatedLdapQuery);
		}
		String searchBase = "";
		if (ldapPerson.getCompanyid().trim().length() > 0) {
			searchBase = "o=" + ldapPerson.getCompanyid() + "," + baseDN;
		} else {
			searchBase = baseDN;
		}
		logger.info("searchBase=====" + searchBase);
		//Execute the query
		List<Map<String, List<Object>>> queryResults = new ArrayList<Map<String, List<Object>>>();
		try {
			queryResults = this.ldapTemplate.search(searchBase, ldapQuery, this.searchControls, MAPPER);
		} catch (Exception e) {
			logger.error(
					"search ldap with [searchBase===" + searchBase + "]  [ldapQuery====" + ldapQuery + "], caused by: "
							+ e.getMessage(), e);
		}
		final List<IPersonAttributes> peopleAttributes = new ArrayList<IPersonAttributes>(queryResults.size());
		for (final Map<String, List<Object>> queryResult : queryResults) {
			IPersonAttributes person;
			//if (ldapPerson.getUsername() != null) {
			if (queryUserName != null && queryUserName.trim().length() > 0) {
				//person = new CaseInsensitiveNamedPersonImpl(ldapPerson.getUsername(), queryResult);
				person = new CaseInsensitiveNamedPersonImpl(queryUserName, queryResult);
			} else {
				//Create the IPersonAttributes doing a best-guess at a userName attribute
				String userNameAttribute = this.getConfiguredUserNameAttribute();
				person = new CaseInsensitiveAttributeNamedPersonImpl(userNameAttribute, queryResult);
			}

			peopleAttributes.add(person);
		}

		return peopleAttributes;
	}

	/**
	 * @see javax.naming.directory.SearchControls#getTimeLimit()
	 * @deprecated Set the property on the {@link SearchControls} and set that via {@link #setSearchControls(SearchControls)}
	 */
	@Deprecated
	public int getTimeLimit() {
		return this.searchControls.getTimeLimit();
	}

	/**
	 * @see javax.naming.directory.SearchControls#setTimeLimit(int)
	 * @deprecated
	 */
	@Deprecated
	public void setTimeLimit(int ms) {
		this.searchControls.setTimeLimit(ms);
	}

	/**
	 * @return The base distinguished name to use for queries.
	 */
	public String getBaseDN() {
		return this.baseDN;
	}

	/**
	 * @param baseDN The base distinguished name to use for queries.
	 */
	public void setBaseDN(String baseDN) {
		if (baseDN == null) {
			baseDN = "";
		}

		this.baseDN = baseDN;
	}

	/**
	 * @return The ContextSource to get DirContext objects for queries from.
	 */
	public ContextSource getContextSource() {
		return this.contextSource;
	}

	/**
	 * @param contextSource The ContextSource to get DirContext objects for queries from.
	 */
	public synchronized void setContextSource(final ContextSource contextSource) {
		Assert.notNull(contextSource, "contextSource can not be null");
		this.contextSource = contextSource;
		this.ldapTemplate = new LdapTemplate(this.contextSource);
	}

	/**
	 * Sets the LdapTemplate, and thus the ContextSource (implicitly).
	 *
	 * @param ldapTemplate the LdapTemplate to query the LDAP server from.  CANNOT be NULL.
	 */
	public synchronized void setLdapTemplate(final LdapTemplate ldapTemplate) {
		Assert.notNull(ldapTemplate, "ldapTemplate cannot be null");
		this.ldapTemplate = ldapTemplate;
		this.contextSource = this.ldapTemplate.getContextSource();
	}

	/**
	 * @return Search controls to use for LDAP queries
	 */
	public SearchControls getSearchControls() {
		return this.searchControls;
	}

	/**
	 * @param searchControls Search controls to use for LDAP queries
	 */
	public void setSearchControls(SearchControls searchControls) {
		Assert.notNull(searchControls, "searchControls can not be null");
		this.searchControls = searchControls;
	}

	/**
	 * @return the queryType
	 */
	public QueryType getQueryType() {
		return queryType;
	}

	/**
	 * Type of logical operator to use when joining WHERE clause components
	 * 
	 * @param queryType the queryType to set
	 */
	public void setQueryType(QueryType queryType) {
		this.queryType = queryType;
	}

	public String getQueryTemplate() {
		return this.queryTemplate;
	}

	/**
	 * Optional wrapper template for the generated part of the query. Use {0} as a placeholder for where the generated query should be inserted.
	 */
	public void setQueryTemplate(String queryTemplate) {
		this.queryTemplate = queryTemplate;
	}
}

注意206到211行處的寫法
		String searchBase = "";
		if (ldapPerson.getCompanyid().trim().length() > 0) {
			searchBase = "o=" + ldapPerson.getCompanyid() + "," + baseDN;
		} else {
			searchBase = baseDN;
		}
		logger.info("searchBase=====" + searchBase);



CASLdapPersonAttributeDao類中需要使用到另外兩個我們自定義的工具類代碼如下:

CASCredentialHelper
package org.jasig.cas.util;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import java.io.StringReader;
import java.util.*;
import org.jdom.*;
import org.jdom.input.SAXBuilder;
import org.jdom.xpath.*;
import org.sky.cas.auth.LdapPersonInfoBean;
import org.xml.sax.InputSource;

public class CASCredentialHelper {
	public final static Log logger = LogFactory.getLog(CASCredentialHelper.class);

	public static LdapPersonInfoBean getPersoninfoFromCredential(String dnStr) {
		LdapPersonInfoBean person = new LdapPersonInfoBean();
		logger.debug("credential str======" + dnStr);
		try {
			if (dnStr != null) {
				//創建一個新的字符串
				String[] p_array = dnStr.split(",");
				if (p_array != null) {
					person.setCompanyid(p_array[1]);
					person.setUsername(p_array[0]);
				}
			}
		} catch (Exception e) {
			logger.error("get personinfo from DN: [:" + dnStr + "] error caused by: " + e.getMessage(), e);
		}
		return person;
	}

	public static void main(String[] args) throws Exception {
		StringBuffer sb = new StringBuffer();
		sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");
		sb.append("<CASCredential>");
		sb.append("<result>");
		sb.append("<loginid>sys</loginid>");
		sb.append("<companyid>401</companyid>");
		sb.append("<email>[email protected]</email>");
		sb.append("</result>");
		sb.append("</CASCredential>");
		getPersoninfoFromCredential(sb.toString());
	}
}



LdapPersonInfoBean
package org.sky.cas.auth;

import java.io.Serializable;

public class LdapPersonInfoBean implements Serializable {

	private String companyid = "";
	private String username = "";

	/**
	 * @return the companyid
	 */
	public String getCompanyid() {
		return companyid;
	}

	/**
	 * @param companyid the companyid to set
	 */
	public void setCompanyid(String companyid) {
		this.companyid = companyid;
	}

	/**
	 * @return the username
	 */
	public String getUsername() {
		return username;
	}

	/**
	 * @param username the username to set
	 */
	public void setUsername(String username) {
		this.username = username;
	}
}

以上這兩個類到底在幹什麼,大家不要急 ,我們接着看下面的這個CASCredentialsToPrincipalResolver類吧

CASCredentialsToPrincipalResolver類

該類的作用是這樣的:

一個客戶在CAS SSO登錄界面登錄了,然後輸入了相關的登錄信息,然後CAS SSO跳轉到客戶端的主界面中去,客戶端在主界面通過以下語句:
	AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal();
	String userName = principal.getName();

即可以得到CAS SSO轉發過來的合法登錄了的用戶名,可是,可是。。。CAS SSO默認只能帶一個username過來給到客戶端,而該成功登錄了的用戶的在LDAP中的其它屬性是通過以下語句得到的:
Map attributes = principal.getAttributes();
String email = (String) attributes.get("email");

現在問題來了,我們新增的companyid即不是該用戶在ldap中的一個屬性,又不能在req.getUserPrincipal();中帶過來,怎麼辦?

熊掌與魚兼得法 ,既可以把用戶在LDAP中其它屬性帶到客戶端又可以把客戶的登錄信息也帶到客戶端

因此我們需要定製CASCredentialsToPrincipalResolver這個類,來看該類的代碼:
package org.sky.cas.auth;

import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.cas.authentication.principal.AbstractPersonDirectoryCredentialsToPrincipalResolver;
import org.jasig.cas.authentication.principal.Credentials;

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

public class CASCredentialsToPrincipalResolver extends AbstractPersonDirectoryCredentialsToPrincipalResolver {
	public final Log logger = LogFactory.getLog(this.getClass());

	protected String extractPrincipalId(final Credentials credentials) {
		final CASCredential casCredential = (CASCredential) credentials;
		return buildCompCredential(casCredential.getUsername(), casCredential.getCompanyid());
	}

	/**
	 * Return true if Credentials are UsernamePasswordCredentials, false
	 * otherwise.
	 */
	public boolean supports(final Credentials credentials) {
		return credentials != null && CASCredential.class.isAssignableFrom(credentials.getClass());
	}

	public String buildCompCredential(String loginId, String companyId) {
		StringBuffer sb = new StringBuffer();
		sb.append(loginId).append(",");
		sb.append(companyId);
		return sb.toString();
	}
}
注意第23行和buildCompCredential方法,大家來看這個類原先是繼承自AbstractPersonDirectoryCredentialsToPrincipalResolver 類對吧,如果我們不自定這個類,CAS SSO有一個默認的Resolver,你們知道CAS SSO默認的這個Resolver是怎麼寫的嗎?

大家可以自己跟一下原碼,在原碼中,它是這樣寫的:
package org.sky.cas.auth;

import org.apache.commons.httpclient.UsernamePasswordCredentials;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.jasig.cas.authentication.principal.AbstractPersonDirectoryCredentialsToPrincipalResolver;
import org.jasig.cas.authentication.principal.Credentials;

import java.io.ByteArrayOutputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.JDOMException;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;

public class CASCredentialsToPrincipalResolver extends AbstractPersonDirectoryCredentialsToPrincipalResolver {
	public final Log logger = LogFactory.getLog(this.getClass());

	protected String extractPrincipalId(final Credentials credentials) {
		final CASCredential casCredential = (CASCredential) credentials;
		return casCredential.getUsername();
	}

	/**
	 * Return true if Credentials are UsernamePasswordCredentials, false
	 * otherwise.
	 */
}

看到了沒有,它只返回了一個username,因此,我們把這個類擴展了一下,使得CAS SSO在登錄成功後可以給客戶端返回這樣的一個字串:"username,companyid”。

通過這樣的方法以使得當客戶在登錄時輸入的那些並不屬於LDAP庫中存儲的信息也能夠被帶到客戶端中去,這樣的話我們在客戶端中如果通過以下這段代碼:
AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal();
String userName = principal.getName();

去試圖獲取從CAS SSO中帶來的登錄信息時,客戶端將會得到一個這樣的字串“username,companyid”,因此我們只要再在客戶端做一次簡單的切割,即可將我們需要的登錄信息進行剝離了,如下例子:

String[] userAttri = userName.split(",");
uinfo.setUserName(userAttri[0]);
uinfo.setCompanyId(userAttri[1]);

最終版src/main/webapp/WEB-INF/deployerConfiguration.xml文件

有了attributeDao, 有了resolver,我們徹底來重新配置一下我們的deployerConfiguration.xml文件吧,來看下面的配置:

<?xml version="1.0" encoding="UTF-8"?>


<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"
	xmlns:tx="http://www.springframework.org/schema/tx" xmlns:sec="http://www.springframework.org/schema/security"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd
       http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">

	<bean id="authenticationManager" class="org.jasig.cas.authentication.AuthenticationManagerImpl">


		<property name="credentialsToPrincipalResolvers">
			<list>
				<bean class="org.sky.cas.auth.CASCredentialsToPrincipalResolver">
					<property name="attributeRepository" ref="attributeRepository" />
				</bean>
			</list>
		</property>


		<property name="authenticationHandlers">
			<list>

				<bean
					class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"
					p:httpClient-ref="httpClient" />

				<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"
					p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org"
					p:contextSource-ref="contextSource" />
			</list>
		</property>
	</bean>
	<!-- ldap datasource -->
	<bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">
		<property name="password" value="secret" />
		<property name="pooled" value="true" />
		<property name="url" value="ldap://localhost:389" />

		<!--管理員 -->
		<property name="userDn" value="cn=Manager,dc=sky,dc=org" />
		<property name="baseEnvironmentProperties">
			<map>
				<!-- Three seconds is an eternity to users. -->
				<entry key="com.sun.jndi.ldap.connect.timeout" value="60" />
				<entry key="com.sun.jndi.ldap.read.timeout" value="60" />
				<entry key="java.naming.security.authentication" value="simple" />
			</map>
		</property>
	</bean>


	<sec:user-service id="userDetailsService">
		<sec:user name="@@THIS SHOULD BE REPLACED@@" password="notused"
			authorities="ROLE_ADMIN" />
	</sec:user-service>


	<bean id="attributeRepository"
		class="org.jasig.services.persondir.support.ldap.CASLdapPersonAttributeDao">
		<property name="contextSource" ref="contextSource" />
		<property name="baseDN" value="o=company,dc=sky,dc=org" />
		<property name="requireAllQueryAttributes" value="true" />
		<property name="queryAttributeMapping">
			<map>
				<entry key="username" value="uid" />
			</map>
		</property>
		<property name="resultAttributeMapping">
			<map>
				<entry key="uid" value="loginid" />
				<entry key="mail" value="email" />
			</map>
		</property>
	</bean>

	<bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">
	
		<property name="registeredServices">
			<list>
				<bean class="org.jasig.cas.services.RegexRegisteredService">
					<property name="id" value="0" />
					<property name="name" value="HTTP and IMAP" />
					<property name="description" value="Allows HTTP(S) and IMAP(S) protocols" />
					<property name="serviceId" value="^(https?|imaps?)://.*" />
					<property name="evaluationOrder" value="10000001" />
					<property name="ignoreAttributes" value="false" />
					<property name="allowedAttributes">
						<list>
							<value>loginid</value>
							<value>email</value>
						</list>
					</property>
				</bean>

			</list>
		</property>
	</bean>

	<bean id="auditTrailManager"
		class="com.github.inspektr.audit.support.Slf4jLoggingAuditTrailManager" />

	<bean id="healthCheckMonitor" class="org.jasig.cas.monitor.HealthCheckMonitor">
		<property name="monitors">
			<list>
				<bean class="org.jasig.cas.monitor.MemoryMonitor"
					p:freeMemoryWarnThreshold="10" />
				<!-- NOTE The following ticket registries support SessionMonitor: * DefaultTicketRegistry 
					* JpaTicketRegistry Remove this monitor if you use an unsupported registry. -->
				<bean class="org.jasig.cas.monitor.SessionMonitor"
					p:ticketRegistry-ref="ticketRegistry"
					p:serviceTicketCountWarnThreshold="5000"
					p:sessionCountWarnThreshold="100000" />
			</list>
		</property>
	</bean>
</beans>

在這個配置文件裏,我們把attributeDao還有Resolver還有我們的Ldap認證時用的AuthenticationHandler都變成了我們自定義的類了,但還是有2段配置代碼大家看起來有些疑惑,沒關係,我們接着來分析接着來變態:
	<bean id="attributeRepository"
		class="org.jasig.services.persondir.support.ldap.CASLdapPersonAttributeDao">
		<property name="contextSource" ref="contextSource" />
		<property name="baseDN" value="o=company,dc=sky,dc=org" />
		<property name="requireAllQueryAttributes" value="true" />
		<property name="queryAttributeMapping">
			<map>
				<entry key="username" value="uid" />
			</map>
		</property>
		<property name="resultAttributeMapping">
			<map>
				<entry key="uid" value="loginid" />
				<entry key="mail" value="email" />
			</map>
		</property>
	</bean>

看到這邊的resultAttributeMapping,它的意思就是:根據 上面的“queryAttributeMapping”的這個鍵值找到ldap中該條數據,然後通過resultAttributeMapping返回給客戶端 ,這段配置做的就是這麼一件事。

注:

  • 一定要在queryAttributeMapping的entry key=後面寫上"username”,這個username來自於我們cas sso登錄主界面中的username這個屬性。
  • 在resultAttributeMapping中key爲LDAP中相關數據的“主鍵”,value就是我們希望讓客戶端通過以下代碼獲取到CAS SSO服務端傳過來的值的那個key,千萬不要搞錯了哦。

Map attributes = principal.getAttributes();
String email = (String) attributes.get("email");


當然,到了這邊,我們的值還不能直接返回給客戶端 !!!

如果能夠直接返回,到此處爲止,我們的變態就應該已經全結束了,可是CAS SSO有着其嚴格的定義,不是說你要返回什麼值給客戶端你就可以返回的,還需要一個“allowed”。

繼續看下去:

	<bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">
	
		<property name="registeredServices">
			<list>
				<bean class="org.jasig.cas.services.RegexRegisteredService">
					<property name="id" value="0" />
					<property name="name" value="HTTP and IMAP" />
					<property name="description" value="Allows HTTP(S) and IMAP(S) protocols" />
					<property name="serviceId" value="^(https?|imaps?)://.*" />
					<property name="evaluationOrder" value="10000001" />
					<property name="ignoreAttributes" value="false" />
					<property name="allowedAttributes">
						<list>
							<value>loginid</value>
							<value>email</value>
						</list>
					</property>
				</bean>

			</list>
		</property>
	</bean>

看到這個地方了嗎?
					<property name="allowedAttributes">
						<list>
							<value>loginid</value>
							<value>email</value>
						</list>
					</property>

這段XML配置的意思就是: 根據上面的“queryAttributeMapping”的這個鍵值找到ldap中該條數據,然後通過resultAttributeMapping返回給客戶端,並且“允許“loginid”與"email“兩個值可以通過客戶端使用如下的的代碼被允許訪問得到:

Map attributes = principal.getAttributes();
String email = (String) attributes.get("email");

很煩? 不是,其實不煩,這是因爲老外的框架做的嚴謹,而且擴展性好,只要通過extend, implement就可以實現我們自己的功能了,這種設計很強,或者說很變態,因爲接下去還沒完呢,哈哈,繼續。

修改cas sso的主登錄界面,把界面修改成如下風格


修改src/main/webapp/WEB-INF/view/jsp/default/ui/casLoginView.jsp


這個改頁面,很簡單,這個頁面在:src/main/webapp/WEB-INF/view/default/ui/casLoginView.jsp

上手把這個頁面的兩個include去掉,如何去?如何增加以下這個下拉框:

<select id="companyid" name="companyid" > 
					<option value="101" selected>上海煤氣公司</option>
   				        <option value="102" selected>上海自來水廠</option>
					<option value="103" selected>FBI</option>
					<option value="104" selected>神盾局</option>							 
</select>

我在這邊就不細說了,這屬於copy & paste的工作,我在此就直接給出我自己製作完成後的casLoginView.jsp頁面內所有的源碼吧:

<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<%@ page session="true"%>
<%@ page pageEncoding="utf-8"%>
<%@ page contentType="text/html; charset=utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<html>
<head>

<link href="${pageContext.request.contextPath}/css/login.css"
	rel="stylesheet" type="text/css" />
<link href="${pageContext.request.contextPath}/css/login_form.css"
	rel="stylesheet" type="text/css" />

<script language="javascript">
	var relativePath="<%=request.getContextPath()%>";
</script>
<title>CAS SSO登錄</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>	
<body id="cas">
	<div style="text-align: center;">
		
	</div>
	<form:form method="post" id="fm1" commandName="${commandName}"
		htmlEscape="true" style="height:300px">
		<div class="login_div" id="login">
			<table border="0" cellspacing="0" cellpadding="0">
				<tr>
					<td colspan="2" style="border-bottom: 1px solid #e5e9ee;"><img src="${pageContext.request.contextPath}/css/images/login_dot.png" width="24" height="24" hspace="5"	align="absbottom" />登錄</td>
				</tr>
				 <tr>
            		<td width="175" class="label"> 用戶名:</td>
            		<td width="405">
            			<c:if test="${empty sessionScope.openIdLocalId}">
							<spring:message code="screen.welcome.label.netid.accesskey"
								var="userNameAccessKey" />
							<form:input οnblur="refreshOrgList();" id="username"
								tabindex="1" accesskey="${userNameAccessKey}" path="username"/>
						</c:if>
            		</td>
        		</tr>
				<tr>
					<td class="label">密碼:</td>
					<td><form:password cssClass="required" cssErrorClass="error"
							id="password" size="25" tabindex="2" path="password"
							accesskey="${passwordAccessKey}" autocomplete="off" />
					</td>
				</tr>
				
				<tr>
					<td class="label">公司ID:</td>
					<td>					
						<select id="companyid" name="companyid" > 
							<option value="101" selected>上海煤氣公司</option>
							 <option value="102" selected>上海自來水廠</option>
							 <option value="103" selected>FBI</option>
							 <option value="104" selected>神盾局</option>							 
						</select>
					</td>
				</tr>				
				<tr>
					<td class="label"></td>
					<td><font color="red"><form:errors id="msg" class="errors" /> </font></td>
				</tr>
			</table>
</div>			
<div class="but_div">
<input type="hidden" name="lt" value="${loginTicket}" /> 
<input type="hidden" name="execution" value="${flowExecutionKey}" /> 
<input type="hidden" name="_eventId" value="submit" /> 
<input name="submit" accesskey="l" class="login_but" value="<spring:message code="screen.welcome.button.login" />"
					       tabindex="4" type="submit" />
<input name="button2" type="reset" class="cancel_but" id="button2" value="取 消" />

</div>
	</form:form>
	<div class="loginbottom_div">
		<div>Copyright  &copy; 紅腸啃殭屍 reserved.</div>
	</div>
</body>	

你可以直接使用我做的頁面,我把它也上傳在”資源共享”中了,你也可以自己照着我這個jsp動手去改,改前請一定記得保存好原文件,反正改壞了你就再改一遍,改個4,5次也就習慣了,呵呵!

修改src/main/webapp/WEB-INF/view/jsp/default/protocol/2.0/casServiceValidationSuccess.jsp

CAS SSO中這個jsp是用於在用戶登錄成功後把用戶登錄成功後的信息組成一個map傳給客戶端調用的,即客戶端可以通過如下代碼:
Map attributes = principal.getAttributes();
String email = (String) attributes.get("email");

它是通過CAS SSO服務端的attributeDao來取得相關的LDAP中的其餘信息的,但是它默認只帶username到客戶端 ,因此爲了讓客戶端能夠取得以下這些額外的信息:
<property name="resultAttributeMapping">
			<map>
				<entry key="uid" value="loginid" />
				<entry key="mail" value="email" />
			</map>
</property>

我們需要更改這個jsp代碼,打開該JSP,加入如下的這段代碼:
<!-- return more attributes from attributeRepository start -->
<c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes) > 0}">

            <cas:attributes>

                <c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}">

                    <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>

                </c:forEach>

            </cas:attributes>

        </c:if>
<!-- return more attributes from attributeRepository end -->

改完後的casServiceValidationSuccess.jsp完整代碼如下,請注意<!-- return more attributes from attributeRepository start --><!-- return more attributes from attributeRepository end-->處的代碼,這段代碼就是我們新增的用於向客戶端返回attributeDao中取出的所有的屬性的遍歷代碼:

<%@ page session="false" %>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>
<cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>
	<cas:authenticationSuccess>
		<cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}
</cas:user>

<!-- return more attributes from attributeRepository start -->
<c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes) > 0}">

            <cas:attributes>

                <c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}">

                    <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>

                </c:forEach>

            </cas:attributes>

        </c:if>
<!-- return more attributes from attributeRepository end -->

<c:if test="${not empty pgtIou}">
		<cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>
</c:if>
<c:if test="${fn:length(assertion.chainedAuthentications) > 1}">
		<cas:proxies>
<c:forEach var="proxy" items="${assertion.chainedAuthentications}" varStatus="loopStatus" begin="0" end="${fn:length(assertion.chainedAuthentications)-2}" step="1">
			<cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>
</c:forEach>
		</cas:proxies>
</c:if>
	</cas:authenticationSuccess>
</cas:serviceResponse>

好了,終於全改完了,開始書寫我們的客戶端來做這個測試吧。

製作測試用客戶端工程

在客戶端我們會設置一個web session,把從cas-server帶過來的用戶ID,租戶ID以及存在LDAP中的該客戶的email都存儲於這個session中。

爲此,我們需要這樣一個東西:即我們需要一個filter,用於在每次從CAS SSO登錄成功後轉到客戶端時把相關的用戶登錄信息存儲到web session中去。

當然,這些工作涉及到一系列的工具類,而且這些個工具類對於cas-sample-site1和cas-sample-site2具有同樣的功能,出於代碼可維護性以及統一性的考慮,這兩個工程所使用到的這塊代碼功能都是相同的,因此我們來重組一下我們的客戶端工程的目錄結構吧。



myplatform工程

myplatform工程結構

該工程是cas-sample-site1和cas-sample-site2共用的一個工程,它的結構如下:





myplatform工程與CAS客戶端工程cas-sample-site1和cas-sample-site2的依賴關係




兩個客戶端工程的依賴全部如上面圖示所列那樣去設置

存儲客戶登錄信息的UserSession

package org.sky.framework.session;

import java.io.Serializable;

public class UserSession implements Serializable {

	private String companyId = "";
	private String userName = "";
	private String userEmail = "";

	public String getCompanyId() {
		return companyId;
	}

	public void setCompanyId(String companyId) {
		this.companyId = companyId;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}

	public String getUserEmail() {
		return userEmail;
	}

	public void setUserEmail(String userEmail) {
		this.userEmail = userEmail;
	}

}

AppSessionListener

package org.sky.framework.session;

import javax.servlet.http.HttpSessionEvent;
import javax.servlet.http.HttpSessionListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.servlet.ServletContext;
import javax.servlet.ServletRequestEvent;
import javax.servlet.ServletRequestListener;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;

public class AppSessionListener implements HttpSessionListener {

	protected Logger logger = LoggerFactory.getLogger(this.getClass());

	@Override
	public void sessionCreated(HttpSessionEvent se) {
		HttpSession session = null;
		try {
			session = se.getSession();
			// get value
			ServletContext context = session.getServletContext();
			String timeoutValue = context.getInitParameter("sessionTimeout");
			int timeout = Integer.valueOf(timeoutValue);
			// set value
			session.setMaxInactiveInterval(timeout);
			logger.info(">>>>>>session max inactive interval has been set to "
					+ timeout + " seconds.");
		} catch (Exception ex) {
			ex.printStackTrace();
		}

	}

	@Override
	public void sessionDestroyed(HttpSessionEvent arg0) {
		// TODO Auto-generated method stub

	}

}

我們的filter SampleSSOSessionFilter

package org.sky.framework.session;

import javax.servlet.Filter;
import javax.servlet.FilterChain;
import javax.servlet.FilterConfig;
import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
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 java.io.IOException;
import java.io.PrintWriter;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;

import org.jasig.cas.client.authentication.AttributePrincipal;
import org.jasig.cas.client.util.AssertionHolder;
import org.jasig.cas.client.validation.Assertion;
import org.sky.util.WebConstants;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class SampleSSOSessionFilter implements Filter {
	protected Logger logger = LoggerFactory.getLogger(this.getClass());
	private String excluded;
	private static final String EXCLUDE = "exclude";
	private boolean no_init = true;
	private ServletContext context = null;
	private FilterConfig config;
	String url = "";
	String actionName = "";

	public void setFilterConfig(FilterConfig paramFilterConfig) {
		if (this.no_init) {
			this.no_init = false;
			this.config = paramFilterConfig;
			if ((this.excluded = paramFilterConfig.getInitParameter("exclude")) != null)
				this.excluded += ",";
		}
	}

	private String getActionName(String actionPath) {
		logger.debug("filter actionPath====" + actionPath);
		StringBuffer actionName = new StringBuffer();
		try {
			int begin = actionPath.lastIndexOf("/");
			if (begin >= 0) {
				actionName.append(actionPath.substring(begin, actionPath.length()));
			}
		} catch (Exception e) {
		}
		return actionName.toString();
	}

	private boolean excluded(String paramString) {
		// logger.info("paramString====" + paramString);
		// logger.info("excluded====" + this.excluded);
		// logger.info(this.excluded.indexOf(paramString + ","));
		if ((paramString == null) || (this.excluded == null))
			return false;
		return (this.excluded.indexOf(paramString + ",") >= 0);
	}

	@Override
	public void destroy() {
		// TODO Auto-generated method stub

	}

	@Override
	public void doFilter(ServletRequest request, ServletResponse response, FilterChain arg2) throws IOException, ServletException {
		HttpServletRequest req = (HttpServletRequest) request;
		HttpServletResponse resp = (HttpServletResponse) response;
		UserSession uinfo = new UserSession();
		HttpSession se = req.getSession();

		url = req.getRequestURI();
		actionName = getActionName(url);
		//actionName = url;
		logger.debug(">>>>>>>>>>>>>>>>>>>>SampleSSOSessionFilter: request actionname" + actionName);
		if (!excluded(actionName)) {
			try {
				uinfo = (UserSession) se.getAttribute(WebConstants.USER_SESSION_OBJECT);
				AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal();
				String userName = principal.getName();
				logger.info("userName: " + userName);
				if (userName != null && userName.length() > 0 && uinfo == null) {
					Map attributes = principal.getAttributes();
					String email = (String) attributes.get("email");
					uinfo = new UserSession();
					String[] userAttri = userName.split(",");
					uinfo.setUserName(userAttri[0]);
					uinfo.setCompanyId(userAttri[1]);
					uinfo.setUserEmail(email);
					se.setAttribute(WebConstants.USER_SESSION_OBJECT, uinfo);

				}
			} catch (Exception e) {
				logger.error("SampleSSOSessionFilter error:" + e.getMessage(), e);
				resp.sendRedirect(req.getContextPath() + "/syserror.jsp");
				return;
			}
		} else {
			arg2.doFilter(request, response);
			return;
		}
		try {
			arg2.doFilter(request, response);
			return;
		} catch (Exception e) {
			logger.error("SampleSSOSessionFilter fault: " + e.getMessage(), e);
		}
	}

	@Override
	public void init(FilterConfig config) throws ServletException {
		// TODO Auto-generated method stub
		this.config = config;
		if ((this.excluded = config.getInitParameter("exclude")) != null)
			this.excluded += ",";
		this.no_init = false;
	}
}

case-sample-site1和cas-sample-site2中的web.xml

我們對於這兩個CAS客戶端工程的web.xml文件所做出的修改如下

  1. 將原有的9090(因爲原來我們的cas-server是放在tomcat裏的,當時設的端口號爲9090,那是爲了避免端口號和我們的jboss中的8080重複。而現在,我們可以把所有的9090改回成8080了)。
  2. 增加以下這段代碼
       <filter>
		<filter-name>SampleSSOSessionFilter</filter-name>
		<filter-class>org.sky.framework.session.SampleSSOSessionFilter</filter-class>
		<init-param>
			<param-name>exclude</param-name>
			<param-value>/syserror.jsp
			</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>SampleSSOSessionFilter</filter-name>
		<url-pattern>*</url-pattern>
	</filter-mapping>

看這個filter,它有一個特殊的地方,即我在標準的基於servlet2.4標準上對這個filter擴展了一個參數。

因爲我們爲兩個CAS客戶端工程增加了一個syserror.jsp,以用於在獲取CAS SERVER端出錯時進行重定向用,而這個syserror.jsp是不需要經過什麼登錄、什麼記錄websession用的,所以,它必須是被“excluded”掉的,對吧,具體它是怎麼實現的,大家可以自己跟一下代碼。

主要是注意看SampleSSOSessionFilter中以下這段代碼:
                                uinfo = (UserSession) se.getAttribute(WebConstants.USER_SESSION_OBJECT);
				AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal();
				String userName = principal.getName();
				logger.info("userName: " + userName);
				if (userName != null && userName.length() > 0 && uinfo == null) {
					Map attributes = principal.getAttributes();
					String email = (String) attributes.get("email");
					uinfo = new UserSession();
					String[] userAttri = userName.split(",");
					uinfo.setUserName(userAttri[0]);
					uinfo.setCompanyId(userAttri[1]);
					uinfo.setUserEmail(email);
					se.setAttribute(WebConstants.USER_SESSION_OBJECT, uinfo);

				}

好了,我們現在要做的就是爲cas-sample-site1/2各配上一個用於顯示我們是否能夠成功從cas-server端傳過來登錄成功後用戶信息的index.jsp了。

cas-sample-site1/index.jsp

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<%@ page import="org.sky.framework.session.UserSession, org.sky.util.WebConstants" %>    
<%
	UserSession us=(UserSession)session.getAttribute(WebConstants.USER_SESSION_OBJECT);
	String uname=us.getUserName();
	String email=us.getUserEmail();
	String companyId=us.getCompanyId();
%>
<!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>cas sample site1</title>
</head>
<body>
<h1>cas sample site1   Hello: <%=uname%>(<%=email%>) u are@Company: <%=companyId%></h1>
</p>
<a href="http://localhost:8080/cas-sample-site2/index.jsp">cas-sample-site2</a>

</br>
<a href="http://localhost:8080/cas-server/logout">退出</a>
</body>
</html>

cas-sample-site2/index.jsp

<%@ page language="java" contentType="text/html; charset=utf-8"
    pageEncoding="utf-8"%>
<%@ page import="org.sky.framework.session.UserSession, org.sky.util.WebConstants" %>    
<%
	UserSession us=(UserSession)session.getAttribute(WebConstants.USER_SESSION_OBJECT);
	String uname=us.getUserName();
	String email=us.getUserEmail();
	String companyId=us.getCompanyId();
%>
<!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>cas sample site2</title>
</head>
<body>
<h1>cas sample site2   Hello: <%=uname%>(<%=email%>) u are@Company: <%=companyId%></h1>
<a href="http://localhost:8080/cas-sample-site1/index.jsp">cas-sample-site1</a>
</br>
<a href="http://localhost:8080/cas-server/logout">退出</a>
</body>
</html>

運行今天所有的例子

測試用ldap中所有用戶的ldif代碼:


dn: dc=sky,dc=org
dc: sky
objectClass: top
objectClass: domain

dn: o=company,dc=sky,dc=org
objectClass: organization
o: company

dn: ou=members,o=company,dc=sky,dc=org
objectClass: organizationalUnit
ou: members

dn: cn=user1,ou=members,o=company,dc=sky,dc=org
sn: user1
cn: user1
userPassword: aaaaaa
objectClass: organizationalPerson

dn: cn=user2,ou=members,o=company,dc=sky,dc=org
sn: user2
cn: user2
userPassword: abcdefg
objectClass: organizationalPerson

dn: uid=mk,ou=members,o=company,dc=sky,dc=org
objectClass: posixAccount
objectClass: top
objectClass: inetOrgPerson
gidNumber: 0
givenName: Yuan
sn: MingKai
displayName: YuanMingKai
uid: mk
homeDirectory: e:\user
mail: [email protected]
cn: YuanMingKai
uidNumber: 13599
userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=

dn: o=101,o=company,dc=sky,dc=org
o: 101
objectClass: organization

dn: o=102,o=company,dc=sky,dc=org
o: 102
objectClass: organization

dn: o=103,o=company,dc=sky,dc=org
o: 103
objectClass: organization

dn: o=104,o=company,dc=sky,dc=org
o: 104
objectClass: organization

dn: uid=marious,o=101,o=company,dc=sky,dc=org
objectClass: posixAccount
objectClass: top
objectClass: inetOrgPerson
gidNumber: 0
givenName: Wang
sn: LiMing
displayName: WangLiMing
uid: marious
homeDirectory: d:\
cn: WangLiMing
uidNumber: 47967
userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=
mail: [email protected]

dn: uid=sky,o=101,o=company,dc=sky,dc=org
objectClass: posixAccount
objectClass: top
objectClass: inetOrgPerson
gidNumber: 0
givenName: Yuan
sn: Tao
displayName: YuanTao
uid: sky
homeDirectory: d:\
cn: YuanTao
uidNumber: 26422
userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=
mail: [email protected]

dn: uid=jason,o=102,o=company,dc=sky,dc=org
objectClass: posixAccount
objectClass: top
objectClass: inetOrgPerson
gidNumber: 0
givenName: zhang
sn: lei
displayName: zhanglei
uid: jason
homeDirectory: d:\
cn: zhanglei
uidNumber: 62360
userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=
mail: [email protected]

dn: uid=andy.li,o=103,o=company,dc=sky,dc=org
objectClass: posixAccount
objectClass: top
objectClass: inetOrgPerson
gidNumber: 0
givenName: Li
sn: Jun
displayName: LiJun
uid: andy.li
homeDirectory: d:\
cn: LiJun
uidNumber: 51204
userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=
mail: [email protected]

dn: uid=pitt,o=104,o=company,dc=sky,dc=org
objectClass: posixAccount
objectClass: top
objectClass: inetOrgPerson
gidNumber: 0
givenName: Brad
sn: Pitt
displayName: Brad Pitt
uid: pitt
homeDirectory: d:\
cn: Brad Pitt
uidNumber: 64650
userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=
mail: [email protected]

把:
  • cas-server
  • cas-sample-site1
  • cas-sample-site2
全部在eclipse裏用jboss7運行起來:



打開一個IE,輸入http://localhost:8080/cas-sample-site1,出現如下界面:


  • 我們在用戶名處輸入jason
  • 密碼輸入aaaaaa
  • 公司ID選擇成“上海自來水廠”
點擊【登錄】按鈕,此時頁面顯示如下:


點擊cas-sample-site2這個鏈接,頁面顯示如下:


我們看來看jason這個人在我們的ldap中的相關信息:



再來看看“上海自來水廠”的companyid是什麼:
                                                <select id="companyid" name="companyid" > 
							<option value="101" selected>上海煤氣公司</option>
							 <option value="102" selected>上海自來水廠</option>
							 <option value="103" selected>FBI</option>
							 <option value="104" selected>神盾局</option>							 
						</select>
是102,說明我們的傳值傳對了。

我們現在再用debug模式來調試一下這個用例。



好了,結束今天的課程。

在今天的課程中我們完成了幾件事,這幾件事中尤其是對於多租戶的CAS SSO的解決方案是目前網上沒有的包括國外的網站和主力論壇上(或者說有人解決了沒有公佈出來):

  1. 自定義CAS SSO登錄界面
  2. 在CAS SSO登錄界面增加我們自定義的登錄用元素
  3. 使用LDAP帶出登錄用戶在LDAP內存儲的其它更多的信息
  4. 實現了CAS SSO支持多租戶登錄的功能
雖然這一過程很痛苦,很變態,但是通過這樣的一個案例,我們完成了一件了不起的事情,同時對於這種國外開源軟件的customization我們也有了一個認識,就是歐美的一些軟件,它的自定義都是通過擴展、繼承、插件式的方式來實現的,這說明他們的軟件在設計之初就考慮到了這些擴展。

那很多人就會來問,改了這麼一堆東西,我怎麼知道要改這些東西,要去動這些代碼或者有些代碼怎麼寫?

我回答這個問題的方式很簡單,我把它稱之爲:play with it

因爲開源的軟件都提供源碼的,你把源碼都導入eclipse工程,想辦法運行起來,這個過程可能折騰個1-2周吧,但是源碼一旦跑起來了,你就可以自己去跟代碼啦,然後看人家這塊邏輯這塊設計是怎麼實現的,然後照着寫或者按照人家的規範插入自己的一部分的自定義的代碼,就這樣一點點,一點點的你也就可以把本屬於別人一個產品變成爲自己的一套東西了,這個過程就叫play with it

做IT的一定要多play with it,要不然,你很難有自己的感性上的認識,沒有了感性認識的基礎,那也就談不上什麼“理性認識”和“昇華”了,呵呵!

在我們今後的教程中,我們動手改代碼或者集成其它開源產品的機會還有很多、很多。。。。。。甚至還會涉及到JDK裏的一些東西,讓我們一起慢慢來吧。












發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章