单点登录之CAS SSO从入门到精通(第三天)
开场白
各位新年好,上海的新年好冷,冷到我手发抖。
做好准备全身心投入到新的学习和工作中去了吗?因为今天开始的教程很“变态”啊,我们要完成下面几件事:
- 自定义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/赵云
<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"p:filter="uid=%u" p:searchBase="o=101,o=company,dc=sky,dc=org"p:contextSource-ref="contextSource" /><bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"p:filter="uid=%u" p:searchBase="o=102,o=company,dc=sky,dc=org"p:contextSource-ref="contextSource" /><bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"p:filter="uid=%u" p:searchBase="o=103,o=company,dc=sky,dc=org"p:contextSource-ref="contextSource" />
可是,我们想想:
- 我们的租户在我们的后台系统中是自动“开户”的,companyid是一个自动增加的,我从companyid_101现在增加到了companyid_110时,你是不是每次用户一开户,你就要去手动改这个CAS SSO中的配置文件呢?
- 如果你不嫌烦,好好好,你够狠,你就手工改吧!但是当你每次在配置文件中新增一条配置语句时,你的CAS SSO是不是要断服务重启啊?那你还怎么做到24*7的这种不间断服务啊
再来看看用于今天练习的我们在LDAP中的组织结构是怎么样的吧。
看到上面这张图了吧,这就是我说的“多租户”的概念,大家应该记得我们在CAS SSO第二天中怎么去拿CAS SSO绑定LDAP中的一条UserDN然后去搜索的吧?
<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org"p:contextSource-ref="contextSource" />
对吧!!!
现在我们要做到的就是:
p:searchBase="xxx.xxx.xx"
这条要做成动态的,比如说:
- 用户是company_id=101的,这时这个p:searchBase就应该变为:“p:searchBase="uid=sky,o=101,o=company,dc=sky,dc=org"
- 用户是company_id=102的,这时这个p:searchBase就应该变为:“p:searchBase="uid=jason,o=102,o=company,dc=sky,dc=org"
前面我们提到过,这些配置是放在XML文件中的,因此每次增加一个”租户“我们要手工在XML配置文件中新增一条,这个不现实,它是实现不了我们的24*7的这种服务的要求的,我们要做的是可以让这个p:searchBase能够动态的去组建这个userDN,所以重点是要解决这个问题。
该问题在国外的YALE CAS论坛上有两种解决方案:
- 一种是直接通过CAS的登录界面然后在输入用户名时要求用户以这种形式“uid=sky,o=101"去输入它的用户名,这种做法先不去说会造成用户登录时的困扰,而且CAS SSO的登录界面也不支持这样格式的用户名输入。
- 一种就是很笨的在CAS SSO的配置文件中绑定多个p:searchBase,这个方法已经被我们否掉了。
创建工程
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错误
<class>org.jasig.cas.services.AbstractRegisteredService</class><class>org.jasig.cas.services.RegexRegisteredService</class><class>org.jasig.cas.services.RegisteredServiceImpl</class><class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class><class>org.jasig.cas.ticket.ServiceTicketImpl</class><class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class>
我们在文件最后加入以下配置代码
<class>org.jasig.cas.services.AbstractRegisteredService</class><class>org.jasig.cas.services.RegexRegisteredService</class><class>org.jasig.cas.services.RegisteredServiceImpl</class><class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class><class>org.jasig.cas.ticket.ServiceTicketImpl</class><class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class><properties><property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" /></properties>
这是完整的改完后的persistence.xml文件的内容:
<persistence xmlns="http://java.sun.com/xml/ns/persistence"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"version="2.0"><persistence-unit name="CasPersistence" transaction-type="RESOURCE_LOCAL"><class>org.jasig.cas.services.AbstractRegisteredService</class><class>org.jasig.cas.services.RegexRegisteredService</class><class>org.jasig.cas.services.RegisteredServiceImpl</class><class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class><class>org.jasig.cas.ticket.ServiceTicketImpl</class><class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class><properties><property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" /></properties></persistence-unit>
</persistence>
改完后请保存。
修正CAS SSO中log4jConfiguration.xml文件在启动时找不到的错误
<bean id="log4jInitialization" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"><property name="targetClass" value="org.springframework.util.Log4jConfigurer"/><property name="targetMethod" value="initLogging"/><property name="arguments"><list><value>${log4j.config.location:classpath:log4j.xml}</value><value>${log4j.refresh.interval:60000}</value></list></property></bean>
整个log4jConfiguration.xml文件修改后是这个样子的:
<beans xmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xmlns:p="http://www.springframework.org/schema/p"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"><!-- <bean id="log4jInitialization" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"><property name="targetClass" value="org.springframework.util.Log4jConfigurer"/><property name="targetMethod" value="initLogging"/><property name="arguments"><list><value>${log4j.config.location:classpath:log4j.xml}</value><value>${log4j.refresh.interval:60000}</value></list></property></bean>-->
</beans>
改完后请保存。
将CAS-SERVER从eclipse java工程改为j2ee工程
组装可在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<String, Object> param;private String companyid;/*** @return the companyid*/public String getCompanyid() {return companyid;}/*** @param companyid the companyid to set*/public void setCompanyid(String companyid) {this.companyid = companyid;}public Map<String, Object> getParam() {return param;}public void setParam(Map<String, Object> param) {this.param = param;}
}
这就是我们扩展的CASCredential类,该类除了拥有原来CAS SSO基本credential中的username和password两个属性外还有一个叫companyid的属性。
将新增的companyid绑定至cas sso的登录页面
<view-state id="viewLoginForm" view="casLoginView" model="credentials"><binder><binding property="username" /><binding property="password" /></binder>
将其改成:
<view-state id="viewLoginForm" view="casLoginView" model="credentials"><binder><binding property="username" /><binding property="password" /><binding property="companyid"/></binder>
扩展CAS SSO登录页面的submit行为以支持我们在页面中新增的companyid属性可以被提交到CAS SSO的后台
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. */@NotNullprivate CentralAuthenticationService centralAuthenticationService;@NotNullprivate 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 ticketfinal 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
<bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"p:centralAuthenticationService-ref="centralAuthenticationService"p:warnCookieGenerator-ref="warnCookieGenerator"/>
把它注释掉改成:
<!-- <bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"p:centralAuthenticationService-ref="centralAuthenticationService"p:warnCookieGenerator-ref="warnCookieGenerator"/>--><bean id="authenticationViaFormAction"class="org.sky.cas.auth.CASAuthenticationViaFormAction"p:centralAuthenticationService-ref="centralAuthenticationService"p:warnCookieGenerator-ref="warnCookieGenerator" />
此时,CAS SSO的登录界面在用户点击submit按钮时,就会使用我们自定义的这个CASAuthenticationViaFormAction类了。
增加p:searchBase使得CAS SSO的LDAP可以根据不同的companyid动态搜索用户的功能
新增一个类CASLDAPAuthenticationHandler,代码如下:
package org.sky.cas.auth;import org.jasig.cas.adaptors.ldap.AbstractLdapUsernamePasswordAuthenticationHandler;
import org.jasig.cas.authentication.handler.AuthenticationException;
import org.jasig.cas.authentication.principal.UsernamePasswordCredentials;
import org.jasig.cas.util.LdapUtils;
import org.springframework.ldap.NamingSecurityException;
import org.springframework.ldap.core.ContextSource;
import org.springframework.ldap.core.LdapTemplate;
import org.springframework.ldap.core.NameClassPairCallbackHandler;
import org.springframework.ldap.core.SearchExecutor;import javax.naming.NameClassPair;
import javax.naming.NamingEnumeration;
import javax.naming.NamingException;
import javax.naming.directory.DirContext;
import javax.naming.directory.SearchControls;
import javax.validation.constraints.Max;
import javax.validation.constraints.Min;
import java.util.ArrayList;
import java.util.List;public class CASLDAPAuthenticationHandler extends AbstractLdapUsernamePasswordAuthenticationHandler {/** The default maximum number of results to return. */private static final int DEFAULT_MAX_NUMBER_OF_RESULTS = 1000;/** The default timeout. */private static final int DEFAULT_TIMEOUT = 1000;/** The search base to find the user under. */private String searchBase;/** The scope. */@Min(0)@Max(2)private int scope = SearchControls.ONELEVEL_SCOPE;/** The maximum number of results to return. */private int maxNumberResults = DEFAULT_MAX_NUMBER_OF_RESULTS;/** The amount of time to wait. */private int timeout = DEFAULT_TIMEOUT;/** Boolean of whether multiple accounts are allowed. */private boolean allowMultipleAccounts;protected final boolean authenticateUsernamePasswordInternal(final UsernamePasswordCredentials credentials)throws AuthenticationException {CASCredential rmupc = (CASCredential) credentials;final String companyid = rmupc.getCompanyid();final List<String> cns = new ArrayList<String>();final SearchControls searchControls = getSearchControls();final String transformedUsername = getPrincipalNameTransformer().transform(credentials.getUsername());final String filter = LdapUtils.getFilterWithValues(getFilter(), transformedUsername);try {this.getLdapTemplate().search(new SearchExecutor() {public NamingEnumeration executeSearch(final DirContext context) throws NamingException {String baseDN = "";if (companyid != null && companyid.trim().length() > 0) {baseDN = "o=" + companyid + "," + searchBase;} else {baseDN = searchBase;}//System.out.println("searchBase=====" + baseDN);return context.search(baseDN, filter, searchControls);}}, new NameClassPairCallbackHandler() {public void handleNameClassPair(final NameClassPair nameClassPair) {cns.add(nameClassPair.getNameInNamespace());}});} catch (Exception e) {log.error("search ldap error casue: " + e.getMessage(), e);return false;}if (cns.isEmpty()) {log.debug("Search for " + filter + " returned 0 results.");return false;}if (cns.size() > 1 && !this.allowMultipleAccounts) {log.warn("Search for " + filter + " returned multiple results, which is not allowed.");return false;}for (final String dn : cns) {DirContext test = null;String finalDn = composeCompleteDnToCheck(dn, credentials);try {this.log.debug("Performing LDAP bind with credential: " + dn);test = this.getContextSource().getContext(finalDn, getPasswordEncoder().encode(credentials.getPassword()));if (test != null) {return true;}} catch (final NamingSecurityException e) {log.debug("Failed to authenticate user {} with error {}", credentials.getUsername(), e.getMessage());return false;} catch (final Exception e) {this.log.error(e.getMessage(), e);return false;} finally {LdapUtils.closeContext(test);}}return false;}protected String composeCompleteDnToCheck(final String dn, final UsernamePasswordCredentials credentials) {return dn;}private SearchControls getSearchControls() {final SearchControls constraints = new SearchControls();constraints.setSearchScope(this.scope);constraints.setReturningAttributes(new String[0]);constraints.setTimeLimit(this.timeout);constraints.setCountLimit(this.maxNumberResults);return constraints;}/*** Method to return whether multiple accounts are allowed.* @return true if multiple accounts are allowed, false otherwise.*/protected boolean isAllowMultipleAccounts() {return this.allowMultipleAccounts;}/*** Method to return the max number of results allowed.* @return the maximum number of results.*/protected int getMaxNumberResults() {return this.maxNumberResults;}/*** Method to return the scope.* @return the scope*/protected int getScope() {return this.scope;}/*** Method to return the search base.* @return the search base.*/protected String getSearchBase() {return this.searchBase;}/*** Method to return the timeout. * @return the timeout.*/protected int getTimeout() {return this.timeout;}public final void setScope(final int scope) {this.scope = scope;}/*** @param allowMultipleAccounts The allowMultipleAccounts to set.*/public void setAllowMultipleAccounts(final boolean allowMultipleAccounts) {this.allowMultipleAccounts = allowMultipleAccounts;}/*** @param maxNumberResults The maxNumberResults to set.*/public final void setMaxNumberResults(final int maxNumberResults) {this.maxNumberResults = maxNumberResults;}/*** @param searchBase The searchBase to set.*/public final void setSearchBase(final String searchBase) {this.searchBase = searchBase;}/*** @param timeout The timeout to set.*/public final void setTimeout(final int timeout) {this.timeout = timeout;}/*** Sets the context source for LDAP searches. This method may be used to* support use cases like the following:* <ul>* <li>Pooling of LDAP connections used for searching (e.g. via instance* of {@link org.springframework.ldap.pool.factory.PoolingContextSource}).</li>* <li>Searching with client certificate credentials.</li>* </ul>* <p>* If this is not defined, the context source defined by* {@link #setContextSource(ContextSource)} is used.** @param contextSource LDAP context source.*/public final void setSearchContextSource(final ContextSource contextSource) {setLdapTemplate(new LdapTemplate(contextSource));}}
这个类的作用就是给src/main/webapp/WEB-INF/deployerConfiguration.xml中以下这段用的:
<property name="authenticationHandlers"><list><beanclass="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。
<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><beanclass="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中登录用户的其它信息也带入到客户端登录成功后跳转的页面中去
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.* * <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)*/@Overrideprotected 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)*/@Overrideprotected 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 runif (StringUtils.isBlank(generatedLdapQuery)) {return null;}//Insert the generated query into the template if it is configuredfinal 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 queryList<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 attributeString 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)}*/@Deprecatedpublic int getTimeLimit() {return this.searchControls.getTimeLimit();}/*** @see javax.naming.directory.SearchControls#setTimeLimit(int)* @deprecated*/@Deprecatedpublic 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;}
}
String searchBase = "";if (ldapPerson.getCompanyid().trim().length() > 0) {searchBase = "o=" + ldapPerson.getCompanyid() + "," + baseDN;} else {searchBase = baseDN;}logger.info("searchBase=====" + searchBase);
CASLdapPersonAttributeDao类中需要使用到另外两个我们自定义的工具类代码如下:
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());}
}
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");
现在问题来了,我们新增的companyid即不是该用户在ldap中的一个属性,又不能在req.getUserPrincipal();中带过来,怎么办?
熊掌与鱼兼得法 ,既可以把用户在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();
去试图获取从CAS SSO中带来的登录信息时,客户端将会得到一个这样的字串“username,companyid”,因此我们只要再在客户端做一次简单的切割,即可将我们需要的登录信息进行剥离了,如下例子:
String[] userAttri = userName.split(",");
uinfo.setUserName(userAttri[0]);
uinfo.setCompanyId(userAttri[1]);
最终版src/main/webapp/WEB-INF/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.xsdhttp://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsdhttp://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><beanclass="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"p:httpClient-ref="httpClient" /><bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org"p:contextSource-ref="contextSource" /></list></property></bean><!-- ldap datasource --><bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource"><property name="password" value="secret" /><property name="pooled" value="true" /><property name="url" value="ldap://localhost:389" /><!--管理员 --><property name="userDn" value="cn=Manager,dc=sky,dc=org" /><property name="baseEnvironmentProperties"><map><!-- Three seconds is an eternity to users. --><entry key="com.sun.jndi.ldap.connect.timeout" value="60" /><entry key="com.sun.jndi.ldap.read.timeout" value="60" /><entry key="java.naming.security.authentication" value="simple" /></map></property></bean><sec:user-service id="userDetailsService"><sec:user name="@@THIS SHOULD BE REPLACED@@" password="notused"authorities="ROLE_ADMIN" /></sec:user-service><bean id="attributeRepository"class="org.jasig.services.persondir.support.ldap.CASLdapPersonAttributeDao"><property name="contextSource" ref="contextSource" /><property name="baseDN" value="o=company,dc=sky,dc=org" /><property name="requireAllQueryAttributes" value="true" /><property name="queryAttributeMapping"><map><entry key="username" value="uid" /></map></property><property name="resultAttributeMapping"><map><entry key="uid" value="loginid" /><entry key="mail" value="email" /></map></property></bean><bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl"><property name="registeredServices"><list><bean class="org.jasig.cas.services.RegexRegisteredService"><property name="id" value="0" /><property name="name" value="HTTP and IMAP" /><property name="description" value="Allows HTTP(S) and IMAP(S) protocols" /><property name="serviceId" value="^(https?|imaps?)://.*" /><property name="evaluationOrder" value="10000001" /><property name="ignoreAttributes" value="false" /><property name="allowedAttributes"><list><value>loginid</value><value>email</value></list></property></bean></list></property></bean><bean id="auditTrailManager"class="com.github.inspektr.audit.support.Slf4jLoggingAuditTrailManager" /><bean id="healthCheckMonitor" class="org.jasig.cas.monitor.HealthCheckMonitor"><property name="monitors"><list><bean class="org.jasig.cas.monitor.MemoryMonitor"p:freeMemoryWarnThreshold="10" /><!-- NOTE The following ticket registries support SessionMonitor: * DefaultTicketRegistry * JpaTicketRegistry Remove this monitor if you use an unsupported registry. --><bean class="org.jasig.cas.monitor.SessionMonitor"p:ticketRegistry-ref="ticketRegistry"p:serviceTicketCountWarnThreshold="5000"p:sessionCountWarnThreshold="100000" /></list></property></bean>
</beans>
在这个配置文件里,我们把attributeDao还有Resolver还有我们的Ldap认证时用的AuthenticationHandler都变成了我们自定义的类了,但还是有2段配置代码大家看起来有些疑惑,没关系,我们接着来分析接着来变态:
<bean id="attributeRepository"class="org.jasig.services.persondir.support.ldap.CASLdapPersonAttributeDao"><property name="contextSource" ref="contextSource" /><property name="baseDN" value="o=company,dc=sky,dc=org" /><property name="requireAllQueryAttributes" value="true" /><property name="queryAttributeMapping"><map><entry key="username" value="uid" /></map></property><property name="resultAttributeMapping"><map><entry key="uid" value="loginid" /><entry key="mail" value="email" /></map></property></bean>
看到这边的resultAttributeMapping,它的意思就是:根据 上面的“queryAttributeMapping”的这个键值找到ldap中该条数据,然后通过resultAttributeMapping返回给客户端 ,这段配置做的就是这么一件事。
- 一定要在queryAttributeMapping的entry key=后面写上"username”,这个username来自于我们cas sso登录主界面中的username这个属性。
- 在resultAttributeMapping中key为LDAP中相关数据的“主键”,value就是我们希望让客户端通过以下代码获取到CAS SSO服务端传过来的值的那个key,千万不要搞错了哦。
Map attributes = principal.getAttributes();
String email = (String) attributes.get("email");
<bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl"><property name="registeredServices"><list><bean class="org.jasig.cas.services.RegexRegisteredService"><property name="id" value="0" /><property name="name" value="HTTP and IMAP" /><property name="description" value="Allows HTTP(S) and IMAP(S) protocols" /><property name="serviceId" value="^(https?|imaps?)://.*" /><property name="evaluationOrder" value="10000001" /><property name="ignoreAttributes" value="false" /><property name="allowedAttributes"><list><value>loginid</value><value>email</value></list></property></bean></list></property></bean>
看到这个地方了吗?
<property name="allowedAttributes"><list><value>loginid</value><value>email</value></list></property>
这段XML配置的意思就是: 根据上面的“queryAttributeMapping”的这个键值找到ldap中该条数据,然后通过resultAttributeMapping返回给客户端,并且“允许“loginid”与"email“两个值可以通过客户端使用如下的的代码被允许访问得到:
Map attributes = principal.getAttributes();
String email = (String) attributes.get("email");
很烦? 不是,其实不烦,这是因为老外的框架做的严谨,而且扩展性好,只要通过extend, implement就可以实现我们自己的功能了,这种设计很强,或者说很变态,因为接下去还没完呢,哈哈,继续。
修改cas sso的主登录界面,把界面修改成如下风格
修改src/main/webapp/WEB-INF/view/jsp/default/ui/casLoginView.jsp
<select id="companyid" name="companyid" > <option value="101" selected>上海煤气公司</option><option value="102" selected>上海自来水厂</option><option value="103" selected>FBI</option><option value="104" selected>神盾局</option>
</select>
我在这边就不细说了,这属于copy & paste的工作,我在此就直接给出我自己制作完成后的casLoginView.jsp页面内所有的源码吧:
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<%@ page session="true"%>
<%@ page pageEncoding="utf-8"%>
<%@ page contentType="text/html; charset=utf-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>
<%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>
<html>
<head><link href="${pageContext.request.contextPath}/css/login.css"rel="stylesheet" type="text/css" />
<link href="${pageContext.request.contextPath}/css/login_form.css"rel="stylesheet" type="text/css" /><script language="javascript">var relativePath="<%=request.getContextPath()%>";
</script>
<title>CAS SSO登录</title>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
</head>
<body id="cas"><div style="text-align: center;"></div><form:form method="post" id="fm1" commandName="${commandName}"htmlEscape="true" style="height:300px"><div class="login_div" id="login"><table border="0" cellspacing="0" cellpadding="0"><tr><td colspan="2" style="border-bottom: 1px solid #e5e9ee;"><img src="${pageContext.request.contextPath}/css/images/login_dot.png" width="24" height="24" hspace="5" align="absbottom" />登录</td></tr><tr><td width="175" class="label"> 用户名:</td><td width="405"><c:if test="${empty sessionScope.openIdLocalId}"><spring:message code="screen.welcome.label.netid.accesskey"var="userNameAccessKey" /><form:input οnblur="refreshOrgList();" id="username"tabindex="1" accesskey="${userNameAccessKey}" path="username"/></c:if></td></tr><tr><td class="label">密码:</td><td><form:password cssClass="required" cssErrorClass="error"id="password" size="25" tabindex="2" path="password"accesskey="${passwordAccessKey}" autocomplete="off" /></td></tr><tr><td class="label">公司ID:</td><td> <select id="companyid" name="companyid" > <option value="101" selected>上海煤气公司</option><option value="102" selected>上海自来水厂</option><option value="103" selected>FBI</option><option value="104" selected>神盾局</option> </select></td></tr> <tr><td class="label"></td><td><font color="red"><form:errors id="msg" class="errors" /> </font></td></tr></table>
</div>
<div class="but_div">
<input type="hidden" name="lt" value="${loginTicket}" />
<input type="hidden" name="execution" value="${flowExecutionKey}" />
<input type="hidden" name="_eventId" value="submit" />
<input name="submit" accesskey="l" class="login_but" value="<spring:message code="screen.welcome.button.login" />"tabindex="4" type="submit" />
<input name="button2" type="reset" class="cancel_but" id="button2" value="取 消" /></div></form:form><div class="loginbottom_div"><div>Copyright © 红肠啃僵尸 reserved.</div></div>
</body>
你可以直接使用我做的页面,我把它也上传在”资源共享”中了,你也可以自己照着我这个jsp动手去改,改前请一定记得保存好原文件,反正改坏了你就再改一遍,改个4,5次也就习惯了,呵呵!
修改src/main/webapp/WEB-INF/view/jsp/default/protocol/2.0/casServiceValidationSuccess.jsp
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>
好了,终于全改完了,开始书写我们的客户端来做这个测试吧。
制作测试用客户端工程
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());@Overridepublic void sessionCreated(HttpSessionEvent se) {HttpSession session = null;try {session = se.getSession();// get valueServletContext context = session.getServletContext();String timeoutValue = context.getInitParameter("sessionTimeout");int timeout = Integer.valueOf(timeoutValue);// set valuesession.setMaxInactiveInterval(timeout);logger.info(">>>>>>session max inactive interval has been set to "+ timeout + " seconds.");} catch (Exception ex) {ex.printStackTrace();}}@Overridepublic 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);}@Overridepublic void destroy() {// TODO Auto-generated method stub}@Overridepublic 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);}}@Overridepublic void init(FilterConfig config) throws ServletException {// TODO Auto-generated method stubthis.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了)。
- 增加以下这段代码
<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扩展了一个参数。
uinfo = (UserSession) se.getAttribute(WebConstants.USER_SESSION_OBJECT);AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal();String userName = principal.getName();logger.info("userName: " + userName);if (userName != null && userName.length() > 0 && uinfo == null) {Map attributes = principal.getAttributes();String email = (String) attributes.get("email");uinfo = new UserSession();String[] userAttri = userName.split(",");uinfo.setUserName(userAttri[0]);uinfo.setCompanyId(userAttri[1]);uinfo.setUserEmail(email);se.setAttribute(WebConstants.USER_SESSION_OBJECT, uinfo);}
好了,我们现在要做的就是为cas-sample-site1/2各配上一个用于显示我们是否能够成功从cas-server端传过来登录成功后用户信息的index.jsp了。
cas-sample-site1/index.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"pageEncoding="utf-8"%>
<%@ page import="org.sky.framework.session.UserSession, org.sky.util.WebConstants" %>
<%UserSession us=(UserSession)session.getAttribute(WebConstants.USER_SESSION_OBJECT);String uname=us.getUserName();String email=us.getUserEmail();String companyId=us.getCompanyId();
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>cas sample site1</title>
</head>
<body>
<h1>cas sample site1 Hello: <%=uname%>(<%=email%>) u are@Company: <%=companyId%></h1>
</p>
<a href="http://localhost:8080/cas-sample-site2/index.jsp">cas-sample-site2</a></br>
<a href="http://localhost:8080/cas-server/logout">退出</a>
</body>
</html>
cas-sample-site2/index.jsp
<%@ page language="java" contentType="text/html; charset=utf-8"pageEncoding="utf-8"%>
<%@ page import="org.sky.framework.session.UserSession, org.sky.util.WebConstants" %>
<%UserSession us=(UserSession)session.getAttribute(WebConstants.USER_SESSION_OBJECT);String uname=us.getUserName();String email=us.getUserEmail();String companyId=us.getCompanyId();
%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
<title>cas sample site2</title>
</head>
<body>
<h1>cas sample site2 Hello: <%=uname%>(<%=email%>) u are@Company: <%=companyId%></h1>
<a href="http://localhost:8080/cas-sample-site1/index.jsp">cas-sample-site1</a>
</br>
<a href="http://localhost:8080/cas-server/logout">退出</a>
</body>
</html>
运行今天所有的例子
dn: dc=sky,dc=org
dc: sky
objectClass: top
objectClass: domaindn: o=company,dc=sky,dc=org
objectClass: organization
o: companydn: ou=members,o=company,dc=sky,dc=org
objectClass: organizationalUnit
ou: membersdn: cn=user1,ou=members,o=company,dc=sky,dc=org
sn: user1
cn: user1
userPassword: aaaaaa
objectClass: organizationalPersondn: cn=user2,ou=members,o=company,dc=sky,dc=org
sn: user2
cn: user2
userPassword: abcdefg
objectClass: organizationalPersondn: 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: organizationdn: o=102,o=company,dc=sky,dc=org
o: 102
objectClass: organizationdn: o=103,o=company,dc=sky,dc=org
o: 103
objectClass: organizationdn: o=104,o=company,dc=sky,dc=org
o: 104
objectClass: organizationdn: 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.netdn: 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.netdn: 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.netdn: 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.chrisdn: 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
把:
- cas-server
- cas-sample-site1
- cas-sample-site2
- 我们在用户名处输入jason
- 密码输入aaaaaa
- 公司ID选择成“上海自来水厂”
<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,说明我们的传值传对了。
- 自定义CAS SSO登录界面
- 在CAS SSO登录界面增加我们自定义的登录用元素
- 使用LDAP带出登录用户在LDAP内存储的其它更多的信息
- 实现了CAS SSO支持多租户登录的功能
单点登录之CAS SSO从入门到精通(第三天)相关推荐
- 单点登录之CAS SSO从入门到精通(第二天)
啊......沙滩,阳光,笔记本往膝上一搁,开始写博客.第一次没在国内过年,避开了吃吃吃,感觉真好,人也觉得轻松多了. 上次说到了CAS SSO最基本的使用方法,并且我们结合了一个数据库表来实现了我们 ...
- 开源的单点登录系统CAS入门
单点登录 单点登录(Single Sign On),简称为SSO,是目前比较流行的企业业务整合的解决方案之一.SSO的定义是在多个应用系统中,用户只需要登录一次就可以访问所有相互信任的应用系统. 当一 ...
- 开源单点登录系统CAS入门
单点登录系统CAS 目录 单点登录系统CAS 什么是单点登录? CAS 介绍 CAS 服务端部署 (1)复制war包到tomcat (2)运行tomcat (3)访问CAS登录页面 CAS服务端配置 ...
- 单点登录之CAS原理和实现
1.开源单点登录系统CAS入门 1.1 什么是单点登录 单点登录(Single Sign On),简称为 SSO,是目前比较流行的企业业务整合的解决方案之一.SSO的定义是在多个应用系统中,用户只需要 ...
- 单点登录解决方案CAS
#单点登录解决方案CAS ##一.单点登录 单点登录SSO,实现跨域登录.当用户第一次访问系统的时候,会被引导进入认证系统中进行登录:根据用户提供的登录信息,认证系统进行身份校验,如果通过校验,应 ...
- 单点登录之CAS原理和实现(转载)
转载源:https://www.jianshu.com/p/613c615b7ef1 单点登录之CAS原理和实现 来源于作者刘欣的<码农翻身> + 自己的备注理解 这家集团公司财大气粗,竟 ...
- pigx-cas 单点登录(一)——初识SSO
pigx-cas 单点登录(一)--初识SSO @(Pigx)[笔记, 单点, 登录] 前言:工作中目前采用CAS,从零开始学习与生产应用实践,这里针对模块知识点,根据本机逐渐记录. 初始CAS 单点 ...
- 单点登录 - 修改CAS服务器的一些配置( 陆续添加)
2019独角兽企业重金招聘Python工程师标准>>> 注意:本系列采用的CAS版本是4.1.xx,其它版本有可能不适用. 1.啰嗦一下原理 在spring中,读取属性文件prope ...
- cas java单点登录_java单点登录系统CAS的简单使用
http://blog.csdn.net/yunye114105/article/details/7997041 参考: http://blog.csdn.net/diyagea/article/de ...
最新文章
- pde中微元分析法的主要思想_果然是清北学霸,高中数学解题思想与技巧方法,学会不下145分...
- Python 技术篇-PyQt5动画功能演示,组件移动、尺寸改变动画演示
- 用正则表达式作html2RSS服务
- Unlinked Gradle project
- RedHat系列软件管理(第二版) --源码包安装
- 在多线程数据平面开发套件(DPDK)应用程序中优化内存使用
- 2015-12-02 计划任务维护数据库
- JQuery官方学习资料(译):类型
- 菜鸟的学习之路(11) — 堆栈与队列
- 四行代码创建复杂(无限级)树
- 【Python】嵌套类的定义与使用
- EDA软件_AD14绘制等长线
- 【错误记录】springboot项目报错Field xxx in com.xx.xx.xx.impl.xxImpl required a bean
- 目前计算机新技术应用领域,计算机的应用领域分为哪六个方面
- 简单的python爬虫--爬取Taobao淘女郎信息
- halcon测量总结
- 0063-【测序行业】-国内首个基于NGS技术的癌症多基因检测试剂盒获CFDA准产批件
- Allegro如何导入高清Logo、二维码、防静电标识等图片以及汉字
- 轮盘赌算法原理(ACO算法概率选择方法)
- 正则表达式的字符串匹配