开场白
各位新年好,上海的新年好冷,冷到我手发抖。
做好准备全身心投入到新的学习和工作中去了吗?因为今天开始的教程很“变态”啊,我们要完成下面几件事:
- 自定义CAS SSO登录界面
- 在CAS SSO登录界面增加我们自定义的登录用元素
- 使用LDAP带出登录用户在LDAP内存储的更多的信息
- 实现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/赵云
可是,我们想想:
- 我们的租户在我们的后台系统中是自动“开户”的,companyid是一个自动增加的,我从companyid_101现在增加到了companyid_110时,你是不是每次用户一开户,你就要去手动改这个CAS SSO中的配置文件呢?
- 如果你不嫌烦,好好好,你够狠,你就手工改吧!但是当你每次在配置文件中新增一条配置语句时,你的CAS SSO是不是要断服务重启啊?那你还怎么做到24*7的这种不间断服务啊
再来看看用于今天练习的我们在LDAP中的组织结构是怎么样的吧。
看到上面这张图了吧,这就是我说的“多租户”的概念,大家应该记得我们在CAS SSO第二天中怎么去拿CAS SSO绑定LDAP中的一条UserDN然后去搜索的吧?
对吧!!!
现在我们要做到的就是:
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,这个方法已经被我们否掉了。
创建工程
CAS SERVER工程的组建
导入所有的配置文件
- org
- META-INF
构建WEB-INF目录
构建cas-server基本源码
- src/main/java
- src/main/resources
构建webapp目录
CAS SSO在jboss/weblogic下的bug的修正
- META-INF文件内的persistence.xml中报HSQLDialect错误
- 报log4jConfiguration.xml文件在启动时找不到的错误
修正CAS SSO的persistence.xml文件中的HSQLDialect错误
org.jasig.cas.services.AbstractRegisteredService
org.jasig.cas.services.RegexRegisteredService
org.jasig.cas.services.RegisteredServiceImpl
org.jasig.cas.ticket.TicketGrantingTicketImpl
org.jasig.cas.ticket.ServiceTicketImpl
org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock
我们在文件最后加入以下配置代码
org.jasig.cas.services.AbstractRegisteredService
org.jasig.cas.services.RegexRegisteredService
org.jasig.cas.services.RegisteredServiceImpl
org.jasig.cas.ticket.TicketGrantingTicketImpl
org.jasig.cas.ticket.ServiceTicketImpl
org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock
这是完整的改完后的persistence.xml文件的内容:
org.jasig.cas.services.AbstractRegisteredService
org.jasig.cas.services.RegexRegisteredService
org.jasig.cas.services.RegisteredServiceImpl
org.jasig.cas.ticket.TicketGrantingTicketImpl
org.jasig.cas.ticket.ServiceTicketImpl
org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock
修正CAS SSO中log4jConfiguration.xml文件在启动时找不到的错误
${log4j.config.location:classpath:log4j.xml}
${log4j.refresh.interval:60000}
改完后请保存。
将CAS-SERVER从eclipse java工程改为j2ee工程
组装可在eclipse中启动的cas-server web工程
在eclipse中启动cas-server工程
开始修改源码
如何让cas-server支持动态的p:searchBase呢
为cas server的登录增加一个项
- username
- password
新建CASCredential类
public class CASCredential extends RememberMeUsernamePasswordCredentials {
private static final long serialVersionUID = 1L;
private Map 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 getParam() {
return param;
}
public void setParam(Map param) {
this.param = param;
}
}
这就是我们扩展的CASCredential类,该类除了拥有原来CAS SSO基本credential中的username和password两个属性外还有一个叫companyid的属性。
将新增的companyid绑定至cas sso的登录页面
将其改成:
扩展CAS SSO登录页面的submit行为以支持我们在页面中新增的companyid属性可以被提交到CAS SSO的后台
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。
修改配置文件:src/main/webapp/WEB-INF/cas-servlet.xml
把它注释掉改成:
此时,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 cns = new ArrayList();
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:
*
* - Pooling of LDAP connections used for searching (e.g. via instance
* of {@link org.springframework.ldap.pool.factory.PoolingContextSource}).
* - Searching with client certificate credentials.
*
*
* 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中以下这段用的:
请注意代码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。
将LDAP中登录用户的其它信息也带入到客户端登录成功后跳转的页面中去
attributeRepository的作用
- attributeDAO是用于根据searchBase在LDAP中定位到一条数据,然后把该条数据所有的属性取出来用的一个工具类
- credentialsToPrincipalResolvers,该类用于向客户端(就是我们的cas-samples-site1/site2)返回用户在CAS SSO中登录画面中输入的登录相关信息用的一个工具类
CASLdapPersonAttributeDao
/**
* 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.
*
*
*
* Configuration:
*
*
* Property
* Description
* Required
* Default
*
*
* searchControls
*
* Set the {@link SearchControls} used for executing the LDAP query.
*
* No
* Default instance with SUBTREE scope.
*
*
* baseDN
*
* The base DistinguishedName to use when executing the query filter.
*
* No
* ""
*
*
* contextSource
*
* A {@link ContextSource} from the Spring-LDAP framework. Provides a DataSource
* style object that this DAO can retrieve LDAP connections from.
*
* Yes
* null
*
*
* setReturningAttributes
*
* 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.
*
* No
* true
*
*
* queryType
*
* How multiple attributes in a query should be concatenated together. The other option is OR.
*
* No
* AND
*
*
*
* @author [email protected]
* @author Eric Dalquist
* @version $Revision$ $Date$
* @since uPortal 2.5
*/
public class CASLdapPersonAttributeDao extends AbstractQueryPersonAttributeDao 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> 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
String searchBase = "";
if (ldapPerson.getCompanyid().trim().length() > 0) {
searchBase = "o=" + ldapPerson.getCompanyid() + "," + baseDN;
} else {
searchBase = baseDN;
}
logger.info("searchBase=====" + searchBase);
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("");
sb.append("");
sb.append("");
sb.append("sys ");
sb.append("401 ");
sb.append("[email protected] ");
sb.append(" ");
sb.append(" ");
getPersoninfoFromCredential(sb.toString());
}
}
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类
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");
熊掌与鱼兼得法 ,既可以把用户在LDAP中其它属性带到客户端又可以把客户的登录信息也带到客户端
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”。
AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal();
String userName = principal.getName();
String[] userAttri = userName.split(",");
uinfo.setUserName(userAttri[0]);
uinfo.setCompanyId(userAttri[1]);
最终版src/main/webapp/WEB-INF/deployerConfiguration.xml文件
loginid
email
在这个配置文件里,我们把attributeDao还有Resolver还有我们的Ldap认证时用的AuthenticationHandler都变成了我们自定义的类了,但还是有2段配置代码大家看起来有些疑惑,没关系,我们接着来分析接着来变态:
看到这边的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");
loginid
email
看到这个地方了吗?
loginid
email
这段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
<%@ 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"%>
CAS SSO登录
登录
用户名:
密码:
公司ID:
"
tabindex="4" type="submit" />
Copyright © 红肠啃僵尸 reserved.
修改src/main/webapp/WEB-INF/view/jsp/default/protocol/2.0/casServiceValidationSuccess.jsp
Map attributes = principal.getAttributes();
String email = (String) attributes.get("email");
${fn:escapeXml(attr.value)}
改完后的casServiceValidationSuccess.jsp完整代码如下,请注意 至 处的代码,这段代码就是我们新增的用于向客户端返回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" %>
${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}
${fn:escapeXml(attr.value)}
${pgtIou}
${fn:escapeXml(proxy.principal.id)}
制作测试用客户端工程
myplatform工程
myplatform工程结构
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
- 将原有的9090(因为原来我们的cas-server是放在tomcat里的,当时设的端口号为9090,那是为了避免端口号和我们的jboss中的8080重复。而现在,我们可以把所有的9090改回成8080了)。
- 增加以下这段代码
SampleSSOSessionFilter
org.sky.framework.session.SampleSSOSessionFilter
exclude
/syserror.jsp
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();
%>
cas sample site1
cas sample site1 Hello: <%=uname%>(<%=email%>) u are@Company: <%=companyId%>
cas-sample-site2
退出
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();
%>
cas sample site2
cas sample site2 Hello: <%=uname%>(<%=email%>) u are@Company: <%=companyId%>
cas-sample-site1
退出
运行今天所有的例子
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
- 我们在用户名处输入jason
- 密码输入aaaaaa
- 公司ID选择成“上海自来水厂”
是102,说明我们的传值传对了。
- 自定义CAS SSO登录界面
- 在CAS SSO登录界面增加我们自定义的登录用元素
- 使用LDAP带出登录用户在LDAP内存储的其它更多的信息
- 实现了CAS SSO支持多租户登录的功能