在同一個域名下如taobao.com下會有多個商鋪(就是租戶)比如:javascript
看張飛這個名字,看!!!css
不一樣的company(租戶)下有着相同的用戶,但其實這是兩個不用的用戶,中國同名同姓的人多了去了,對吧,這時company_101的張飛登陸是因該只看到它所屬的company_101這個租戶下全部的數據和信息吧,而不能跑到company_102中看到別人家的信息,對吧?html
國外不少解決方案說是在咱們的CAS SSO的配置文件裏在綁定LDAP的context時寫上多條這樣的東西:java
<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"
git
p:filter="uid=%u" p:searchBase="o=101,o=company,dc=sky,dc=org"
github
p:contextSource-ref="contextSource" />
web
<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"
spring
p:filter="uid=%u" p:searchBase="o=102,o=company,dc=sky,dc=org"
express
p:contextSource-ref="contextSource" />
apache
<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" />
但是,咱們想一想:
通常來講,咱們的開戶是用程序自動寫入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"
這條要作成動態的,好比說:
前面咱們提到過,這些配置是放在XML文件中的,所以每次增長一個」租戶「咱們要手工在XML配置文件中新增一條,這個不現實,它是實現不了咱們的24*7的這種服務的要求的,咱們要作的是可讓這個p:searchBase可以動態的去組建這個userDN,因此重點是要解決這個問題。
該問題在國外的YALE CAS論壇上有兩種解決方案:
所以,筆者在這邊要提的將是首創的能夠作到全動態的去根據用戶名,密碼和所該用戶所屬組織自動在後臺建立p:searchBase的最完美的解決方案,下面咱們就開始吧。
咱們此次是要在CAS SSO這個產品上作擴展了,爲此,咱們不能再像咱們第一天和次日中那樣直接拿個文本編輯器去改CAS SSO裏的配置文件了,咱們須要建立一個eclipse工程,來看咱們的eclipse工程。
look,今天咱們把這個cas-server放到了eclipse工程中去了,而後在eclipse裏隨改隨測試,如今咱們就來說述如何建立這個工程以使得cas server能夠運行在咱們的eclipse的工程中。
所以咱們在eclipse中新建一個java工程-是java工程你可千萬不要建成j2ee工程啊,而後按照上圖創建相應的目錄。
這是咱們在第一天,次日中佈署在tomcat下的cas server工程的目錄:
D:\tomcat\webapps\cas-server\WEB-INF\classes
把這個目錄下全部的內容,除去如下2個目錄:
外全部的東西通通拷貝入eclipse中的cas-server工程中的src/main/resources目錄下
將D:\tomcat\webapps\cas-server\WEB-INF目錄下這幾個目錄放入cas-server工程的src/main/webapp/WEB-INF目錄下
解壓開咱們下載的」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/webapp不做爲編譯路徑。
別忘了把全部的src/main/webapp/WEB-INF/lib目錄下的jar加到cas-server工程的Libraries中去。
將咱們在第一天、次日中佈署在tomcat中的case-server中如下這些目錄
拷貝到eclipse的cas-server工程中的src/main/webapp目錄
因爲咱們的eclipse中的cas-server將和咱們的cas-sample-site1以及cas-sample-site2啓動在jboss下,所以cas sso在jboss或者是在weblogic下有兩個小問題,在此須要修正。
下面咱們來看如何修正這兩個小BUG。
這是原始的/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>
改完後請保存。
找到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-sso工程,選擇project properties,而後選擇project facet,按照以下截圖來作選擇。
右鍵單擊cas-sso工程,選擇project properties,而後選擇Deployment Assembly,這個基本功我已經在 通向架構師的道路(第二十天)萬能框架spring(二)maven結合spring與ibatis中詳細講述過這個Deployment Assembly是幹什麼用的了。
一切無誤後請在eclipse中啓動cas-sso吧。
咱們的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 sso的登陸項只有兩個屬性:
咱們須要增長一個companyid,用於判斷當前登陸的用戶是屬於哪一個租戶的。
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的屬性。
修改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>
新建一個類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. */
private CentralAuthenticationService centralAuthenticationService;
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:
找到如下這行:
<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類了。
新增一個類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類在這邊的做用了吧。
咱們知道,CAS SSO能夠把username(uid)帶入到客戶端登陸成功後的頁面中去,但是一個uid在LDAP中還關聯着許多其它有用的信息如:email。 還有就是咱們剛纔新增的companyid,咱們也想把這些信息同時帶到客戶端登陸成功的畫面中去呢?
這邊就須要使用到CAS SSO中的一個特殊的屬性,它叫attributeRepository。
attributeRepository關聯着一個dao和一個resolver,它們的做用以下:
先來講attributeDAO的做用吧。
好比說咱們這邊想要把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 andrew.petro@yale.edu
* @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>aaa@a.net</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類吧
該類的做用是這樣的:
一個客戶在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();中帶過來,怎麼辦?
所以咱們須要定製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]);
有了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返回給客戶端 ,這段配置作的就是這麼一件事。
注:
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就能夠實現咱們本身的功能了,這種設計很強,或者說很變態,由於接下去還沒完呢,哈哈,繼續。
這個改頁面,很簡單,這個頁面在: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 onblur="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 © 紅腸啃殭屍 reserved.</div>
</div>
</body>
你能夠直接使用我作的頁面,我把它也上傳在」資源共享」中了,你也能夠本身照着我這個jsp動手去改,改前請必定記得保存好原文件,反正改壞了你就再改一遍,改個4,5次也就習慣了,呵呵!
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具備一樣的功能,出於代碼可維護性以及統一性的考慮,這兩個工程所使用到的這塊代碼功能都是相同的,所以咱們來重組一下咱們的客戶端工程的目錄結構吧。
該工程是cas-sample-site1和cas-sample-site2共用的一個工程,它的結構以下:
兩個客戶端工程的依賴所有如上面圖示所列那樣去設置。
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;
}
}
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
}
}
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;
}
}
咱們對於這兩個CAS客戶端工程的web.xml文件所作出的修改以下
<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了。
<%@ 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>
<%@ 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: mk.yuan@nttdata.com
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: aaa@a.net
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: bbb@b.net
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: jason@abc.net
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: andy.li@jesus.chris
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: pitt@hollywood.com
把:
所有在eclipse裏用jboss7運行起來:
打開一個IE,輸入http://localhost:8080/cas-sample-site1,出現以下界面:
點擊【登陸】按鈕,此時頁面顯示以下:
點擊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的解決方案是目前網上沒有的包括國外的網站和主力論壇上(或者說有人解決了沒有公佈出來):
雖然這一過程很痛苦,很變態,可是經過這樣的一個案例,咱們完成了一件了不得的事情,同時對於這種國外開源軟件的customization咱們也有了一個認識,就是歐美的一些軟件,它的自定義都是經過擴展、繼承、插件式的方式來實現的,這說明他們的軟件在設計之初就考慮到了這些擴展。
那不少人就會來問,改了這麼一堆東西,我怎麼知道要改這些東西,要去動這些代碼或者有些代碼怎麼寫?
我回答這個問題的方式很簡單,我把它稱之爲:play with it。
由於開源的軟件都提供源碼的,你把源碼都導入eclipse工程,想辦法運行起來,這個過程可能折騰個1-2周吧,可是源碼一旦跑起來了,你就能夠本身去跟代碼啦,而後看人家這塊邏輯這塊設計是怎麼實現的,而後照着寫或者按照人家的規範插入本身的一部分的自定義的代碼,就這樣一點點,一點點的你也就能夠把本屬於別人一個產品變成爲本身的一套東西了,這個過程就叫play with it。
作IT的必定要多play with it,要否則,你很難有本身的感性上的認識,沒有了感性認識的基礎,那也就談不上什麼「理性認識」和「昇華」了,呵呵!
在咱們從此的教程中,咱們動手改代碼或者集成其它開源產品的機會還有不少、不少。。。。。。甚至還會涉及到JDK裏的一些東西,讓咱們一塊兒慢慢來吧。