单点登录之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/赵云
看张飞这个名字,看!!!
不同的company(租户)下有着相同的用户,但其实这是两个不用的用户,中国同名同姓的人多了去了,对吧,这时company_101的张飞登录是因该只看到它所属的company_101这个租户下所有的数据和信息吧,而不能跑到company_102中看到别人家的信息,对吧?
国外很多解决方案说是在我们的CAS SSO的配置文件里在绑定LDAP的context时写上多条这样的东西:
<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler" p:filter="uid=%u" p:searchBase="o=101,o=company,dc=sky,dc=org" p:contextSource-ref="contextSource" /> <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler" p:filter="uid=%u" p:searchBase="o=102,o=company,dc=sky,dc=org" p:contextSource-ref="contextSource" /> <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler" p:filter="uid=%u" p:searchBase="o=103,o=company,dc=sky,dc=org" p:contextSource-ref="contextSource" />
可是,我们想想:
- 我们的租户在我们的后台系统中是自动“开户”的,companyid是一个自动增加的,我从companyid_101现在增加到了companyid_110时,你是不是每次用户一开户,你就要去手动改这个CAS SSO中的配置文件呢?
- 如果你不嫌烦,好好好,你够狠,你就手工改吧!但是当你每次在配置文件中新增一条配置语句时,你的CAS SSO是不是要断服务重启啊?那你还怎么做到24*7的这种不间断服务啊
一般来说,我们的开户是用程序自动写入LDAP中去的,即LDAP中的company_101, company_102, company_103是由程序自动生成的,那我们的程序就需要能够让用户在登录后台B2B系统时自动可以根据用户选择的租户来为用户正确登录的这么一种自动识别功能,就好比下面这样的一个登录界面:
看到这个界面了吗?
对的,这个就是CAS SSO的主登录界面,我把它都给改了,还加入了支持多租户登录的功能,我们今天就要来讲这个功能是怎么做出来的,包括如何去定制自己的CAS SSO的登录界面。
再来看看用于今天练习的我们在LDAP中的组织结构是怎么样的吧。
看到上面这张图了吧,这就是我说的“多租户”的概念,大家应该记得我们在CAS SSO第二天中怎么去拿CAS SSO绑定LDAP中的一条UserDN然后去搜索的吧?
<bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler" p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org" p:contextSource-ref="contextSource" />对吧!!!
现在我们要做到的就是:
p:searchBase="xxx.xxx.xx"
这条要做成动态的,比如说:
- 用户是company_id=101的,这时这个p:searchBase就应该变为:“p:searchBase="uid=sky,o=101,o=company,dc=sky,dc=org"
- 用户是company_id=102的,这时这个p:searchBase就应该变为:“p:searchBase="uid=jason,o=102,o=company,dc=sky,dc=org"
前面我们提到过,这些配置是放在XML文件中的,因此每次增加一个”租户“我们要手工在XML配置文件中新增一条,这个不现实,它是实现不了我们的24*7的这种服务的要求的,我们要做的是可以让这个p:searchBase能够动态的去组建这个userDN,所以重点是要解决这个问题。
该问题在国外的YALE CAS论坛上有两种解决方案:
- 一种是直接通过CAS的登录界面然后在输入用户名时要求用户以这种形式“uid=sky,o=101"去输入它的用户名,这种做法先不去说会造成用户登录时的困扰,而且CAS SSO的登录界面也不支持这样格式的用户名输入。
- 一种就是很笨的在CAS SSO的配置文件中绑定多个p:searchBase,这个方法已经被我们否掉了。
因此,笔者在这边要提的将是独创的可以做到全动态的去根据用户名,密码和所该用户所属组织自动在后台创建p:searchBase的最完美的解决方案,下面我们就开始吧。
创建工程
我们这次是要在CAS SSO这个产品上做扩展了,为此,我们不能再像我们第一天和第二天中那样直接拿个文本编辑器去改CAS SSO里的配置文件了,我们需要创建一个eclipse工程,来看我们的eclipse工程。
look,今天我们把这个cas-server放到了eclipse工程中去了,然后在eclipse里随改随测试,现在我们就来讲述如何创建这个工程以使得cas server可以运行在我们的eclipse的工程中。
因此我们在eclipse中新建一个java工程-是java工程你可千万不要建成j2ee工程啊,然后按照上图建立相应的目录。
CAS SERVER工程的组建
导入所有的配置文件
这是我们在第一天,第二天中布署在tomcat下的cas server工程的目录:
D:\tomcat\webapps\cas-server\WEB-INF\classes
把这个目录下所有的内容,除去以下2个目录:
- org
- META-INF
外所有的东西统统拷贝入eclipse中的cas-server工程中的src/main/resources目录下
构建WEB-INF目录
将D:\tomcat\webapps\cas-server\WEB-INF目录下这几个目录放入cas-server工程的src/main/webapp/WEB-INF目录下
构建cas-server基本源码
解压开我们下载的”cas-server-3.5.2-release"包,内含源码,它位于这样的一个目录cas-server-3.5.2\cas-server-webapp\src\main\java“
将这个目录下所有的文件置于cas-server工程的src/main/java目录下
并在eclipse工程中做如下设置
此处需要注意的是我们把:
- src/main/java
- src/main/resources
这两个目录做成编译路径,而src/main/webapp不作为编译路径。
别忘了把所有的src/main/webapp/WEB-INF/lib目录下的jar加到cas-server工程的Libraries中去。
构建webapp目录
将我们在第一天、第二天中布署在tomcat中的case-server中以下这些目录
拷贝到eclipse的cas-server工程中的src/main/webapp目录
CAS SSO在jboss/weblogic下的bug的修正
由于我们的eclipse中的cas-server将和我们的cas-sample-site1以及cas-sample-site2启动在jboss下,因此cas sso在jboss或者是在weblogic下有两个小问题,在此需要修正。
- META-INF文件内的persistence.xml中报HSQLDialect错误
- 报log4jConfiguration.xml文件在启动时找不到的错误
下面我们来看如何修正这两个小BUG。
修正CAS SSO的persistence.xml文件中的HSQLDialect错误
这是原始的/META-INF/persistence.xml文件的内容:
<class>org.jasig.cas.services.AbstractRegisteredService</class> <class>org.jasig.cas.services.RegexRegisteredService</class> <class>org.jasig.cas.services.RegisteredServiceImpl</class> <class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class> <class>org.jasig.cas.ticket.ServiceTicketImpl</class> <class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class>
我们在文件最后加入以下配置代码
<class>org.jasig.cas.services.AbstractRegisteredService</class> <class>org.jasig.cas.services.RegexRegisteredService</class> <class>org.jasig.cas.services.RegisteredServiceImpl</class> <class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class> <class>org.jasig.cas.ticket.ServiceTicketImpl</class> <class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" /> </properties>这是完整的改完后的persistence.xml文件的内容:
<persistence xmlns="http://java.sun.com/xml/ns/persistence" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd" version="2.0"> <persistence-unit name="CasPersistence" transaction-type="RESOURCE_LOCAL"> <class>org.jasig.cas.services.AbstractRegisteredService</class> <class>org.jasig.cas.services.RegexRegisteredService</class> <class>org.jasig.cas.services.RegisteredServiceImpl</class> <class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class> <class>org.jasig.cas.ticket.ServiceTicketImpl</class> <class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class> <properties> <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" /> </properties> </persistence-unit> </persistence>
修正CAS SSO中log4jConfiguration.xml文件在启动时找不到的错误
找到eclipse的cas-server工程中WEB-INF/spring-configuration/log4jConfiguration.xml文件,将这段内容注释掉
<bean id="log4jInitialization" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="targetClass" value="org.springframework.util.Log4jConfigurer"/> <property name="targetMethod" value="initLogging"/> <property name="arguments"> <list> <value>${log4j.config.location:classpath:log4j.xml}</value> <value>${log4j.refresh.interval:60000}</value> </list> </property> </bean>
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"> <!-- <bean id="log4jInitialization" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean"> <property name="targetClass" value="org.springframework.util.Log4jConfigurer"/> <property name="targetMethod" value="initLogging"/> <property name="arguments"> <list> <value>${log4j.config.location:classpath:log4j.xml}</value> <value>${log4j.refresh.interval:60000}</value> </list> </property> </bean> --> </beans>
改完后请保存。
将CAS-SERVER从eclipse java工程改为j2ee工程
右键单击cas-sso工程,选择project properties,然后选择project facet,按照如下截图来做选择。
组装可在eclipse中启动的cas-server web工程
右键单击cas-sso工程,选择project properties,然后选择Deployment Assembly,这个基本功我已经在 通向架构师的道路(第二十天)万能框架spring(二)maven结合spring与ibatis中详细讲述过这个Deployment Assembly是干什么用的了。
在eclipse中启动cas-server工程
一切无误后请在eclipse中启动cas-sso吧。
开始修改源码
如何让cas-server支持动态的p:searchBase呢
我们的p:searchBase从这一层o=company,dc=sky,dc=org开始要进行动态组装,因此我们将在deployConfiguration.xml文件中将我们的ldap的p:searchBase的绑定改成如下:
p:searchBase="o=company,dc=sky,dc=org",然后我们使用程序动态组建o=company,dc=sky,dc=org之前的内容到底是该“o=101”呢还是因该是“o=102” 这样的串。
为cas server的登录增加一个项
原来的cas sso的登录项只有两个属性:
- username
- password
我们需要增加一个companyid,用于判断当前登录的用户是属于哪个租户的。
新建CASCredential类
public class CASCredential extends RememberMeUsernamePasswordCredentials { private static final long serialVersionUID = 1L; private Map<String, Object> param; private String companyid; /** * @return the companyid */ public String getCompanyid() { return companyid; } /** * @param companyid the companyid to set */ public void setCompanyid(String companyid) { this.companyid = companyid; } public Map<String, Object> getParam() { return param; } public void setParam(Map<String, Object> param) { this.param = param; } }
这就是我们扩展的CASCredential类,该类除了拥有原来CAS SSO基本credential中的username和password两个属性外还有一个叫companyid的属性。
将新增的companyid绑定至cas sso的登录页面
修改src/main/webapp/WEB-INF/login-webflow.xml文件,找到以下这段:
<view-state id="viewLoginForm" view="casLoginView" model="credentials"> <binder> <binding property="username" /> <binding property="password" /> </binder>
将其改成:
<view-state id="viewLoginForm" view="casLoginView" model="credentials"> <binder> <binding property="username" /> <binding property="password" /> <binding property="companyid"/> </binder>
扩展CAS SSO登录页面的submit行为以支持我们在页面中新增的companyid属性可以被提交到CAS SSO的后台
新建一个类CASAuthenticationViaFormAction,内容如下:
package org.sky.cas.auth; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.validation.constraints.NotNull; import org.jasig.cas.CentralAuthenticationService; import org.jasig.cas.authentication.handler.AuthenticationException; import org.jasig.cas.authentication.principal.Credentials; import org.jasig.cas.authentication.principal.Service; import org.jasig.cas.ticket.TicketException; import org.jasig.cas.web.bind.CredentialsBinder; import org.jasig.cas.web.support.WebUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.binding.message.MessageBuilder; import org.springframework.binding.message.MessageContext; import org.springframework.util.StringUtils; import org.springframework.web.util.CookieGenerator; import org.springframework.webflow.core.collection.MutableAttributeMap; import org.springframework.webflow.execution.RequestContext; @SuppressWarnings("deprecation") public class CASAuthenticationViaFormAction { /** * Binder that allows additional binding of form object beyond Spring * defaults. */ private CredentialsBinder credentialsBinder; /** Core we delegate to for handling all ticket related tasks. */ @NotNull private CentralAuthenticationService centralAuthenticationService; @NotNull private CookieGenerator warnCookieGenerator; protected Logger logger = LoggerFactory.getLogger(getClass()); public final void doBind(final RequestContext context, final Credentials credentials) throws Exception { final HttpServletRequest request = WebUtils.getHttpServletRequest(context); if (this.credentialsBinder != null && this.credentialsBinder.supports(credentials.getClass())) { this.credentialsBinder.bind(request, credentials); } } public final String submit(final RequestContext context, final Credentials credentials, final MessageContext messageContext) throws Exception { String companyid = ""; // Validate login ticket final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context); final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context); if (credentials instanceof CASCredential) { String companyCode = "compnayid"; CASCredential rmupc = (CASCredential) credentials; companyid = rmupc.getCompanyid(); } if (!authoritativeLoginTicket.equals(providedLoginTicket)) { this.logger.warn("Invalid login ticket " + providedLoginTicket); final String code = "INVALID_TICKET"; messageContext.addMessage(new MessageBuilder().error().code(code).arg(providedLoginTicket).defaultText(code).build()); return "error"; } final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context); final Service service = WebUtils.getService(context); if (StringUtils.hasText(context.getRequestParameters().get("renew")) && ticketGrantingTicketId != null && service != null) { try { final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId, service, credentials); WebUtils.putServiceTicketInRequestScope(context, serviceTicketId); putWarnCookieIfRequestParameterPresent(context); return "warn"; } catch (final TicketException e) { if (isCauseAuthenticationException(e)) { populateErrorsInstance(e, messageContext); return getAuthenticationExceptionEventId(e); } this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId); if (logger.isDebugEnabled()) { logger.debug("Attempted to generate a ServiceTicket using renew=true with different credentials", e); } } } try { CASCredential rmupc = (CASCredential) credentials; WebUtils.putTicketGrantingTicketInRequestScope(context, centralAuthenticationService.createTicketGrantingTicket(rmupc)); putWarnCookieIfRequestParameterPresent(context); return "success"; } catch (final TicketException e) { populateErrorsInstance(e, messageContext); if (isCauseAuthenticationException(e)) return getAuthenticationExceptionEventId(e); return "error"; } } private void populateErrorsInstance(final TicketException e, final MessageContext messageContext) { try { messageContext.addMessage(new MessageBuilder().error().code(e.getCode()).defaultText(e.getCode()).build()); } catch (final Exception fe) { logger.error(fe.getMessage(), fe); } } private void putWarnCookieIfRequestParameterPresent(final RequestContext context) { final HttpServletResponse response = WebUtils.getHttpServletResponse(context); if (StringUtils.hasText(context.getExternalContext().getRequestParameterMap().get("warn"))) { this.warnCookieGenerator.addCookie(response, "true"); } else { this.warnCookieGenerator.removeCookie(response); } } private AuthenticationException getAuthenticationExceptionAsCause(final TicketException e) { return (AuthenticationException) e.getCause(); } private String getAuthenticationExceptionEventId(final TicketException e) { final AuthenticationException authEx = getAuthenticationExceptionAsCause(e); if (this.logger.isDebugEnabled()) this.logger.debug("An authentication error has occurred. Returning the event id " + authEx.getType()); return authEx.getType(); } private boolean isCauseAuthenticationException(final TicketException e) { return e.getCause() != null && AuthenticationException.class.isAssignableFrom(e.getCause().getClass()); } public final void setCentralAuthenticationService(final CentralAuthenticationService centralAuthenticationService) { this.centralAuthenticationService = centralAuthenticationService; } /** * Set a CredentialsBinder for additional binding of the HttpServletRequest * to the Credentials instance, beyond our default binding of the * Credentials as a Form Object in Spring WebMVC parlance. By the time we * invoke this CredentialsBinder, we have already engaged in default binding * such that for each HttpServletRequest parameter, if there was a JavaBean * property of the Credentials implementation of the same name, we have set * that property to be the value of the corresponding request parameter. * This CredentialsBinder plugin point exists to allow consideration of * things other than HttpServletRequest parameters in populating the * Credentials (or more sophisticated consideration of the * HttpServletRequest parameters). * * @param credentialsBinder the credentials binder to set. */ public final void setCredentialsBinder(final CredentialsBinder credentialsBinder) { this.credentialsBinder = credentialsBinder; } public final void setWarnCookieGenerator(final CookieGenerator warnCookieGenerator) { this.warnCookieGenerator = warnCookieGenerator; } }
这个类很简单,主要是第59行到第64行的:
if (credentials instanceof CASCredential) { String companyCode = "compnayid"; CASCredential rmupc = (CASCredential) credentials; companyid = rmupc.getCompanyid(); }
以及第98行到第100行的:
CASCredential rmupc = (CASCredential) credentials; WebUtils.putTicketGrantingTicketInRequestScope(context, centralAuthenticationService.createTicketGrantingTicket(rmupc));它告诉了CAS SSO使用我们自定义的CASCredential来验证用户在CAS SSO中的登录信息,而不是原来CAS SSO默认的UsernameAndPasswordCredential。
把”CASAuthenticationViaFormAction“类注册给CAS SSO,告诉CAS SSO在登录页面点击”登录“按钮后能够使用这个我们自定义的submit action:
修改配置文件:src/main/webapp/WEB-INF/cas-servlet.xml
找到以下这行:
<bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction" p:centralAuthenticationService-ref="centralAuthenticationService" p:warnCookieGenerator-ref="warnCookieGenerator"/>把它注释掉改成:
<!-- <bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction" p:centralAuthenticationService-ref="centralAuthenticationService" p:warnCookieGenerator-ref="warnCookieGenerator"/> --> <bean id="authenticationViaFormAction" class="org.sky.cas.auth.CASAuthenticationViaFormAction" p:centralAuthenticationService-ref="centralAuthenticationService" p:warnCookieGenerator-ref="warnCookieGenerator" />
此时,CAS SSO的登录界面在用户点击submit按钮时,就会使用我们自定义的这个CASAuthenticationViaFormAction类了。
增加p:searchBase使得CAS SSO的LDAP可以根据不同的companyid动态搜索用户的功能
新增一个类CASLDAPAuthenticationHandler,代码如下:package org.sky.cas.auth; import org.jasig.cas.adaptors.ldap.AbstractLdapUsernamePasswordAuthenticationHandler; import org.jasig.cas.authentication.handler.AuthenticationException; import org.jasig.cas.authentication.principal.UsernamePasswordCredentials; import org.jasig.cas.util.LdapUtils; import org.springframework.ldap.NamingSecurityException; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.core.NameClassPairCallbackHandler; import org.springframework.ldap.core.SearchExecutor; import javax.naming.NameClassPair; import javax.naming.NamingEnumeration; import javax.naming.NamingException; import javax.naming.directory.DirContext; import javax.naming.directory.SearchControls; import javax.validation.constraints.Max; import javax.validation.constraints.Min; import java.util.ArrayList; import java.util.List; public class CASLDAPAuthenticationHandler extends AbstractLdapUsernamePasswordAuthenticationHandler { /** The default maximum number of results to return. */ private static final int DEFAULT_MAX_NUMBER_OF_RESULTS = 1000; /** The default timeout. */ private static final int DEFAULT_TIMEOUT = 1000; /** The search base to find the user under. */ private String searchBase; /** The scope. */ @Min(0) @Max(2) private int scope = SearchControls.ONELEVEL_SCOPE; /** The maximum number of results to return. */ private int maxNumberResults = DEFAULT_MAX_NUMBER_OF_RESULTS; /** The amount of time to wait. */ private int timeout = DEFAULT_TIMEOUT; /** Boolean of whether multiple accounts are allowed. */ private boolean allowMultipleAccounts; protected final boolean authenticateUsernamePasswordInternal(final UsernamePasswordCredentials credentials) throws AuthenticationException { CASCredential rmupc = (CASCredential) credentials; final String companyid = rmupc.getCompanyid(); final List<String> cns = new ArrayList<String>(); final SearchControls searchControls = getSearchControls(); final String transformedUsername = getPrincipalNameTransformer().transform(credentials.getUsername()); final String filter = LdapUtils.getFilterWithValues(getFilter(), transformedUsername); try { this.getLdapTemplate().search(new SearchExecutor() { public NamingEnumeration executeSearch(final DirContext context) throws NamingException { String baseDN = ""; if (companyid != null && companyid.trim().length() > 0) { baseDN = "o=" + companyid + "," + searchBase; } else { baseDN = searchBase; } //System.out.println("searchBase=====" + baseDN); return context.search(baseDN, filter, searchControls); } }, new NameClassPairCallbackHandler() { public void handleNameClassPair(final NameClassPair nameClassPair) { cns.add(nameClassPair.getNameInNamespace()); } }); } catch (Exception e) { log.error("search ldap error casue: " + e.getMessage(), e); return false; } if (cns.isEmpty()) { log.debug("Search for " + filter + " returned 0 results."); return false; } if (cns.size() > 1 && !this.allowMultipleAccounts) { log.warn("Search for " + filter + " returned multiple results, which is not allowed."); return false; } for (final String dn : cns) { DirContext test = null; String finalDn = composeCompleteDnToCheck(dn, credentials); try { this.log.debug("Performing LDAP bind with credential: " + dn); test = this.getContextSource().getContext(finalDn, getPasswordEncoder().encode(credentials.getPassword())); if (test != null) { return true; } } catch (final NamingSecurityException e) { log.debug("Failed to authenticate user {} with error {}", credentials.getUsername(), e.getMessage()); return false; } catch (final Exception e) { this.log.error(e.getMessage(), e); return false; } finally { LdapUtils.closeContext(test); } } return false; } protected String composeCompleteDnToCheck(final String dn, final UsernamePasswordCredentials credentials) { return dn; } private SearchControls getSearchControls() { final SearchControls constraints = new SearchControls(); constraints.setSearchScope(this.scope); constraints.setReturningAttributes(new String[0]); constraints.setTimeLimit(this.timeout); constraints.setCountLimit(this.maxNumberResults); return constraints; } /** * Method to return whether multiple accounts are allowed. * @return true if multiple accounts are allowed, false otherwise. */ protected boolean isAllowMultipleAccounts() { return this.allowMultipleAccounts; } /** * Method to return the max number of results allowed. * @return the maximum number of results. */ protected int getMaxNumberResults() { return this.maxNumberResults; } /** * Method to return the scope. * @return the scope */ protected int getScope() { return this.scope; } /** * Method to return the search base. * @return the search base. */ protected String getSearchBase() { return this.searchBase; } /** * Method to return the timeout. * @return the timeout. */ protected int getTimeout() { return this.timeout; } public final void setScope(final int scope) { this.scope = scope; } /** * @param allowMultipleAccounts The allowMultipleAccounts to set. */ public void setAllowMultipleAccounts(final boolean allowMultipleAccounts) { this.allowMultipleAccounts = allowMultipleAccounts; } /** * @param maxNumberResults The maxNumberResults to set. */ public final void setMaxNumberResults(final int maxNumberResults) { this.maxNumberResults = maxNumberResults; } /** * @param searchBase The searchBase to set. */ public final void setSearchBase(final String searchBase) { this.searchBase = searchBase; } /** * @param timeout The timeout to set. */ public final void setTimeout(final int timeout) { this.timeout = timeout; } /** * Sets the context source for LDAP searches. This method may be used to * support use cases like the following: * <ul> * <li>Pooling of LDAP connections used for searching (e.g. via instance * of {@link org.springframework.ldap.pool.factory.PoolingContextSource}).</li> * <li>Searching with client certificate credentials.</li> * </ul> * <p> * If this is not defined, the context source defined by * {@link #setContextSource(ContextSource)} is used. * * @param contextSource LDAP context source. */ public final void setSearchContextSource(final ContextSource contextSource) { setLdapTemplate(new LdapTemplate(contextSource)); } }
这个类的作用就是给src/main/webapp/WEB-INF/deployerConfiguration.xml中以下这段用的:
<property name="authenticationHandlers"> <list> <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" /> <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler" p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org" p:contextSource-ref="contextSource" /> </list> </property>
请注意代码50行处:
final String companyid = rmupc.getCompanyid();
以及59行到69行处:
public NamingEnumeration executeSearch(final DirContext context) throws NamingException { String baseDN = ""; if (companyid != null && companyid.trim().length() > 0) { baseDN = "o=" + companyid + "," + searchBase; } else { baseDN = searchBase; } //System.out.println("searchBase=====" + baseDN); return context.search(baseDN, filter, searchControls); } }, new NameClassPairCallbackHandler() {
这就是在根据用户在登录界面中选择的companyid不同,而动态的去重组这个searchBase,以使得这个searchBase可以是o=101,o=company,dc=sky,dc=org, 也可以是o=102,o=company,dc=sky,dc=org同时它也可以变成o=103,o=company,dc=sky,dc=org。
有了这个类我们要修改我们的src/main/webapp/WEB-INF/deployerConfiguration.xml文件了,注意这个bean中的写法 ,已经被我修改掉了
<bean id="authenticationManager" class="org.jasig.cas.authentication.AuthenticationManagerImpl"> <property name="credentialsToPrincipalResolvers"> <list> <bean class="org.sky.cas.auth.CASCredentialsToPrincipalResolver"> <property name="attributeRepository" ref="attributeRepository" /> </bean> </list> </property> <property name="authenticationHandlers"> <list> <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" /> <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler" p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org" p:contextSource-ref="contextSource" /> </list> </property> </bean>
看到这个CASLDAPAuthenticationHandler类在这边的作用了吧。
将LDAP中登录用户的其它信息也带入到客户端登录成功后跳转的页面中去
我们知道,CAS SSO可以把username(uid)带入到客户端登录成功后的页面中去,可是一个uid在LDAP中还关联着许多其它有用的信息如:email。 还有就是我们刚才新增的companyid,我们也想把这些信息同时带到客户端登录成功的画面中去呢?
这边就需要使用到CAS SSO中的一个特殊的属性,它叫attributeRepository。
attributeRepository的作用
attributeRepository关联着一个dao和一个resolver,它们的作用如下:
- attributeDAO是用于根据searchBase在LDAP中定位到一条数据,然后把该条数据所有的属性取出来用的一个工具类
- credentialsToPrincipalResolvers,该类用于向客户端(就是我们的cas-samples-site1/site2)返回用户在CAS SSO中登录画面中输入的登录相关信息用的一个工具类
先来说attributeDAO的作用吧。
CASLdapPersonAttributeDao
比如说我们这边想要把ldap中某个uid的mail属性也带给到客户端中去
我们就要按照下面这段代码来书写CASLdapPersonAttributeDao类,该类扩展自”AbstractQueryPersonAttributeDao“类,它被置于”package org.jasig.services.persondir.support.ldap“包中,因为该包中还有其它相关的此类需要”引用"的工具类,我们不想到处import来import去了,因此直接把这个我们自定义的attributeDao类就直接放置于该包中了。
但是,嘿嘿嘿,在package org.jasig.services.persondir.support.ldap包中没有其它这个类需要引用的那些外部类,如下图所示:
怎么办?
很简单,直接找到cas-server 3.5.2的源码,将这两个外部类置于我们自定义的CASLdapPersonAttributeDao同一层的包路径下即可,我会在本文结束后直接给出完整的eclipse中可运行的cas-server的全部源码。
/** * Licensed to Jasig under one or more contributor license * agreements. See the NOTICE file distributed with this work * for additional information regarding copyright ownership. * Jasig licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a * copy of the License at: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jasig.services.persondir.support.ldap; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; import javax.naming.directory.SearchControls; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jasig.cas.util.CASCredentialHelper; import org.jasig.services.persondir.IPersonAttributes; import org.jasig.services.persondir.support.AbstractQueryPersonAttributeDao; import org.jasig.services.persondir.support.CaseInsensitiveAttributeNamedPersonImpl; import org.jasig.services.persondir.support.CaseInsensitiveNamedPersonImpl; import org.jasig.services.persondir.support.QueryType; import org.sky.cas.auth.LdapPersonInfoBean; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.InitializingBean; import org.springframework.ldap.core.AttributesMapper; import org.springframework.ldap.core.ContextSource; import org.springframework.ldap.core.LdapTemplate; import org.springframework.ldap.filter.EqualsFilter; import org.springframework.ldap.filter.Filter; import org.springframework.ldap.filter.LikeFilter; import org.springframework.util.Assert; /** * LDAP implementation of {@link org.jasig.services.persondir.IPersonAttributeDao}. * * In the case of multi valued attributes a {@link java.util.List} is set as the value. * * <br> * <br> * Configuration: * <table border="1"> * <tr> * <th align="left">Property</th> * <th align="left">Description</th> * <th align="left">Required</th> * <th align="left">Default</th> * </tr> * <tr> * <td align="right" valign="top">searchControls</td> * <td> * Set the {@link SearchControls} used for executing the LDAP query. * </td> * <td valign="top">No</td> * <td valign="top">Default instance with SUBTREE scope.</td> * </tr> * <tr> * <td align="right" valign="top">baseDN</td> * <td> * The base DistinguishedName to use when executing the query filter. * </td> * <td valign="top">No</td> * <td valign="top">""</td> * </tr> * <tr> * <td align="right" valign="top">contextSource</td> * <td> * A {@link ContextSource} from the Spring-LDAP framework. Provides a DataSource * style object that this DAO can retrieve LDAP connections from. * </td> * <td valign="top">Yes</td> * <td valign="top">null</td> * </tr> * <tr> * <td align="right" valign="top">setReturningAttributes</td> * <td> * If the ldap attributes set in the ldapAttributesToPortalAttributes Map should be copied * into the {@link SearchControls#setReturningAttributes(String[])}. Setting this helps reduce * wire traffic of ldap queries. * </td> * <td valign="top">No</td> * <td valign="top">true</td> * </tr> * <tr> * <td align="right" valign="top">queryType</td> * <td> * How multiple attributes in a query should be concatenated together. The other option is OR. * </td> * <td valign="top">No</td> * <td valign="top">AND</td> * </tr> * </table> * * @author andrew.petro@yale.edu * @author Eric Dalquist * @version $Revision$ $Date$ * @since uPortal 2.5 */ public class CASLdapPersonAttributeDao extends AbstractQueryPersonAttributeDao<LogicalFilterWrapper> implements InitializingBean { private static final Pattern QUERY_PLACEHOLDER = Pattern.compile("\\{0\\}"); private final static AttributesMapper MAPPER = new AttributeMapAttributesMapper(); protected final Log logger = LogFactory.getLog(getClass()); /** * The LdapTemplate to use to execute queries on the DirContext */ private LdapTemplate ldapTemplate = null; private String baseDN = ""; private String queryTemplate = null; private ContextSource contextSource = null; private SearchControls searchControls = new SearchControls(); private boolean setReturningAttributes = true; private QueryType queryType = QueryType.AND; public CASLdapPersonAttributeDao() { this.searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE); this.searchControls.setReturningObjFlag(false); } /* (non-Javadoc) * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet() */ public void afterPropertiesSet() throws Exception { final Map<String, Set<String>> resultAttributeMapping = this.getResultAttributeMapping(); if (this.setReturningAttributes && resultAttributeMapping != null) { this.searchControls.setReturningAttributes(resultAttributeMapping.keySet().toArray( new String[resultAttributeMapping.size()])); } if (this.contextSource == null) { throw new BeanCreationException("contextSource must be set"); } } /* (non-Javadoc) * @see org.jasig.services.persondir.support.AbstractQueryPersonAttributeDao#appendAttributeToQuery(java.lang.Object, java.lang.String, java.util.List) */ @Override protected LogicalFilterWrapper appendAttributeToQuery(LogicalFilterWrapper queryBuilder, String dataAttribute, List<Object> queryValues) { if (queryBuilder == null) { queryBuilder = new LogicalFilterWrapper(this.queryType); } for (final Object queryValue : queryValues) { String queryValueString = queryValue == null ? null : queryValue.toString(); LdapPersonInfoBean person = new LdapPersonInfoBean(); //person = CASCredentialHelper.getPersoninfoFromCredential(queryValueString); //queryValueString = person.getUsername(); person = CASCredentialHelper.getPersoninfoFromCredential(queryValueString); queryValueString=person.getUsername(); if (StringUtils.isNotBlank(queryValueString)) { final Filter filter; if (!queryValueString.contains("*")) { filter = new EqualsFilter(dataAttribute, queryValueString); } else { filter = new LikeFilter(dataAttribute, queryValueString); } queryBuilder.append(filter); } } return queryBuilder; } /* (non-Javadoc) * @see org.jasig.services.persondir.support.AbstractQueryPersonAttributeDao#getPeopleForQuery(java.lang.Object, java.lang.String) */ @Override protected List<IPersonAttributes> getPeopleForQuery(LogicalFilterWrapper queryBuilder, String queryUserName) { LdapPersonInfoBean ldapPerson = new LdapPersonInfoBean(); ldapPerson = CASCredentialHelper.getPersoninfoFromCredential(queryUserName); final String generatedLdapQuery = queryBuilder.encode(); //If no query is generated return null since the query cannot be run if (StringUtils.isBlank(generatedLdapQuery)) { return null; } //Insert the generated query into the template if it is configured final String ldapQuery; if (this.queryTemplate == null) { ldapQuery = generatedLdapQuery; } else { final Matcher queryMatcher = QUERY_PLACEHOLDER.matcher(this.queryTemplate); ldapQuery = queryMatcher.replaceAll(generatedLdapQuery); } String searchBase = ""; if (ldapPerson.getCompanyid().trim().length() > 0) { searchBase = "o=" + ldapPerson.getCompanyid() + "," + baseDN; } else { searchBase = baseDN; } logger.info("searchBase=====" + searchBase); //Execute the query List<Map<String, List<Object>>> queryResults = new ArrayList<Map<String, List<Object>>>(); try { queryResults = this.ldapTemplate.search(searchBase, ldapQuery, this.searchControls, MAPPER); } catch (Exception e) { logger.error( "search ldap with [searchBase===" + searchBase + "] [ldapQuery====" + ldapQuery + "], caused by: " + e.getMessage(), e); } final List<IPersonAttributes> peopleAttributes = new ArrayList<IPersonAttributes>(queryResults.size()); for (final Map<String, List<Object>> queryResult : queryResults) { IPersonAttributes person; //if (ldapPerson.getUsername() != null) { if (queryUserName != null && queryUserName.trim().length() > 0) { //person = new CaseInsensitiveNamedPersonImpl(ldapPerson.getUsername(), queryResult); person = new CaseInsensitiveNamedPersonImpl(queryUserName, queryResult); } else { //Create the IPersonAttributes doing a best-guess at a userName attribute String userNameAttribute = this.getConfiguredUserNameAttribute(); person = new CaseInsensitiveAttributeNamedPersonImpl(userNameAttribute, queryResult); } peopleAttributes.add(person); } return peopleAttributes; } /** * @see javax.naming.directory.SearchControls#getTimeLimit() * @deprecated Set the property on the {@link SearchControls} and set that via {@link #setSearchControls(SearchControls)} */ @Deprecated public int getTimeLimit() { return this.searchControls.getTimeLimit(); } /** * @see javax.naming.directory.SearchControls#setTimeLimit(int) * @deprecated */ @Deprecated public void setTimeLimit(int ms) { this.searchControls.setTimeLimit(ms); } /** * @return The base distinguished name to use for queries. */ public String getBaseDN() { return this.baseDN; } /** * @param baseDN The base distinguished name to use for queries. */ public void setBaseDN(String baseDN) { if (baseDN == null) { baseDN = ""; } this.baseDN = baseDN; } /** * @return The ContextSource to get DirContext objects for queries from. */ public ContextSource getContextSource() { return this.contextSource; } /** * @param contextSource The ContextSource to get DirContext objects for queries from. */ public synchronized void setContextSource(final ContextSource contextSource) { Assert.notNull(contextSource, "contextSource can not be null"); this.contextSource = contextSource; this.ldapTemplate = new LdapTemplate(this.contextSource); } /** * Sets the LdapTemplate, and thus the ContextSource (implicitly). * * @param ldapTemplate the LdapTemplate to query the LDAP server from. CANNOT be NULL. */ public synchronized void setLdapTemplate(final LdapTemplate ldapTemplate) { Assert.notNull(ldapTemplate, "ldapTemplate cannot be null"); this.ldapTemplate = ldapTemplate; this.contextSource = this.ldapTemplate.getContextSource(); } /** * @return Search controls to use for LDAP queries */ public SearchControls getSearchControls() { return this.searchControls; } /** * @param searchControls Search controls to use for LDAP queries */ public void setSearchControls(SearchControls searchControls) { Assert.notNull(searchControls, "searchControls can not be null"); this.searchControls = searchControls; } /** * @return the queryType */ public QueryType getQueryType() { return queryType; } /** * Type of logical operator to use when joining WHERE clause components * * @param queryType the queryType to set */ public void setQueryType(QueryType queryType) { this.queryType = queryType; } public String getQueryTemplate() { return this.queryTemplate; } /** * Optional wrapper template for the generated part of the query. Use {0} as a placeholder for where the generated query should be inserted. */ public void setQueryTemplate(String queryTemplate) { this.queryTemplate = queryTemplate; } }
注意206到211行处的写法:
String searchBase = ""; if (ldapPerson.getCompanyid().trim().length() > 0) { searchBase = "o=" + ldapPerson.getCompanyid() + "," + baseDN; } else { searchBase = baseDN; } logger.info("searchBase=====" + searchBase);
CASCredentialHelper
package org.jasig.cas.util; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import java.io.StringReader; import java.util.*; import org.jdom.*; import org.jdom.input.SAXBuilder; import org.jdom.xpath.*; import org.sky.cas.auth.LdapPersonInfoBean; import org.xml.sax.InputSource; public class CASCredentialHelper { public final static Log logger = LogFactory.getLog(CASCredentialHelper.class); public static LdapPersonInfoBean getPersoninfoFromCredential(String dnStr) { LdapPersonInfoBean person = new LdapPersonInfoBean(); logger.debug("credential str======" + dnStr); try { if (dnStr != null) { //创建一个新的字符串 String[] p_array = dnStr.split(","); if (p_array != null) { person.setCompanyid(p_array[1]); person.setUsername(p_array[0]); } } } catch (Exception e) { logger.error("get personinfo from DN: [:" + dnStr + "] error caused by: " + e.getMessage(), e); } return person; } public static void main(String[] args) throws Exception { StringBuffer sb = new StringBuffer(); sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>"); sb.append("<CASCredential>"); sb.append("<result>"); sb.append("<loginid>sys</loginid>"); sb.append("<companyid>401</companyid>"); sb.append("<email>aaa@a.net</email>"); sb.append("</result>"); sb.append("</CASCredential>"); getPersoninfoFromCredential(sb.toString()); } }
LdapPersonInfoBean
package org.sky.cas.auth; import java.io.Serializable; public class LdapPersonInfoBean implements Serializable { private String companyid = ""; private String username = ""; /** * @return the companyid */ public String getCompanyid() { return companyid; } /** * @param companyid the companyid to set */ public void setCompanyid(String companyid) { this.companyid = companyid; } /** * @return the username */ public String getUsername() { return username; } /** * @param username the username to set */ public void setUsername(String username) { this.username = username; } }
以上这两个类到底在干什么,大家不要急 ,我们接着看下面的这个CASCredentialsToPrincipalResolver类吧
CASCredentialsToPrincipalResolver类
该类的作用是这样的:
一个客户在CAS SSO登录界面登录了,然后输入了相关的登录信息,然后CAS SSO跳转到客户端的主界面中去,客户端在主界面通过以下语句:
AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal(); String userName = principal.getName();
即可以得到CAS SSO转发过来的合法登录了的用户名,可是,可是。。。CAS SSO默认只能带一个username过来给到客户端,而该成功登录了的用户的在LDAP中的其它属性是通过以下语句得到的:
Map attributes = principal.getAttributes(); String email = (String) attributes.get("email");
熊掌与鱼兼得法 ,既可以把用户在LDAP中其它属性带到客户端又可以把客户的登录信息也带到客户端
因此我们需要定制CASCredentialsToPrincipalResolver这个类,来看该类的代码:
package org.sky.cas.auth; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jasig.cas.authentication.principal.AbstractPersonDirectoryCredentialsToPrincipalResolver; import org.jasig.cas.authentication.principal.Credentials; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.output.Format; import org.jdom.output.XMLOutputter; public class CASCredentialsToPrincipalResolver extends AbstractPersonDirectoryCredentialsToPrincipalResolver { public final Log logger = LogFactory.getLog(this.getClass()); protected String extractPrincipalId(final Credentials credentials) { final CASCredential casCredential = (CASCredential) credentials; return buildCompCredential(casCredential.getUsername(), casCredential.getCompanyid()); } /** * Return true if Credentials are UsernamePasswordCredentials, false * otherwise. */ public boolean supports(final Credentials credentials) { return credentials != null && CASCredential.class.isAssignableFrom(credentials.getClass()); } public String buildCompCredential(String loginId, String companyId) { StringBuffer sb = new StringBuffer(); sb.append(loginId).append(","); sb.append(companyId); return sb.toString(); } }注意第23行和buildCompCredential方法,大家来看这个类原先是继承自AbstractPersonDirectoryCredentialsToPrincipalResolver 类对吧,如果我们不自定这个类,CAS SSO有一个默认的Resolver,你们知道CAS SSO默认的这个Resolver是怎么写的吗?
大家可以自己跟一下原码,在原码中,它是这样写的:
package org.sky.cas.auth; import org.apache.commons.httpclient.UsernamePasswordCredentials; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.jasig.cas.authentication.principal.AbstractPersonDirectoryCredentialsToPrincipalResolver; import org.jasig.cas.authentication.principal.Credentials; import java.io.ByteArrayOutputStream; import java.io.FileOutputStream; import java.io.IOException; import org.jdom.Document; import org.jdom.Element; import org.jdom.JDOMException; import org.jdom.output.Format; import org.jdom.output.XMLOutputter; public class CASCredentialsToPrincipalResolver extends AbstractPersonDirectoryCredentialsToPrincipalResolver { public final Log logger = LogFactory.getLog(this.getClass()); protected String extractPrincipalId(final Credentials credentials) { final CASCredential casCredential = (CASCredential) credentials; return casCredential.getUsername(); } /** * Return true if Credentials are UsernamePasswordCredentials, false * otherwise. */ }
看到了没有,它只返回了一个username,因此,我们把这个类扩展了一下,使得CAS SSO在登录成功后可以给客户端返回这样的一个字串:"username,companyid”。
通过这样的方法以使得当客户在登录时输入的那些并不属于LDAP库中存储的信息也能够被带到客户端中去,这样的话我们在客户端中如果通过以下这段代码:
AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal(); String userName = principal.getName();
String[] userAttri = userName.split(","); uinfo.setUserName(userAttri[0]); uinfo.setCompanyId(userAttri[1]);
最终版src/main/webapp/WEB-INF/deployerConfiguration.xml文件
有了attributeDao, 有了resolver,我们彻底来重新配置一下我们的deployerConfiguration.xml文件吧,来看下面的配置:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p" xmlns:tx="http://www.springframework.org/schema/tx" xmlns:sec="http://www.springframework.org/schema/security" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd"> <bean id="authenticationManager" class="org.jasig.cas.authentication.AuthenticationManagerImpl"> <property name="credentialsToPrincipalResolvers"> <list> <bean class="org.sky.cas.auth.CASCredentialsToPrincipalResolver"> <property name="attributeRepository" ref="attributeRepository" /> </bean> </list> </property> <property name="authenticationHandlers"> <list> <bean class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler" p:httpClient-ref="httpClient" /> <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler" p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org" p:contextSource-ref="contextSource" /> </list> </property> </bean> <!-- ldap datasource --> <bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource"> <property name="password" value="secret" /> <property name="pooled" value="true" /> <property name="url" value="ldap://localhost:389" /> <!--管理员 --> <property name="userDn" value="cn=Manager,dc=sky,dc=org" /> <property name="baseEnvironmentProperties"> <map> <!-- Three seconds is an eternity to users. --> <entry key="com.sun.jndi.ldap.connect.timeout" value="60" /> <entry key="com.sun.jndi.ldap.read.timeout" value="60" /> <entry key="java.naming.security.authentication" value="simple" /> </map> </property> </bean> <sec:user-service id="userDetailsService"> <sec:user name="@@THIS SHOULD BE REPLACED@@" password="notused" authorities="ROLE_ADMIN" /> </sec:user-service> <bean id="attributeRepository" class="org.jasig.services.persondir.support.ldap.CASLdapPersonAttributeDao"> <property name="contextSource" ref="contextSource" /> <property name="baseDN" value="o=company,dc=sky,dc=org" /> <property name="requireAllQueryAttributes" value="true" /> <property name="queryAttributeMapping"> <map> <entry key="username" value="uid" /> </map> </property> <property name="resultAttributeMapping"> <map> <entry key="uid" value="loginid" /> <entry key="mail" value="email" /> </map> </property> </bean> <bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl"> <property name="registeredServices"> <list> <bean class="org.jasig.cas.services.RegexRegisteredService"> <property name="id" value="0" /> <property name="name" value="HTTP and IMAP" /> <property name="description" value="Allows HTTP(S) and IMAP(S) protocols" /> <property name="serviceId" value="^(https?|imaps?)://.*" /> <property name="evaluationOrder" value="10000001" /> <property name="ignoreAttributes" value="false" /> <property name="allowedAttributes"> <list> <value>loginid</value> <value>email</value> </list> </property> </bean> </list> </property> </bean> <bean id="auditTrailManager" class="com.github.inspektr.audit.support.Slf4jLoggingAuditTrailManager" /> <bean id="healthCheckMonitor" class="org.jasig.cas.monitor.HealthCheckMonitor"> <property name="monitors"> <list> <bean class="org.jasig.cas.monitor.MemoryMonitor" p:freeMemoryWarnThreshold="10" /> <!-- NOTE The following ticket registries support SessionMonitor: * DefaultTicketRegistry * JpaTicketRegistry Remove this monitor if you use an unsupported registry. --> <bean class="org.jasig.cas.monitor.SessionMonitor" p:ticketRegistry-ref="ticketRegistry" p:serviceTicketCountWarnThreshold="5000" p:sessionCountWarnThreshold="100000" /> </list> </property> </bean> </beans>
在这个配置文件里,我们把attributeDao还有Resolver还有我们的Ldap认证时用的AuthenticationHandler都变成了我们自定义的类了,但还是有2段配置代码大家看起来有些疑惑,没关系,我们接着来分析接着来变态:
<bean id="attributeRepository" class="org.jasig.services.persondir.support.ldap.CASLdapPersonAttributeDao"> <property name="contextSource" ref="contextSource" /> <property name="baseDN" value="o=company,dc=sky,dc=org" /> <property name="requireAllQueryAttributes" value="true" /> <property name="queryAttributeMapping"> <map> <entry key="username" value="uid" /> </map> </property> <property name="resultAttributeMapping"> <map> <entry key="uid" value="loginid" /> <entry key="mail" value="email" /> </map> </property> </bean>
看到这边的resultAttributeMapping,它的意思就是:根据 上面的“queryAttributeMapping”的这个键值找到ldap中该条数据,然后通过resultAttributeMapping返回给客户端 ,这段配置做的就是这么一件事。
注:
- 一定要在queryAttributeMapping的entry key=后面写上"username”,这个username来自于我们cas sso登录主界面中的username这个属性。
- 在resultAttributeMapping中key为LDAP中相关数据的“主键”,value就是我们希望让客户端通过以下代码获取到CAS SSO服务端传过来的值的那个key,千万不要搞错了哦。
Map attributes = principal.getAttributes(); String email = (String) attributes.get("email");
当然,到了这边,我们的值还不能直接返回给客户端 !!!
如果能够直接返回,到此处为止,我们的变态就应该已经全结束了,可是CAS SSO有着其严格的定义,不是说你要返回什么值给客户端你就可以返回的,还需要一个“allowed”。
继续看下去:
<bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl"> <property name="registeredServices"> <list> <bean class="org.jasig.cas.services.RegexRegisteredService"> <property name="id" value="0" /> <property name="name" value="HTTP and IMAP" /> <property name="description" value="Allows HTTP(S) and IMAP(S) protocols" /> <property name="serviceId" value="^(https?|imaps?)://.*" /> <property name="evaluationOrder" value="10000001" /> <property name="ignoreAttributes" value="false" /> <property name="allowedAttributes"> <list> <value>loginid</value> <value>email</value> </list> </property> </bean> </list> </property> </bean>
看到这个地方了吗?
<property name="allowedAttributes"> <list> <value>loginid</value> <value>email</value> </list> </property>
这段XML配置的意思就是: 根据上面的“queryAttributeMapping”的这个键值找到ldap中该条数据,然后通过resultAttributeMapping返回给客户端,并且“允许“loginid”与"email“两个值可以通过客户端使用如下的的代码被允许访问得到:
Map attributes = principal.getAttributes(); String email = (String) attributes.get("email");
很烦? 不是,其实不烦,这是因为老外的框架做的严谨,而且扩展性好,只要通过extend, implement就可以实现我们自己的功能了,这种设计很强,或者说很变态,因为接下去还没完呢,哈哈,继续。
修改cas sso的主登录界面,把界面修改成如下风格
修改src/main/webapp/WEB-INF/view/jsp/default/ui/casLoginView.jsp
这个改页面,很简单,这个页面在:src/main/webapp/WEB-INF/view/default/ui/casLoginView.jsp
上手把这个页面的两个include去掉,如何去?如何增加以下这个下拉框:
<select id="companyid" name="companyid" > <option value="101" selected>上海煤气公司</option> <option value="102" selected>上海自来水厂</option> <option value="103" selected>FBI</option> <option value="104" selected>神盾局</option> </select>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <%@ page session="true"%> <%@ page pageEncoding="utf-8"%> <%@ page contentType="text/html; charset=utf-8"%> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%> <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%> <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%> <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%> <html> <head> <link href="${pageContext.request.contextPath}/css/login.css" rel="stylesheet" type="text/css" /> <link href="${pageContext.request.contextPath}/css/login_form.css" rel="stylesheet" type="text/css" /> <script language="javascript"> var relativePath="<%=request.getContextPath()%>"; </script> <title>CAS SSO登录</title> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> </head> <body id="cas"> <div style="text-align: center;"> </div> <form:form method="post" id="fm1" commandName="${commandName}" htmlEscape="true" style="height:300px"> <div class="login_div" id="login"> <table border="0" cellspacing="0" cellpadding="0"> <tr> <td colspan="2" style="border-bottom: 1px solid #e5e9ee;"><img src="${pageContext.request.contextPath}/css/images/login_dot.png" width="24" height="24" hspace="5" align="absbottom" />登录</td> </tr> <tr> <td width="175" class="label"> 用户名:</td> <td width="405"> <c:if test="${empty sessionScope.openIdLocalId}"> <spring:message code="screen.welcome.label.netid.accesskey" var="userNameAccessKey" /> <form:input onblur="refreshOrgList();" id="username" tabindex="1" accesskey="${userNameAccessKey}" path="username"/> </c:if> </td> </tr> <tr> <td class="label">密码:</td> <td><form:password cssClass="required" cssErrorClass="error" id="password" size="25" tabindex="2" path="password" accesskey="${passwordAccessKey}" autocomplete="off" /> </td> </tr> <tr> <td class="label">公司ID:</td> <td> <select id="companyid" name="companyid" > <option value="101" selected>上海煤气公司</option> <option value="102" selected>上海自来水厂</option> <option value="103" selected>FBI</option> <option value="104" selected>神盾局</option> </select> </td> </tr> <tr> <td class="label"></td> <td><font color="red"><form:errors id="msg" class="errors" /> </font></td> </tr> </table> </div> <div class="but_div"> <input type="hidden" name="lt" value="${loginTicket}" /> <input type="hidden" name="execution" value="${flowExecutionKey}" /> <input type="hidden" name="_eventId" value="submit" /> <input name="submit" accesskey="l" class="login_but" value="<spring:message code="screen.welcome.button.login" />" tabindex="4" type="submit" /> <input name="button2" type="reset" class="cancel_but" id="button2" value="取 消" /> </div> </form:form> <div class="loginbottom_div"> <div>Copyright © 红肠啃僵尸 reserved.</div> </div> </body>
修改src/main/webapp/WEB-INF/view/jsp/default/protocol/2.0/casServiceValidationSuccess.jsp
CAS SSO中这个jsp是用于在用户登录成功后把用户登录成功后的信息组成一个map传给客户端调用的,即客户端可以通过如下代码:
Map attributes = principal.getAttributes(); String email = (String) attributes.get("email");
<property name="resultAttributeMapping"> <map> <entry key="uid" value="loginid" /> <entry key="mail" value="email" /> </map> </property>
<!-- return more attributes from attributeRepository start --> <c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes) > 0}"> <cas:attributes> <c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}"> <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}> </c:forEach> </cas:attributes> </c:if> <!-- return more attributes from attributeRepository end -->
改完后的casServiceValidationSuccess.jsp完整代码如下,请注意<!-- return more attributes from attributeRepository start -->至<!-- return more attributes from attributeRepository end-->处的代码,这段代码就是我们新增的用于向客户端返回attributeDao中取出的所有的属性的遍历代码:
<%@ page session="false" %> <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %> <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'> <cas:authenticationSuccess> <cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)} </cas:user> <!-- return more attributes from attributeRepository start --> <c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes) > 0}"> <cas:attributes> <c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}"> <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}> </c:forEach> </cas:attributes> </c:if> <!-- return more attributes from attributeRepository end --> <c:if test="${not empty pgtIou}"> <cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket> </c:if> <c:if test="${fn:length(assertion.chainedAuthentications) > 1}"> <cas:proxies> <c:forEach var="proxy" items="${assertion.chainedAuthentications}" varStatus="loopStatus" begin="0" end="${fn:length(assertion.chainedAuthentications)-2}" step="1"> <cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy> </c:forEach> </cas:proxies> </c:if> </cas:authenticationSuccess> </cas:serviceResponse>
制作测试用客户端工程
在客户端我们会设置一个web session,把从cas-server带过来的用户ID,租户ID以及存在LDAP中的该客户的email都存储于这个session中。
为此,我们需要这样一个东西:即我们需要一个filter,用于在每次从CAS SSO登录成功后转到客户端时把相关的用户登录信息存储到web session中去。
当然,这些工作涉及到一系列的工具类,而且这些个工具类对于cas-sample-site1和cas-sample-site2具有同样的功能,出于代码可维护性以及统一性的考虑,这两个工程所使用到的这块代码功能都是相同的,因此我们来重组一下我们的客户端工程的目录结构吧。
myplatform工程
myplatform工程结构
该工程是cas-sample-site1和cas-sample-site2共用的一个工程,它的结构如下:
myplatform工程与CAS客户端工程cas-sample-site1和cas-sample-site2的依赖关系
两个客户端工程的依赖全部如上面图示所列那样去设置。
存储客户登录信息的UserSession
package org.sky.framework.session; import java.io.Serializable; public class UserSession implements Serializable { private String companyId = ""; private String userName = ""; private String userEmail = ""; public String getCompanyId() { return companyId; } public void setCompanyId(String companyId) { this.companyId = companyId; } public String getUserName() { return userName; } public void setUserName(String userName) { this.userName = userName; } public String getUserEmail() { return userEmail; } public void setUserEmail(String userEmail) { this.userEmail = userEmail; } }
AppSessionListener
package org.sky.framework.session; import javax.servlet.http.HttpSessionEvent; import javax.servlet.http.HttpSessionListener; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import javax.servlet.ServletContext; import javax.servlet.ServletRequestEvent; import javax.servlet.ServletRequestListener; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; public class AppSessionListener implements HttpSessionListener { protected Logger logger = LoggerFactory.getLogger(this.getClass()); @Override public void sessionCreated(HttpSessionEvent se) { HttpSession session = null; try { session = se.getSession(); // get value ServletContext context = session.getServletContext(); String timeoutValue = context.getInitParameter("sessionTimeout"); int timeout = Integer.valueOf(timeoutValue); // set value session.setMaxInactiveInterval(timeout); logger.info(">>>>>>session max inactive interval has been set to " + timeout + " seconds."); } catch (Exception ex) { ex.printStackTrace(); } } @Override public void sessionDestroyed(HttpSessionEvent arg0) { // TODO Auto-generated method stub } }
我们的filter SampleSSOSessionFilter
package org.sky.framework.session; import javax.servlet.Filter; import javax.servlet.FilterChain; import javax.servlet.FilterConfig; import javax.servlet.RequestDispatcher; import javax.servlet.ServletContext; import javax.servlet.ServletException; import javax.servlet.ServletRequest; import javax.servlet.ServletResponse; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import java.io.IOException; import java.io.PrintWriter; import java.util.Enumeration; import java.util.HashMap; import java.util.Map; import org.jasig.cas.client.authentication.AttributePrincipal; import org.jasig.cas.client.util.AssertionHolder; import org.jasig.cas.client.validation.Assertion; import org.sky.util.WebConstants; import org.slf4j.Logger; import org.slf4j.LoggerFactory; public class SampleSSOSessionFilter implements Filter { protected Logger logger = LoggerFactory.getLogger(this.getClass()); private String excluded; private static final String EXCLUDE = "exclude"; private boolean no_init = true; private ServletContext context = null; private FilterConfig config; String url = ""; String actionName = ""; public void setFilterConfig(FilterConfig paramFilterConfig) { if (this.no_init) { this.no_init = false; this.config = paramFilterConfig; if ((this.excluded = paramFilterConfig.getInitParameter("exclude")) != null) this.excluded += ","; } } private String getActionName(String actionPath) { logger.debug("filter actionPath====" + actionPath); StringBuffer actionName = new StringBuffer(); try { int begin = actionPath.lastIndexOf("/"); if (begin >= 0) { actionName.append(actionPath.substring(begin, actionPath.length())); } } catch (Exception e) { } return actionName.toString(); } private boolean excluded(String paramString) { // logger.info("paramString====" + paramString); // logger.info("excluded====" + this.excluded); // logger.info(this.excluded.indexOf(paramString + ",")); if ((paramString == null) || (this.excluded == null)) return false; return (this.excluded.indexOf(paramString + ",") >= 0); } @Override public void destroy() { // TODO Auto-generated method stub } @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain arg2) throws IOException, ServletException { HttpServletRequest req = (HttpServletRequest) request; HttpServletResponse resp = (HttpServletResponse) response; UserSession uinfo = new UserSession(); HttpSession se = req.getSession(); url = req.getRequestURI(); actionName = getActionName(url); //actionName = url; logger.debug(">>>>>>>>>>>>>>>>>>>>SampleSSOSessionFilter: request actionname" + actionName); if (!excluded(actionName)) { try { uinfo = (UserSession) se.getAttribute(WebConstants.USER_SESSION_OBJECT); AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal(); String userName = principal.getName(); logger.info("userName: " + userName); if (userName != null && userName.length() > 0 && uinfo == null) { Map attributes = principal.getAttributes(); String email = (String) attributes.get("email"); uinfo = new UserSession(); String[] userAttri = userName.split(","); uinfo.setUserName(userAttri[0]); uinfo.setCompanyId(userAttri[1]); uinfo.setUserEmail(email); se.setAttribute(WebConstants.USER_SESSION_OBJECT, uinfo); } } catch (Exception e) { logger.error("SampleSSOSessionFilter error:" + e.getMessage(), e); resp.sendRedirect(req.getContextPath() + "/syserror.jsp"); return; } } else { arg2.doFilter(request, response); return; } try { arg2.doFilter(request, response); return; } catch (Exception e) { logger.error("SampleSSOSessionFilter fault: " + e.getMessage(), e); } } @Override public void init(FilterConfig config) throws ServletException { // TODO Auto-generated method stub this.config = config; if ((this.excluded = config.getInitParameter("exclude")) != null) this.excluded += ","; this.no_init = false; } }
case-sample-site1和cas-sample-site2中的web.xml
我们对于这两个CAS客户端工程的web.xml文件所做出的修改如下
- 将原有的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>
因为我们为两个CAS客户端工程增加了一个syserror.jsp,以用于在获取CAS SERVER端出错时进行重定向用,而这个syserror.jsp是不需要经过什么登录、什么记录websession用的,所以,它必须是被“excluded”掉的,对吧,具体它是怎么实现的,大家可以自己跟一下代码。
主要是注意看SampleSSOSessionFilter中以下这段代码:
uinfo = (UserSession) se.getAttribute(WebConstants.USER_SESSION_OBJECT); AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal(); String userName = principal.getName(); logger.info("userName: " + userName); if (userName != null && userName.length() > 0 && uinfo == null) { Map attributes = principal.getAttributes(); String email = (String) attributes.get("email"); uinfo = new UserSession(); String[] userAttri = userName.split(","); uinfo.setUserName(userAttri[0]); uinfo.setCompanyId(userAttri[1]); uinfo.setUserEmail(email); se.setAttribute(WebConstants.USER_SESSION_OBJECT, uinfo); }
好了,我们现在要做的就是为cas-sample-site1/2各配上一个用于显示我们是否能够成功从cas-server端传过来登录成功后用户信息的index.jsp了。
cas-sample-site1/index.jsp
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ page import="org.sky.framework.session.UserSession, org.sky.util.WebConstants" %> <% UserSession us=(UserSession)session.getAttribute(WebConstants.USER_SESSION_OBJECT); String uname=us.getUserName(); String email=us.getUserEmail(); String companyId=us.getCompanyId(); %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>cas sample site1</title> </head> <body> <h1>cas sample site1 Hello: <%=uname%>(<%=email%>) u are@Company: <%=companyId%></h1> </p> <a href="http://localhost:8080/cas-sample-site2/index.jsp">cas-sample-site2</a> </br> <a href="http://localhost:8080/cas-server/logout">退出</a> </body> </html>
cas-sample-site2/index.jsp
<%@ page language="java" contentType="text/html; charset=utf-8" pageEncoding="utf-8"%> <%@ page import="org.sky.framework.session.UserSession, org.sky.util.WebConstants" %> <% UserSession us=(UserSession)session.getAttribute(WebConstants.USER_SESSION_OBJECT); String uname=us.getUserName(); String email=us.getUserEmail(); String companyId=us.getCompanyId(); %> <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd"> <html> <head> <meta http-equiv="Content-Type" content="text/html; charset=utf-8"> <title>cas sample site2</title> </head> <body> <h1>cas sample site2 Hello: <%=uname%>(<%=email%>) u are@Company: <%=companyId%></h1> <a href="http://localhost:8080/cas-sample-site1/index.jsp">cas-sample-site1</a> </br> <a href="http://localhost:8080/cas-server/logout">退出</a> </body> </html>
运行今天所有的例子
测试用ldap中所有用户的ldif代码:
dn: dc=sky,dc=org dc: sky objectClass: top objectClass: domain dn: o=company,dc=sky,dc=org objectClass: organization o: company dn: ou=members,o=company,dc=sky,dc=org objectClass: organizationalUnit ou: members dn: cn=user1,ou=members,o=company,dc=sky,dc=org sn: user1 cn: user1 userPassword: aaaaaa objectClass: organizationalPerson dn: cn=user2,ou=members,o=company,dc=sky,dc=org sn: user2 cn: user2 userPassword: abcdefg objectClass: organizationalPerson dn: uid=mk,ou=members,o=company,dc=sky,dc=org objectClass: posixAccount objectClass: top objectClass: inetOrgPerson gidNumber: 0 givenName: Yuan sn: MingKai displayName: YuanMingKai uid: mk homeDirectory: e:\user mail: mk.yuan@nttdata.com cn: YuanMingKai uidNumber: 13599 userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs= dn: o=101,o=company,dc=sky,dc=org o: 101 objectClass: organization dn: o=102,o=company,dc=sky,dc=org o: 102 objectClass: organization dn: o=103,o=company,dc=sky,dc=org o: 103 objectClass: organization dn: o=104,o=company,dc=sky,dc=org o: 104 objectClass: organization dn: uid=marious,o=101,o=company,dc=sky,dc=org objectClass: posixAccount objectClass: top objectClass: inetOrgPerson gidNumber: 0 givenName: Wang sn: LiMing displayName: WangLiMing uid: marious homeDirectory: d:\ cn: WangLiMing uidNumber: 47967 userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs= mail: aaa@a.net dn: uid=sky,o=101,o=company,dc=sky,dc=org objectClass: posixAccount objectClass: top objectClass: inetOrgPerson gidNumber: 0 givenName: Yuan sn: Tao displayName: YuanTao uid: sky homeDirectory: d:\ cn: YuanTao uidNumber: 26422 userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs= mail: bbb@b.net dn: uid=jason,o=102,o=company,dc=sky,dc=org objectClass: posixAccount objectClass: top objectClass: inetOrgPerson gidNumber: 0 givenName: zhang sn: lei displayName: zhanglei uid: jason homeDirectory: d:\ cn: zhanglei uidNumber: 62360 userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs= mail: jason@abc.net dn: uid=andy.li,o=103,o=company,dc=sky,dc=org objectClass: posixAccount objectClass: top objectClass: inetOrgPerson gidNumber: 0 givenName: Li sn: Jun displayName: LiJun uid: andy.li homeDirectory: d:\ cn: LiJun uidNumber: 51204 userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs= mail: andy.li@jesus.chris dn: uid=pitt,o=104,o=company,dc=sky,dc=org objectClass: posixAccount objectClass: top objectClass: inetOrgPerson gidNumber: 0 givenName: Brad sn: Pitt displayName: Brad Pitt uid: pitt homeDirectory: d:\ cn: Brad Pitt uidNumber: 64650 userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs= mail: pitt@hollywood.com
把:
- cas-server
- cas-sample-site1
- cas-sample-site2
全部在eclipse里用jboss7运行起来:
打开一个IE,输入http://localhost:8080/cas-sample-site1,出现如下界面:
- 我们在用户名处输入jason
- 密码输入aaaaaa
- 公司ID选择成“上海自来水厂”
点击【登录】按钮,此时页面显示如下:
点击cas-sample-site2这个链接,页面显示如下:
我们看来看jason这个人在我们的ldap中的相关信息:
再来看看“上海自来水厂”的companyid是什么:
<select id="companyid" name="companyid" > <option value="101" selected>上海煤气公司</option> <option value="102" selected>上海自来水厂</option> <option value="103" selected>FBI</option> <option value="104" selected>神盾局</option> </select>是102,说明我们的传值传对了。
我们现在再用debug模式来调试一下这个用例。
好了,结束今天的课程。
在今天的课程中我们完成了几件事,这几件事中尤其是对于多租户的CAS SSO的解决方案是目前网上没有的包括国外的网站和主力论坛上(或者说有人解决了没有公布出来):
- 自定义CAS SSO登录界面
- 在CAS SSO登录界面增加我们自定义的登录用元素
- 使用LDAP带出登录用户在LDAP内存储的其它更多的信息
- 实现了CAS SSO支持多租户登录的功能
虽然这一过程很痛苦,很变态,但是通过这样的一个案例,我们完成了一件了不起的事情,同时对于这种国外开源软件的customization我们也有了一个认识,就是欧美的一些软件,它的自定义都是通过扩展、继承、插件式的方式来实现的,这说明他们的软件在设计之初就考虑到了这些扩展。
那很多人就会来问,改了这么一堆东西,我怎么知道要改这些东西,要去动这些代码或者有些代码怎么写?
我回答这个问题的方式很简单,我把它称之为:play with it。
因为开源的软件都提供源码的,你把源码都导入eclipse工程,想办法运行起来,这个过程可能折腾个1-2周吧,但是源码一旦跑起来了,你就可以自己去跟代码啦,然后看人家这块逻辑这块设计是怎么实现的,然后照着写或者按照人家的规范插入自己的一部分的自定义的代码,就这样一点点,一点点的你也就可以把本属于别人一个产品变成为自己的一套东西了,这个过程就叫play with it。
做IT的一定要多play with it,要不然,你很难有自己的感性上的认识,没有了感性认识的基础,那也就谈不上什么“理性认识”和“升华”了,呵呵!
在我们今后的教程中,我们动手改代码或者集成其它开源产品的机会还有很多、很多。。。。。。甚至还会涉及到JDK里的一些东西,让我们一起慢慢来吧。