cas-sample-site1/2各配上一個用於顯示咱們是否可以成功後用戶信息的index.jsp

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

 

  • taobao.com/company_101/張飛
  • taobao.com/company_102/張飛
  • taobao.com/company_103/趙雲

看張飛這個名字,看!!!css

 

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

 

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

 

 
  1. <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"git

  2. p:filter="uid=%u" p:searchBase="o=101,o=company,dc=sky,dc=org"github

  3. p:contextSource-ref="contextSource" />web

  4.  
  5. <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"spring

  6. p:filter="uid=%u" p:searchBase="o=102,o=company,dc=sky,dc=org"express

  7. p:contextSource-ref="contextSource" />apache

  8.  
  9. <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"

  10. p:filter="uid=%u" p:searchBase="o=103,o=company,dc=sky,dc=org"

  11. p:contextSource-ref="contextSource" />


但是,咱們想一想:

 

  1. 咱們的租戶在咱們的後臺系統中是自動「開戶」的,companyid是一個自動增長的,我從companyid_101如今增長到了companyid_110時,你是否是每次用戶一開戶,你就要去手動改這個CAS SSO中的配置文件呢?
  2. 若是你不嫌煩,好好好,你夠狠,你就手工改吧!可是當你每次在配置文件中新增一條配置語句時,你的CAS SSO是否是要斷服務重啓啊?那你還怎麼作到24*7的這種不間斷服務啊疑問

通常來講,咱們的開戶是用程序自動寫入LDAP中去的,即LDAP中的company_101, company_102, company_103是由程序自動生成的,那咱們的程序就須要可以讓用戶在登陸後臺B2B系統時自動能夠根據用戶選擇的租戶來爲用戶正確登陸的這麼一種自動識別功能,就比如下面這樣的一個登陸界面:

 

 

看到這個界面了嗎?

 

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

 

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

看到上面這張圖了吧,這就是我說的「多租戶」的概念,你們應該記得咱們在CAS SSO次日中怎麼去拿CAS SSO綁定LDAP中的一條UserDN而後去搜索的吧?

 

 
  1. <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"

  2. p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org"

  3. p:contextSource-ref="contextSource" />

對吧!!!

 

 

如今咱們要作到的就是:

p:searchBase="xxx.xxx.xx"

這條要作成動態的,好比說:

 

 

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

 

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

 

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

 

  • 一種是直接經過CAS的登陸界面而後在輸入用戶名時要求用戶以這種形式「uid=sky,o=101"去輸入它的用戶名,這種作法先不去說會形成用戶登陸時的困擾,並且CAS SSO的登陸界面也不支持這樣格式的用戶名輸入。
  • 一種就是很笨的在CAS SSO的配置文件中綁定多個p:searchBase,這個方法已經被咱們否掉了。

所以,筆者在這邊要提的將是首創的能夠作到全動態的去根據用戶名,密碼和所該用戶所屬組織自動在後臺建立p:searchBase的最完美的解決方案,下面咱們就開始吧。

 

建立工程

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

 

 

look,今天咱們把這個cas-server放到了eclipse工程中去了,而後在eclipse裏隨改隨測試,如今咱們就來說述如何建立這個工程以使得cas server能夠運行在咱們的eclipse的工程中。

所以咱們在eclipse中新建一個java工程-是java工程你可千萬不要建成j2ee工程啊,而後按照上圖創建相應的目錄。

 

CAS SERVER工程的組建

導入全部的配置文件

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

 

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

 

把這個目錄下全部的內容,除去如下2個目錄:

  • org
  • META-INF

外全部的東西通通拷貝入eclipse中的cas-server工程中的src/main/resources目錄下

 

構建WEB-INF目錄

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

 

 

 

構建cas-server基本源碼

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

 

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

 

並在eclipse工程中作以下設置

 

此處須要注意的是咱們把:

  • src/main/java
  • src/main/resources

這兩個目錄作成編譯路徑,而src/main/webapp不做爲編譯路徑

 

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

 

 

構建webapp目錄

將咱們在第一天、次日中佈署在tomcat中的case-server中如下這些目錄

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

 

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

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

 

  1. META-INF文件內的persistence.xml中報HSQLDialect錯誤
  2. 報log4jConfiguration.xml文件在啓動時找不到的錯誤

下面咱們來看如何修正這兩個小BUG。

 

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

這是原始的/META-INF/persistence.xml文件的內容:

 
  1. <class>org.jasig.cas.services.AbstractRegisteredService</class>

  2. <class>org.jasig.cas.services.RegexRegisteredService</class>

  3. <class>org.jasig.cas.services.RegisteredServiceImpl</class>

  4. <class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class>

  5. <class>org.jasig.cas.ticket.ServiceTicketImpl</class>

  6. <class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class>


咱們在文件最後加入如下配置代碼

 
  1. <class>org.jasig.cas.services.AbstractRegisteredService</class>

  2. <class>org.jasig.cas.services.RegexRegisteredService</class>

  3. <class>org.jasig.cas.services.RegisteredServiceImpl</class>

  4. <class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class>

  5. <class>org.jasig.cas.ticket.ServiceTicketImpl</class>

  6. <class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class>

  7. <properties>

  8. <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />

  9. </properties>

這是完整的改完後的persistence.xml文件的內容:

 
  1. <persistence xmlns="http://java.sun.com/xml/ns/persistence"

  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  3. xsi:schemaLocation="http://java.sun.com/xml/ns/persistence http://java.sun.com/xml/ns/persistence/persistence_2_0.xsd"

  4. version="2.0">

  5.  
  6. <persistence-unit name="CasPersistence" transaction-type="RESOURCE_LOCAL">

  7. <class>org.jasig.cas.services.AbstractRegisteredService</class>

  8. <class>org.jasig.cas.services.RegexRegisteredService</class>

  9. <class>org.jasig.cas.services.RegisteredServiceImpl</class>

  10. <class>org.jasig.cas.ticket.TicketGrantingTicketImpl</class>

  11. <class>org.jasig.cas.ticket.ServiceTicketImpl</class>

  12. <class>org.jasig.cas.ticket.registry.support.JpaLockingStrategy$Lock</class>

  13. <properties>

  14. <property name="hibernate.dialect" value="org.hibernate.dialect.HSQLDialect" />

  15. </properties>

  16. </persistence-unit>

  17. </persistence>

 

改完後請保存。

 

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

找到eclipse的cas-server工程中WEB-INF/spring-configuration/log4jConfiguration.xml文件,將這段內容註釋掉

 
  1. <bean id="log4jInitialization" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">

  2. <property name="targetClass" value="org.springframework.util.Log4jConfigurer"/>

  3. <property name="targetMethod" value="initLogging"/>

  4. <property name="arguments">

  5. <list>

  6. <value>${log4j.config.location:classpath:log4j.xml}</value>

  7. <value>${log4j.refresh.interval:60000}</value>

  8. </list>

  9. </property>

  10. </bean>

 

整個log4jConfiguration.xml文件修改後是這個樣子的:

 
  1. <beans xmlns="http://www.springframework.org/schema/beans"

  2. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

  3. xmlns:p="http://www.springframework.org/schema/p"

  4. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd">

  5.  
  6.  
  7. <!--

  8. <bean id="log4jInitialization" class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">

  9. <property name="targetClass" value="org.springframework.util.Log4jConfigurer"/>

  10. <property name="targetMethod" value="initLogging"/>

  11. <property name="arguments">

  12. <list>

  13. <value>${log4j.config.location:classpath:log4j.xml}</value>

  14. <value>${log4j.refresh.interval:60000}</value>

  15. </list>

  16. </property>

  17. </bean>

  18. -->

  19. </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類

 
  1. public class CASCredential extends RememberMeUsernamePasswordCredentials {

  2. private static final long serialVersionUID = 1L;

  3.  
  4. private Map<String, Object> param;

  5.  
  6.  
  7. private String companyid;

  8.  
  9. /**

  10. * @return the companyid

  11. */

  12. public String getCompanyid() {

  13. return companyid;

  14. }

  15.  
  16. /**

  17. * @param companyid the companyid to set

  18. */

  19. public void setCompanyid(String companyid) {

  20. this.companyid = companyid;

  21. }

  22.  
  23. public Map<String, Object> getParam() {

  24. return param;

  25. }

  26.  
  27. public void setParam(Map<String, Object> param) {

  28. this.param = param;

  29. }

  30. }


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

 

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

修改src/main/webapp/WEB-INF/login-webflow.xml文件,找到如下這段:

 
  1. <view-state id="viewLoginForm" view="casLoginView" model="credentials">

  2. <binder>

  3. <binding property="username" />

  4. <binding property="password" />

  5. </binder>


將其改爲:

 
  1. <view-state id="viewLoginForm" view="casLoginView" model="credentials">

  2. <binder>

  3. <binding property="username" />

  4. <binding property="password" />

  5. <binding property="companyid"/>

  6. </binder>

 

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

新建一個類CASAuthenticationViaFormAction,內容以下:

 
  1. package org.sky.cas.auth;

  2.  
  3. import java.util.ArrayList;

  4. import java.util.HashMap;

  5. import java.util.List;

  6. import java.util.Map;

  7.  
  8. import javax.servlet.http.HttpServletRequest;

  9. import javax.servlet.http.HttpServletResponse;

  10. import javax.validation.constraints.NotNull;

  11.  
  12. import org.jasig.cas.CentralAuthenticationService;

  13. import org.jasig.cas.authentication.handler.AuthenticationException;

  14. import org.jasig.cas.authentication.principal.Credentials;

  15. import org.jasig.cas.authentication.principal.Service;

  16. import org.jasig.cas.ticket.TicketException;

  17. import org.jasig.cas.web.bind.CredentialsBinder;

  18. import org.jasig.cas.web.support.WebUtils;

  19. import org.slf4j.Logger;

  20. import org.slf4j.LoggerFactory;

  21. import org.springframework.binding.message.MessageBuilder;

  22. import org.springframework.binding.message.MessageContext;

  23. import org.springframework.util.StringUtils;

  24. import org.springframework.web.util.CookieGenerator;

  25. import org.springframework.webflow.core.collection.MutableAttributeMap;

  26. import org.springframework.webflow.execution.RequestContext;

  27.  
  28. @SuppressWarnings("deprecation")

  29. public class CASAuthenticationViaFormAction {

  30. /**

  31. * Binder that allows additional binding of form object beyond Spring

  32. * defaults.

  33. */

  34. private CredentialsBinder credentialsBinder;

  35.  
  36. /** Core we delegate to for handling all ticket related tasks. */

  37. @NotNull

  38. private CentralAuthenticationService centralAuthenticationService;

  39.  
  40. @NotNull

  41. private CookieGenerator warnCookieGenerator;

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

  44.  
  45. public final void doBind(final RequestContext context, final Credentials credentials) throws Exception {

  46. final HttpServletRequest request = WebUtils.getHttpServletRequest(context);

  47.  
  48. if (this.credentialsBinder != null && this.credentialsBinder.supports(credentials.getClass())) {

  49. this.credentialsBinder.bind(request, credentials);

  50. }

  51. }

  52.  
  53. public final String submit(final RequestContext context, final Credentials credentials, final MessageContext messageContext)

  54. throws Exception {

  55. String companyid = "";

  56. // Validate login ticket

  57. final String authoritativeLoginTicket = WebUtils.getLoginTicketFromFlowScope(context);

  58. final String providedLoginTicket = WebUtils.getLoginTicketFromRequest(context);

  59. if (credentials instanceof CASCredential) {

  60. String companyCode = "compnayid";

  61. CASCredential rmupc = (CASCredential) credentials;

  62. companyid = rmupc.getCompanyid();

  63.  
  64. }

  65.  
  66. if (!authoritativeLoginTicket.equals(providedLoginTicket)) {

  67. this.logger.warn("Invalid login ticket " + providedLoginTicket);

  68. final String code = "INVALID_TICKET";

  69. messageContext.addMessage(new MessageBuilder().error().code(code).arg(providedLoginTicket).defaultText(code).build());

  70. return "error";

  71. }

  72.  
  73. final String ticketGrantingTicketId = WebUtils.getTicketGrantingTicketId(context);

  74. final Service service = WebUtils.getService(context);

  75. if (StringUtils.hasText(context.getRequestParameters().get("renew")) && ticketGrantingTicketId != null && service != null) {

  76.  
  77. try {

  78. final String serviceTicketId = this.centralAuthenticationService.grantServiceTicket(ticketGrantingTicketId,

  79. service, credentials);

  80. WebUtils.putServiceTicketInRequestScope(context, serviceTicketId);

  81. putWarnCookieIfRequestParameterPresent(context);

  82. return "warn";

  83. } catch (final TicketException e) {

  84. if (isCauseAuthenticationException(e)) {

  85. populateErrorsInstance(e, messageContext);

  86. return getAuthenticationExceptionEventId(e);

  87. }

  88.  
  89. this.centralAuthenticationService.destroyTicketGrantingTicket(ticketGrantingTicketId);

  90. if (logger.isDebugEnabled()) {

  91. logger.debug("Attempted to generate a ServiceTicket using renew=true with different credentials", e);

  92. }

  93. }

  94. }

  95.  
  96. try {

  97.  
  98. CASCredential rmupc = (CASCredential) credentials;

  99. WebUtils.putTicketGrantingTicketInRequestScope(context,

  100. centralAuthenticationService.createTicketGrantingTicket(rmupc));

  101. putWarnCookieIfRequestParameterPresent(context);

  102.  
  103. return "success";

  104. } catch (final TicketException e) {

  105. populateErrorsInstance(e, messageContext);

  106. if (isCauseAuthenticationException(e))

  107. return getAuthenticationExceptionEventId(e);

  108. return "error";

  109. }

  110. }

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

  113.  
  114. try {

  115. messageContext.addMessage(new MessageBuilder().error().code(e.getCode()).defaultText(e.getCode()).build());

  116. } catch (final Exception fe) {

  117. logger.error(fe.getMessage(), fe);

  118. }

  119. }

  120.  
  121. private void putWarnCookieIfRequestParameterPresent(final RequestContext context) {

  122. final HttpServletResponse response = WebUtils.getHttpServletResponse(context);

  123.  
  124. if (StringUtils.hasText(context.getExternalContext().getRequestParameterMap().get("warn"))) {

  125. this.warnCookieGenerator.addCookie(response, "true");

  126. } else {

  127. this.warnCookieGenerator.removeCookie(response);

  128. }

  129. }

  130.  
  131. private AuthenticationException getAuthenticationExceptionAsCause(final TicketException e) {

  132. return (AuthenticationException) e.getCause();

  133. }

  134.  
  135. private String getAuthenticationExceptionEventId(final TicketException e) {

  136. final AuthenticationException authEx = getAuthenticationExceptionAsCause(e);

  137.  
  138. if (this.logger.isDebugEnabled())

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

  140.  
  141. return authEx.getType();

  142. }

  143.  
  144. private boolean isCauseAuthenticationException(final TicketException e) {

  145. return e.getCause() != null && AuthenticationException.class.isAssignableFrom(e.getCause().getClass());

  146. }

  147.  
  148. public final void setCentralAuthenticationService(final CentralAuthenticationService centralAuthenticationService) {

  149. this.centralAuthenticationService = centralAuthenticationService;

  150. }

  151.  
  152. /**

  153. * Set a CredentialsBinder for additional binding of the HttpServletRequest

  154. * to the Credentials instance, beyond our default binding of the

  155. * Credentials as a Form Object in Spring WebMVC parlance. By the time we

  156. * invoke this CredentialsBinder, we have already engaged in default binding

  157. * such that for each HttpServletRequest parameter, if there was a JavaBean

  158. * property of the Credentials implementation of the same name, we have set

  159. * that property to be the value of the corresponding request parameter.

  160. * This CredentialsBinder plugin point exists to allow consideration of

  161. * things other than HttpServletRequest parameters in populating the

  162. * Credentials (or more sophisticated consideration of the

  163. * HttpServletRequest parameters).

  164. *

  165. * @param credentialsBinder the credentials binder to set.

  166. */

  167. public final void setCredentialsBinder(final CredentialsBinder credentialsBinder) {

  168. this.credentialsBinder = credentialsBinder;

  169. }

  170.  
  171. public final void setWarnCookieGenerator(final CookieGenerator warnCookieGenerator) {

  172. this.warnCookieGenerator = warnCookieGenerator;

  173. }

  174. }


這個類很簡單,主要是第59行到第64行的:

 
  1. if (credentials instanceof CASCredential) {

  2. String companyCode = "compnayid";

  3. CASCredential rmupc = (CASCredential) credentials;

  4. companyid = rmupc.getCompanyid();

  5.  
  6. }


以及第98行到第100行的:

 
  1. CASCredential rmupc = (CASCredential) credentials;

  2. WebUtils.putTicketGrantingTicketInRequestScope(context,

  3. 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

找到如下這行:

 
  1. <bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"

  2. p:centralAuthenticationService-ref="centralAuthenticationService"

  3. p:warnCookieGenerator-ref="warnCookieGenerator"/>

把它註釋掉改爲:

 
  1. <!-- <bean id="authenticationViaFormAction" class="org.jasig.cas.web.flow.AuthenticationViaFormAction"

  2. p:centralAuthenticationService-ref="centralAuthenticationService"

  3. p:warnCookieGenerator-ref="warnCookieGenerator"/>

  4. -->

  5.  
  6. <bean id="authenticationViaFormAction"

  7. class="org.sky.cas.auth.CASAuthenticationViaFormAction"

  8. p:centralAuthenticationService-ref="centralAuthenticationService"

  9. p:warnCookieGenerator-ref="warnCookieGenerator" />


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

增長p:searchBase使得CAS SSO的LDAP能夠根據不一樣的companyid動態搜索用戶的功能

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

 

 
  1. package org.sky.cas.auth;

  2.  
  3. import org.jasig.cas.adaptors.ldap.AbstractLdapUsernamePasswordAuthenticationHandler;

  4. import org.jasig.cas.authentication.handler.AuthenticationException;

  5. import org.jasig.cas.authentication.principal.UsernamePasswordCredentials;

  6. import org.jasig.cas.util.LdapUtils;

  7. import org.springframework.ldap.NamingSecurityException;

  8. import org.springframework.ldap.core.ContextSource;

  9. import org.springframework.ldap.core.LdapTemplate;

  10. import org.springframework.ldap.core.NameClassPairCallbackHandler;

  11. import org.springframework.ldap.core.SearchExecutor;

  12.  
  13. import javax.naming.NameClassPair;

  14. import javax.naming.NamingEnumeration;

  15. import javax.naming.NamingException;

  16. import javax.naming.directory.DirContext;

  17. import javax.naming.directory.SearchControls;

  18. import javax.validation.constraints.Max;

  19. import javax.validation.constraints.Min;

  20. import java.util.ArrayList;

  21. import java.util.List;

  22.  
  23. public class CASLDAPAuthenticationHandler extends AbstractLdapUsernamePasswordAuthenticationHandler {

  24. /** The default maximum number of results to return. */

  25. private static final int DEFAULT_MAX_NUMBER_OF_RESULTS = 1000;

  26.  
  27. /** The default timeout. */

  28. private static final int DEFAULT_TIMEOUT = 1000;

  29.  
  30. /** The search base to find the user under. */

  31. private String searchBase;

  32.  
  33. /** The scope. */

  34. @Min(0)

  35. @Max(2)

  36. private int scope = SearchControls.ONELEVEL_SCOPE;

  37.  
  38. /** The maximum number of results to return. */

  39. private int maxNumberResults = DEFAULT_MAX_NUMBER_OF_RESULTS;

  40.  
  41. /** The amount of time to wait. */

  42. private int timeout = DEFAULT_TIMEOUT;

  43.  
  44. /** Boolean of whether multiple accounts are allowed. */

  45. private boolean allowMultipleAccounts;

  46.  
  47. protected final boolean authenticateUsernamePasswordInternal(final UsernamePasswordCredentials credentials)

  48. throws AuthenticationException {

  49. CASCredential rmupc = (CASCredential) credentials;

  50. final String companyid = rmupc.getCompanyid();

  51. final List<String> cns = new ArrayList<String>();

  52.  
  53. final SearchControls searchControls = getSearchControls();

  54.  
  55. final String transformedUsername = getPrincipalNameTransformer().transform(credentials.getUsername());

  56. final String filter = LdapUtils.getFilterWithValues(getFilter(), transformedUsername);

  57. try {

  58. this.getLdapTemplate().search(new SearchExecutor() {

  59. public NamingEnumeration executeSearch(final DirContext context) throws NamingException {

  60. String baseDN = "";

  61. if (companyid != null && companyid.trim().length() > 0) {

  62. baseDN = "o=" + companyid + "," + searchBase;

  63. } else {

  64. baseDN = searchBase;

  65. }

  66. //System.out.println("searchBase=====" + baseDN);

  67. return context.search(baseDN, filter, searchControls);

  68. }

  69. }, new NameClassPairCallbackHandler() {

  70.  
  71. public void handleNameClassPair(final NameClassPair nameClassPair) {

  72. cns.add(nameClassPair.getNameInNamespace());

  73. }

  74. });

  75. } catch (Exception e) {

  76. log.error("search ldap error casue: " + e.getMessage(), e);

  77. return false;

  78. }

  79. if (cns.isEmpty()) {

  80. log.debug("Search for " + filter + " returned 0 results.");

  81. return false;

  82. }

  83. if (cns.size() > 1 && !this.allowMultipleAccounts) {

  84. log.warn("Search for " + filter + " returned multiple results, which is not allowed.");

  85. return false;

  86. }

  87.  
  88. for (final String dn : cns) {

  89. DirContext test = null;

  90. String finalDn = composeCompleteDnToCheck(dn, credentials);

  91. try {

  92. this.log.debug("Performing LDAP bind with credential: " + dn);

  93. test = this.getContextSource().getContext(finalDn, getPasswordEncoder().encode(credentials.getPassword()));

  94.  
  95. if (test != null) {

  96. return true;

  97. }

  98. } catch (final NamingSecurityException e) {

  99. log.debug("Failed to authenticate user {} with error {}", credentials.getUsername(), e.getMessage());

  100. return false;

  101. } catch (final Exception e) {

  102. this.log.error(e.getMessage(), e);

  103. return false;

  104. } finally {

  105. LdapUtils.closeContext(test);

  106. }

  107. }

  108.  
  109. return false;

  110. }

  111.  
  112. protected String composeCompleteDnToCheck(final String dn, final UsernamePasswordCredentials credentials) {

  113. return dn;

  114. }

  115.  
  116. private SearchControls getSearchControls() {

  117. final SearchControls constraints = new SearchControls();

  118. constraints.setSearchScope(this.scope);

  119. constraints.setReturningAttributes(new String[0]);

  120. constraints.setTimeLimit(this.timeout);

  121. constraints.setCountLimit(this.maxNumberResults);

  122.  
  123. return constraints;

  124. }

  125.  
  126. /**

  127. * Method to return whether multiple accounts are allowed.

  128. * @return true if multiple accounts are allowed, false otherwise.

  129. */

  130. protected boolean isAllowMultipleAccounts() {

  131. return this.allowMultipleAccounts;

  132. }

  133.  
  134. /**

  135. * Method to return the max number of results allowed.

  136. * @return the maximum number of results.

  137. */

  138. protected int getMaxNumberResults() {

  139. return this.maxNumberResults;

  140. }

  141.  
  142. /**

  143. * Method to return the scope.

  144. * @return the scope

  145. */

  146. protected int getScope() {

  147. return this.scope;

  148. }

  149.  
  150. /**

  151. * Method to return the search base.

  152. * @return the search base.

  153. */

  154. protected String getSearchBase() {

  155. return this.searchBase;

  156. }

  157.  
  158. /**

  159. * Method to return the timeout.

  160. * @return the timeout.

  161. */

  162. protected int getTimeout() {

  163. return this.timeout;

  164. }

  165.  
  166. public final void setScope(final int scope) {

  167. this.scope = scope;

  168. }

  169.  
  170. /**

  171. * @param allowMultipleAccounts The allowMultipleAccounts to set.

  172. */

  173. public void setAllowMultipleAccounts(final boolean allowMultipleAccounts) {

  174. this.allowMultipleAccounts = allowMultipleAccounts;

  175. }

  176.  
  177. /**

  178. * @param maxNumberResults The maxNumberResults to set.

  179. */

  180. public final void setMaxNumberResults(final int maxNumberResults) {

  181. this.maxNumberResults = maxNumberResults;

  182. }

  183.  
  184. /**

  185. * @param searchBase The searchBase to set.

  186. */

  187. public final void setSearchBase(final String searchBase) {

  188. this.searchBase = searchBase;

  189. }

  190.  
  191. /**

  192. * @param timeout The timeout to set.

  193. */

  194. public final void setTimeout(final int timeout) {

  195. this.timeout = timeout;

  196. }

  197.  
  198. /**

  199. * Sets the context source for LDAP searches. This method may be used to

  200. * support use cases like the following:

  201. * <ul>

  202. * <li>Pooling of LDAP connections used for searching (e.g. via instance

  203. * of {@link org.springframework.ldap.pool.factory.PoolingContextSource}).</li>

  204. * <li>Searching with client certificate credentials.</li>

  205. * </ul>

  206. * <p>

  207. * If this is not defined, the context source defined by

  208. * {@link #setContextSource(ContextSource)} is used.

  209. *

  210. * @param contextSource LDAP context source.

  211. */

  212. public final void setSearchContextSource(final ContextSource contextSource) {

  213. setLdapTemplate(new LdapTemplate(contextSource));

  214. }

  215.  
  216. }


這個類的做用就是給src/main/webapp/WEB-INF/deployerConfiguration.xml中如下這段用的:

 
  1. <property name="authenticationHandlers">

  2. <list>

  3.  
  4. <bean

  5. class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"

  6. p:httpClient-ref="httpClient" />

  7.  
  8. <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"

  9. p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org"

  10. p:contextSource-ref="contextSource" />

  11. </list>

  12. </property>


請注意代碼50行處:

final String companyid = rmupc.getCompanyid();


以及59行到69行處:

 
  1. public NamingEnumeration executeSearch(final DirContext context) throws NamingException {

  2. String baseDN = "";

  3. if (companyid != null && companyid.trim().length() > 0) {

  4. baseDN = "o=" + companyid + "," + searchBase;

  5. } else {

  6. baseDN = searchBase;

  7. }

  8. //System.out.println("searchBase=====" + baseDN);

  9. return context.search(baseDN, filter, searchControls);

  10. }

  11. }, 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中的寫法 ,已經被我修改掉了

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

  2.  
  3.  
  4. <property name="credentialsToPrincipalResolvers">

  5. <list>

  6. <bean class="org.sky.cas.auth.CASCredentialsToPrincipalResolver">

  7. <property name="attributeRepository" ref="attributeRepository" />

  8. </bean>

  9. </list>

  10. </property>

  11.  
  12.  
  13. <property name="authenticationHandlers">

  14. <list>

  15.  
  16. <bean

  17. class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"

  18. p:httpClient-ref="httpClient" />

  19.  
  20. <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"

  21. p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org"

  22. p:contextSource-ref="contextSource" />

  23. </list>

  24. </property>

  25. </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的所有源碼。

 

 
  1. /**

  2. * Licensed to Jasig under one or more contributor license

  3. * agreements. See the NOTICE file distributed with this work

  4. * for additional information regarding copyright ownership.

  5. * Jasig licenses this file to you under the Apache License,

  6. * Version 2.0 (the "License"); you may not use this file

  7. * except in compliance with the License. You may obtain a

  8. * copy of the License at:

  9. *

  10. * http://www.apache.org/licenses/LICENSE-2.0

  11. *

  12. * Unless required by applicable law or agreed to in writing,

  13. * software distributed under the License is distributed on

  14. * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY

  15. * KIND, either express or implied. See the License for the

  16. * specific language governing permissions and limitations

  17. * under the License.

  18. */

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

  21.  
  22. import java.util.ArrayList;

  23. import java.util.List;

  24. import java.util.Map;

  25. import java.util.Set;

  26. import java.util.regex.Matcher;

  27. import java.util.regex.Pattern;

  28.  
  29. import javax.naming.directory.SearchControls;

  30.  
  31. import org.apache.commons.lang.StringUtils;

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

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

  34. import org.jasig.cas.util.CASCredentialHelper;

  35. import org.jasig.services.persondir.IPersonAttributes;

  36. import org.jasig.services.persondir.support.AbstractQueryPersonAttributeDao;

  37. import org.jasig.services.persondir.support.CaseInsensitiveAttributeNamedPersonImpl;

  38. import org.jasig.services.persondir.support.CaseInsensitiveNamedPersonImpl;

  39. import org.jasig.services.persondir.support.QueryType;

  40. import org.sky.cas.auth.LdapPersonInfoBean;

  41. import org.springframework.beans.factory.BeanCreationException;

  42. import org.springframework.beans.factory.InitializingBean;

  43. import org.springframework.ldap.core.AttributesMapper;

  44. import org.springframework.ldap.core.ContextSource;

  45. import org.springframework.ldap.core.LdapTemplate;

  46. import org.springframework.ldap.filter.EqualsFilter;

  47. import org.springframework.ldap.filter.Filter;

  48. import org.springframework.ldap.filter.LikeFilter;

  49. import org.springframework.util.Assert;

  50.  
  51. /**

  52. * LDAP implementation of {@link org.jasig.services.persondir.IPersonAttributeDao}.

  53. *

  54. * In the case of multi valued attributes a {@link java.util.List} is set as the value.

  55. *

  56. * <br>

  57. * <br>

  58. * Configuration:

  59. * <table border="1">

  60. * <tr>

  61. * <th align="left">Property</th>

  62. * <th align="left">Description</th>

  63. * <th align="left">Required</th>

  64. * <th align="left">Default</th>

  65. * </tr>

  66. * <tr>

  67. * <td align="right" valign="top">searchControls</td>

  68. * <td>

  69. * Set the {@link SearchControls} used for executing the LDAP query.

  70. * </td>

  71. * <td valign="top">No</td>

  72. * <td valign="top">Default instance with SUBTREE scope.</td>

  73. * </tr>

  74. * <tr>

  75. * <td align="right" valign="top">baseDN</td>

  76. * <td>

  77. * The base DistinguishedName to use when executing the query filter.

  78. * </td>

  79. * <td valign="top">No</td>

  80. * <td valign="top">""</td>

  81. * </tr>

  82. * <tr>

  83. * <td align="right" valign="top">contextSource</td>

  84. * <td>

  85. * A {@link ContextSource} from the Spring-LDAP framework. Provides a DataSource

  86. * style object that this DAO can retrieve LDAP connections from.

  87. * </td>

  88. * <td valign="top">Yes</td>

  89. * <td valign="top">null</td>

  90. * </tr>

  91. * <tr>

  92. * <td align="right" valign="top">setReturningAttributes</td>

  93. * <td>

  94. * If the ldap attributes set in the ldapAttributesToPortalAttributes Map should be copied

  95. * into the {@link SearchControls#setReturningAttributes(String[])}. Setting this helps reduce

  96. * wire traffic of ldap queries.

  97. * </td>

  98. * <td valign="top">No</td>

  99. * <td valign="top">true</td>

  100. * </tr>

  101. * <tr>

  102. * <td align="right" valign="top">queryType</td>

  103. * <td>

  104. * How multiple attributes in a query should be concatenated together. The other option is OR.

  105. * </td>

  106. * <td valign="top">No</td>

  107. * <td valign="top">AND</td>

  108. * </tr>

  109. * </table>

  110. *

  111. * @author andrew.petro@yale.edu

  112. * @author Eric Dalquist

  113. * @version $Revision$ $Date$

  114. * @since uPortal 2.5

  115. */

  116. public class CASLdapPersonAttributeDao extends AbstractQueryPersonAttributeDao<LogicalFilterWrapper> implements InitializingBean {

  117. private static final Pattern QUERY_PLACEHOLDER = Pattern.compile("\\{0\\}");

  118. private final static AttributesMapper MAPPER = new AttributeMapAttributesMapper();

  119. protected final Log logger = LogFactory.getLog(getClass());

  120. /**

  121. * The LdapTemplate to use to execute queries on the DirContext

  122. */

  123. private LdapTemplate ldapTemplate = null;

  124.  
  125. private String baseDN = "";

  126. private String queryTemplate = null;

  127. private ContextSource contextSource = null;

  128. private SearchControls searchControls = new SearchControls();

  129. private boolean setReturningAttributes = true;

  130. private QueryType queryType = QueryType.AND;

  131.  
  132. public CASLdapPersonAttributeDao() {

  133. this.searchControls.setSearchScope(SearchControls.SUBTREE_SCOPE);

  134. this.searchControls.setReturningObjFlag(false);

  135. }

  136.  
  137. /* (non-Javadoc)

  138. * @see org.springframework.beans.factory.InitializingBean#afterPropertiesSet()

  139. */

  140. public void afterPropertiesSet() throws Exception {

  141. final Map<String, Set<String>> resultAttributeMapping = this.getResultAttributeMapping();

  142. if (this.setReturningAttributes && resultAttributeMapping != null) {

  143. this.searchControls.setReturningAttributes(resultAttributeMapping.keySet().toArray(

  144. new String[resultAttributeMapping.size()]));

  145. }

  146.  
  147. if (this.contextSource == null) {

  148. throw new BeanCreationException("contextSource must be set");

  149. }

  150. }

  151.  
  152. /* (non-Javadoc)

  153. * @see org.jasig.services.persondir.support.AbstractQueryPersonAttributeDao#appendAttributeToQuery(java.lang.Object, java.lang.String, java.util.List)

  154. */

  155. @Override

  156. protected LogicalFilterWrapper appendAttributeToQuery(LogicalFilterWrapper queryBuilder, String dataAttribute,

  157. List<Object> queryValues) {

  158. if (queryBuilder == null) {

  159. queryBuilder = new LogicalFilterWrapper(this.queryType);

  160. }

  161.  
  162. for (final Object queryValue : queryValues) {

  163. String queryValueString = queryValue == null ? null : queryValue.toString();

  164.  
  165. LdapPersonInfoBean person = new LdapPersonInfoBean();

  166. //person = CASCredentialHelper.getPersoninfoFromCredential(queryValueString);

  167. //queryValueString = person.getUsername();

  168. person = CASCredentialHelper.getPersoninfoFromCredential(queryValueString);

  169. queryValueString=person.getUsername();

  170. if (StringUtils.isNotBlank(queryValueString)) {

  171. final Filter filter;

  172. if (!queryValueString.contains("*")) {

  173. filter = new EqualsFilter(dataAttribute, queryValueString);

  174. } else {

  175. filter = new LikeFilter(dataAttribute, queryValueString);

  176. }

  177. queryBuilder.append(filter);

  178. }

  179. }

  180.  
  181. return queryBuilder;

  182. }

  183.  
  184. /* (non-Javadoc)

  185. * @see org.jasig.services.persondir.support.AbstractQueryPersonAttributeDao#getPeopleForQuery(java.lang.Object, java.lang.String)

  186. */

  187.  
  188. @Override

  189. protected List<IPersonAttributes> getPeopleForQuery(LogicalFilterWrapper queryBuilder, String queryUserName) {

  190. LdapPersonInfoBean ldapPerson = new LdapPersonInfoBean();

  191. ldapPerson = CASCredentialHelper.getPersoninfoFromCredential(queryUserName);

  192. final String generatedLdapQuery = queryBuilder.encode();

  193. //If no query is generated return null since the query cannot be run

  194. if (StringUtils.isBlank(generatedLdapQuery)) {

  195. return null;

  196. }

  197.  
  198. //Insert the generated query into the template if it is configured

  199. final String ldapQuery;

  200. if (this.queryTemplate == null) {

  201. ldapQuery = generatedLdapQuery;

  202. } else {

  203. final Matcher queryMatcher = QUERY_PLACEHOLDER.matcher(this.queryTemplate);

  204. ldapQuery = queryMatcher.replaceAll(generatedLdapQuery);

  205. }

  206. String searchBase = "";

  207. if (ldapPerson.getCompanyid().trim().length() > 0) {

  208. searchBase = "o=" + ldapPerson.getCompanyid() + "," + baseDN;

  209. } else {

  210. searchBase = baseDN;

  211. }

  212. logger.info("searchBase=====" + searchBase);

  213. //Execute the query

  214. List<Map<String, List<Object>>> queryResults = new ArrayList<Map<String, List<Object>>>();

  215. try {

  216. queryResults = this.ldapTemplate.search(searchBase, ldapQuery, this.searchControls, MAPPER);

  217. } catch (Exception e) {

  218. logger.error(

  219. "search ldap with [searchBase===" + searchBase + "] [ldapQuery====" + ldapQuery + "], caused by: "

  220. + e.getMessage(), e);

  221. }

  222. final List<IPersonAttributes> peopleAttributes = new ArrayList<IPersonAttributes>(queryResults.size());

  223. for (final Map<String, List<Object>> queryResult : queryResults) {

  224. IPersonAttributes person;

  225. //if (ldapPerson.getUsername() != null) {

  226. if (queryUserName != null && queryUserName.trim().length() > 0) {

  227. //person = new CaseInsensitiveNamedPersonImpl(ldapPerson.getUsername(), queryResult);

  228. person = new CaseInsensitiveNamedPersonImpl(queryUserName, queryResult);

  229. } else {

  230. //Create the IPersonAttributes doing a best-guess at a userName attribute

  231. String userNameAttribute = this.getConfiguredUserNameAttribute();

  232. person = new CaseInsensitiveAttributeNamedPersonImpl(userNameAttribute, queryResult);

  233. }

  234.  
  235. peopleAttributes.add(person);

  236. }

  237.  
  238. return peopleAttributes;

  239. }

  240.  
  241. /**

  242. * @see javax.naming.directory.SearchControls#getTimeLimit()

  243. * @deprecated Set the property on the {@link SearchControls} and set that via {@link #setSearchControls(SearchControls)}

  244. */

  245. @Deprecated

  246. public int getTimeLimit() {

  247. return this.searchControls.getTimeLimit();

  248. }

  249.  
  250. /**

  251. * @see javax.naming.directory.SearchControls#setTimeLimit(int)

  252. * @deprecated

  253. */

  254. @Deprecated

  255. public void setTimeLimit(int ms) {

  256. this.searchControls.setTimeLimit(ms);

  257. }

  258.  
  259. /**

  260. * @return The base distinguished name to use for queries.

  261. */

  262. public String getBaseDN() {

  263. return this.baseDN;

  264. }

  265.  
  266. /**

  267. * @param baseDN The base distinguished name to use for queries.

  268. */

  269. public void setBaseDN(String baseDN) {

  270. if (baseDN == null) {

  271. baseDN = "";

  272. }

  273.  
  274. this.baseDN = baseDN;

  275. }

  276.  
  277. /**

  278. * @return The ContextSource to get DirContext objects for queries from.

  279. */

  280. public ContextSource getContextSource() {

  281. return this.contextSource;

  282. }

  283.  
  284. /**

  285. * @param contextSource The ContextSource to get DirContext objects for queries from.

  286. */

  287. public synchronized void setContextSource(final ContextSource contextSource) {

  288. Assert.notNull(contextSource, "contextSource can not be null");

  289. this.contextSource = contextSource;

  290. this.ldapTemplate = new LdapTemplate(this.contextSource);

  291. }

  292.  
  293. /**

  294. * Sets the LdapTemplate, and thus the ContextSource (implicitly).

  295. *

  296. * @param ldapTemplate the LdapTemplate to query the LDAP server from. CANNOT be NULL.

  297. */

  298. public synchronized void setLdapTemplate(final LdapTemplate ldapTemplate) {

  299. Assert.notNull(ldapTemplate, "ldapTemplate cannot be null");

  300. this.ldapTemplate = ldapTemplate;

  301. this.contextSource = this.ldapTemplate.getContextSource();

  302. }

  303.  
  304. /**

  305. * @return Search controls to use for LDAP queries

  306. */

  307. public SearchControls getSearchControls() {

  308. return this.searchControls;

  309. }

  310.  
  311. /**

  312. * @param searchControls Search controls to use for LDAP queries

  313. */

  314. public void setSearchControls(SearchControls searchControls) {

  315. Assert.notNull(searchControls, "searchControls can not be null");

  316. this.searchControls = searchControls;

  317. }

  318.  
  319. /**

  320. * @return the queryType

  321. */

  322. public QueryType getQueryType() {

  323. return queryType;

  324. }

  325.  
  326. /**

  327. * Type of logical operator to use when joining WHERE clause components

  328. *

  329. * @param queryType the queryType to set

  330. */

  331. public void setQueryType(QueryType queryType) {

  332. this.queryType = queryType;

  333. }

  334.  
  335. public String getQueryTemplate() {

  336. return this.queryTemplate;

  337. }

  338.  
  339. /**

  340. * Optional wrapper template for the generated part of the query. Use {0} as a placeholder for where the generated query should be inserted.

  341. */

  342. public void setQueryTemplate(String queryTemplate) {

  343. this.queryTemplate = queryTemplate;

  344. }

  345. }

 

注意206到211行處的寫法

 
  1. String searchBase = "";

  2. if (ldapPerson.getCompanyid().trim().length() > 0) {

  3. searchBase = "o=" + ldapPerson.getCompanyid() + "," + baseDN;

  4. } else {

  5. searchBase = baseDN;

  6. }

  7. logger.info("searchBase=====" + searchBase);


 

 

CASLdapPersonAttributeDao類中須要使用到另外兩個咱們自定義的工具類代碼以下:

 

CASCredentialHelper

 
  1. package org.jasig.cas.util;

  2.  
  3. import org.apache.commons.logging.Log;

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

  5.  
  6. import java.io.StringReader;

  7. import java.util.*;

  8. import org.jdom.*;

  9. import org.jdom.input.SAXBuilder;

  10. import org.jdom.xpath.*;

  11. import org.sky.cas.auth.LdapPersonInfoBean;

  12. import org.xml.sax.InputSource;

  13.  
  14. public class CASCredentialHelper {

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

  16.  
  17. public static LdapPersonInfoBean getPersoninfoFromCredential(String dnStr) {

  18. LdapPersonInfoBean person = new LdapPersonInfoBean();

  19. logger.debug("credential str======" + dnStr);

  20. try {

  21. if (dnStr != null) {

  22. //建立一個新的字符串

  23. String[] p_array = dnStr.split(",");

  24. if (p_array != null) {

  25. person.setCompanyid(p_array[1]);

  26. person.setUsername(p_array[0]);

  27. }

  28. }

  29. } catch (Exception e) {

  30. logger.error("get personinfo from DN: [:" + dnStr + "] error caused by: " + e.getMessage(), e);

  31. }

  32. return person;

  33. }

  34.  
  35. public static void main(String[] args) throws Exception {

  36. StringBuffer sb = new StringBuffer();

  37. sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>");

  38. sb.append("<CASCredential>");

  39. sb.append("<result>");

  40. sb.append("<loginid>sys</loginid>");

  41. sb.append("<companyid>401</companyid>");

  42. sb.append("<email>aaa@a.net</email>");

  43. sb.append("</result>");

  44. sb.append("</CASCredential>");

  45. getPersoninfoFromCredential(sb.toString());

  46. }

  47. }


 

 

LdapPersonInfoBean

 
  1. package org.sky.cas.auth;

  2.  
  3. import java.io.Serializable;

  4.  
  5. public class LdapPersonInfoBean implements Serializable {

  6.  
  7. private String companyid = "";

  8. private String username = "";

  9.  
  10. /**

  11. * @return the companyid

  12. */

  13. public String getCompanyid() {

  14. return companyid;

  15. }

  16.  
  17. /**

  18. * @param companyid the companyid to set

  19. */

  20. public void setCompanyid(String companyid) {

  21. this.companyid = companyid;

  22. }

  23.  
  24. /**

  25. * @return the username

  26. */

  27. public String getUsername() {

  28. return username;

  29. }

  30.  
  31. /**

  32. * @param username the username to set

  33. */

  34. public void setUsername(String username) {

  35. this.username = username;

  36. }

  37. }


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

CASCredentialsToPrincipalResolver類

該類的做用是這樣的:

 

一個客戶在CAS SSO登陸界面登陸了,而後輸入了相關的登陸信息,而後CAS SSO跳轉到客戶端的主界面中去,客戶端在主界面經過如下語句:

 
  1. AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal();

  2. String userName = principal.getName();


便可以獲得CAS SSO轉發過來的合法登陸了的用戶名,但是,但是。。。CAS SSO默認只能帶一個username過來給到客戶端,而該成功登陸了的用戶的在LDAP中的其它屬性是經過如下語句獲得的:

 
  1. Map attributes = principal.getAttributes();

  2. String email = (String) attributes.get("email");

 

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

 

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

所以咱們須要定製CASCredentialsToPrincipalResolver這個類,來看該類的代碼:

 
  1. package org.sky.cas.auth;

  2.  
  3. import org.apache.commons.httpclient.UsernamePasswordCredentials;

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

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

  6. import org.jasig.cas.authentication.principal.AbstractPersonDirectoryCredentialsToPrincipalResolver;

  7. import org.jasig.cas.authentication.principal.Credentials;

  8.  
  9. import java.io.ByteArrayOutputStream;

  10. import java.io.FileOutputStream;

  11. import java.io.IOException;

  12. import org.jdom.Document;

  13. import org.jdom.Element;

  14. import org.jdom.JDOMException;

  15. import org.jdom.output.Format;

  16. import org.jdom.output.XMLOutputter;

  17.  
  18. public class CASCredentialsToPrincipalResolver extends AbstractPersonDirectoryCredentialsToPrincipalResolver {

  19. public final Log logger = LogFactory.getLog(this.getClass());

  20.  
  21. protected String extractPrincipalId(final Credentials credentials) {

  22. final CASCredential casCredential = (CASCredential) credentials;

  23. return buildCompCredential(casCredential.getUsername(), casCredential.getCompanyid());

  24. }

  25.  
  26. /**

  27. * Return true if Credentials are UsernamePasswordCredentials, false

  28. * otherwise.

  29. */

  30. public boolean supports(final Credentials credentials) {

  31. return credentials != null && CASCredential.class.isAssignableFrom(credentials.getClass());

  32. }

  33.  
  34. public String buildCompCredential(String loginId, String companyId) {

  35. StringBuffer sb = new StringBuffer();

  36. sb.append(loginId).append(",");

  37. sb.append(companyId);

  38. return sb.toString();

  39. }

  40. }

注意第23行和buildCompCredential方法,你們來看這個類原先是繼承自AbstractPersonDirectoryCredentialsToPrincipalResolver 類對吧,若是咱們不自定這個類,CAS SSO有一個默認的Resolver,大家知道CAS SSO默認的這個Resolver是怎麼寫的嗎?

 

你們能夠本身跟一下原碼,在原碼中,它是這樣寫的:

 
  1. package org.sky.cas.auth;

  2.  
  3. import org.apache.commons.httpclient.UsernamePasswordCredentials;

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

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

  6. import org.jasig.cas.authentication.principal.AbstractPersonDirectoryCredentialsToPrincipalResolver;

  7. import org.jasig.cas.authentication.principal.Credentials;

  8.  
  9. import java.io.ByteArrayOutputStream;

  10. import java.io.FileOutputStream;

  11. import java.io.IOException;

  12. import org.jdom.Document;

  13. import org.jdom.Element;

  14. import org.jdom.JDOMException;

  15. import org.jdom.output.Format;

  16. import org.jdom.output.XMLOutputter;

  17.  
  18. public class CASCredentialsToPrincipalResolver extends AbstractPersonDirectoryCredentialsToPrincipalResolver {

  19. public final Log logger = LogFactory.getLog(this.getClass());

  20.  
  21. protected String extractPrincipalId(final Credentials credentials) {

  22. final CASCredential casCredential = (CASCredential) credentials;

  23. return casCredential.getUsername();

  24. }

  25.  
  26. /**

  27. * Return true if Credentials are UsernamePasswordCredentials, false

  28. * otherwise.

  29. */

  30. }


看到了沒有,它只返回了一個username,所以,咱們把這個類擴展了一下,使得CAS SSO在登陸成功後能夠給客戶端返回這樣的一個字串:"username,companyid」。

 

經過這樣的方法以使得當客戶在登陸時輸入的那些並不屬於LDAP庫中存儲的信息也可以被帶到客戶端中去,這樣的話咱們在客戶端中若是經過如下這段代碼:

 
  1. AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal();

  2. String userName = principal.getName();

 

去試圖獲取從CAS SSO中帶來的登陸信息時,客戶端將會獲得一個這樣的字串「username,companyid」,所以咱們只要再在客戶端作一次簡單的切割,便可將咱們須要的登陸信息進行剝離了,以下例子:

 

 
  1. String[] userAttri = userName.split(",");

  2. uinfo.setUserName(userAttri[0]);

  3. uinfo.setCompanyId(userAttri[1]);

 

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

有了attributeDao, 有了resolver,咱們完全來從新配置一下咱們的deployerConfiguration.xml文件吧,來看下面的配置:

 

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

  2.  
  3.  
  4. <beans xmlns="http://www.springframework.org/schema/beans"

  5. xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:p="http://www.springframework.org/schema/p"

  6. xmlns:tx="http://www.springframework.org/schema/tx" xmlns:sec="http://www.springframework.org/schema/security"

  7. xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd

  8. http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.1.xsd

  9. http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security-3.1.xsd">

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

  12.  
  13.  
  14. <property name="credentialsToPrincipalResolvers">

  15. <list>

  16. <bean class="org.sky.cas.auth.CASCredentialsToPrincipalResolver">

  17. <property name="attributeRepository" ref="attributeRepository" />

  18. </bean>

  19. </list>

  20. </property>

  21.  
  22.  
  23. <property name="authenticationHandlers">

  24. <list>

  25.  
  26. <bean

  27. class="org.jasig.cas.authentication.handler.support.HttpBasedServiceCredentialsAuthenticationHandler"

  28. p:httpClient-ref="httpClient" />

  29.  
  30. <bean class=" org.sky.cas.auth.CASLDAPAuthenticationHandler"

  31. p:filter="uid=%u" p:searchBase="o=company,dc=sky,dc=org"

  32. p:contextSource-ref="contextSource" />

  33. </list>

  34. </property>

  35. </bean>

  36. <!-- ldap datasource -->

  37. <bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource">

  38. <property name="password" value="secret" />

  39. <property name="pooled" value="true" />

  40. <property name="url" value="ldap://localhost:389" />

  41.  
  42. <!--管理員 -->

  43. <property name="userDn" value="cn=Manager,dc=sky,dc=org" />

  44. <property name="baseEnvironmentProperties">

  45. <map>

  46. <!-- Three seconds is an eternity to users. -->

  47. <entry key="com.sun.jndi.ldap.connect.timeout" value="60" />

  48. <entry key="com.sun.jndi.ldap.read.timeout" value="60" />

  49. <entry key="java.naming.security.authentication" value="simple" />

  50. </map>

  51. </property>

  52. </bean>

  53.  
  54.  
  55. <sec:user-service id="userDetailsService">

  56. <sec:user name="@@THIS SHOULD BE REPLACED@@" password="notused"

  57. authorities="ROLE_ADMIN" />

  58. </sec:user-service>

  59.  
  60.  
  61. <bean id="attributeRepository"

  62. class="org.jasig.services.persondir.support.ldap.CASLdapPersonAttributeDao">

  63. <property name="contextSource" ref="contextSource" />

  64. <property name="baseDN" value="o=company,dc=sky,dc=org" />

  65. <property name="requireAllQueryAttributes" value="true" />

  66. <property name="queryAttributeMapping">

  67. <map>

  68. <entry key="username" value="uid" />

  69. </map>

  70. </property>

  71. <property name="resultAttributeMapping">

  72. <map>

  73. <entry key="uid" value="loginid" />

  74. <entry key="mail" value="email" />

  75. </map>

  76. </property>

  77. </bean>

  78.  
  79. <bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">

  80.  
  81. <property name="registeredServices">

  82. <list>

  83. <bean class="org.jasig.cas.services.RegexRegisteredService">

  84. <property name="id" value="0" />

  85. <property name="name" value="HTTP and IMAP" />

  86. <property name="description" value="Allows HTTP(S) and IMAP(S) protocols" />

  87. <property name="serviceId" value="^(https?|imaps?)://.*" />

  88. <property name="evaluationOrder" value="10000001" />

  89. <property name="ignoreAttributes" value="false" />

  90. <property name="allowedAttributes">

  91. <list>

  92. <value>loginid</value>

  93. <value>email</value>

  94. </list>

  95. </property>

  96. </bean>

  97.  
  98. </list>

  99. </property>

  100. </bean>

  101.  
  102. <bean id="auditTrailManager"

  103. class="com.github.inspektr.audit.support.Slf4jLoggingAuditTrailManager" />

  104.  
  105. <bean id="healthCheckMonitor" class="org.jasig.cas.monitor.HealthCheckMonitor">

  106. <property name="monitors">

  107. <list>

  108. <bean class="org.jasig.cas.monitor.MemoryMonitor"

  109. p:freeMemoryWarnThreshold="10" />

  110. <!-- NOTE The following ticket registries support SessionMonitor: * DefaultTicketRegistry

  111. * JpaTicketRegistry Remove this monitor if you use an unsupported registry. -->

  112. <bean class="org.jasig.cas.monitor.SessionMonitor"

  113. p:ticketRegistry-ref="ticketRegistry"

  114. p:serviceTicketCountWarnThreshold="5000"

  115. p:sessionCountWarnThreshold="100000" />

  116. </list>

  117. </property>

  118. </bean>

  119. </beans>


在這個配置文件裏,咱們把attributeDao還有Resolver還有咱們的Ldap認證時用的AuthenticationHandler都變成了咱們自定義的類了,但仍是有2段配置代碼你們看起來有些疑惑,不要緊,咱們接着來分析接着來變態:

 
  1. <bean id="attributeRepository"

  2. class="org.jasig.services.persondir.support.ldap.CASLdapPersonAttributeDao">

  3. <property name="contextSource" ref="contextSource" />

  4. <property name="baseDN" value="o=company,dc=sky,dc=org" />

  5. <property name="requireAllQueryAttributes" value="true" />

  6. <property name="queryAttributeMapping">

  7. <map>

  8. <entry key="username" value="uid" />

  9. </map>

  10. </property>

  11. <property name="resultAttributeMapping">

  12. <map>

  13. <entry key="uid" value="loginid" />

  14. <entry key="mail" value="email" />

  15. </map>

  16. </property>

  17. </bean>


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

 

注:

 

  • 必定要在queryAttributeMapping的entry key=後面寫上"username」,這個username來自於咱們cas sso登陸主界面中的username這個屬性。
  • 在resultAttributeMapping中key爲LDAP中相關數據的「主鍵」,value就是咱們但願讓客戶端經過如下代碼獲取到CAS SSO服務端傳過來的值的那個key,千萬不要搞錯了哦。

 

 
  1. Map attributes = principal.getAttributes();

  2. String email = (String) attributes.get("email");

 

 

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

 

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

 

繼續看下去:

 

 
  1. <bean id="serviceRegistryDao" class="org.jasig.cas.services.InMemoryServiceRegistryDaoImpl">

  2.  
  3. <property name="registeredServices">

  4. <list>

  5. <bean class="org.jasig.cas.services.RegexRegisteredService">

  6. <property name="id" value="0" />

  7. <property name="name" value="HTTP and IMAP" />

  8. <property name="description" value="Allows HTTP(S) and IMAP(S) protocols" />

  9. <property name="serviceId" value="^(https?|imaps?)://.*" />

  10. <property name="evaluationOrder" value="10000001" />

  11. <property name="ignoreAttributes" value="false" />

  12. <property name="allowedAttributes">

  13. <list>

  14. <value>loginid</value>

  15. <value>email</value>

  16. </list>

  17. </property>

  18. </bean>

  19.  
  20. </list>

  21. </property>

  22. </bean>


看到這個地方了嗎?

 
  1. <property name="allowedAttributes">

  2. <list>

  3. <value>loginid</value>

  4. <value>email</value>

  5. </list>

  6. </property>


這段XML配置的意思就是: 根據上面的「queryAttributeMapping」的這個鍵值找到ldap中該條數據,而後經過resultAttributeMapping返回給客戶端,而且「容許「loginid」與"email「兩個值能夠經過客戶端使用以下的的代碼被容許訪問獲得:

 

 
  1. Map attributes = principal.getAttributes();

  2. 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去掉,如何去?如何增長如下這個下拉框:

 

 
  1. <select id="companyid" name="companyid" >

  2. <option value="101" selected>上海煤氣公司</option>

  3. <option value="102" selected>上海自來水廠</option>

  4. <option value="103" selected>FBI</option>

  5. <option value="104" selected>神盾局</option>

  6. </select>

 

我在這邊就不細說了,這屬於copy & paste的工做,我在此就直接給出我本身製做完成後的casLoginView.jsp頁面內全部的源碼吧:

 
  1. <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">

  2. <%@ page session="true"%>

  3. <%@ page pageEncoding="utf-8"%>

  4. <%@ page contentType="text/html; charset=utf-8"%>

  5. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

  6. <%@ taglib prefix="spring" uri="http://www.springframework.org/tags"%>

  7. <%@ taglib prefix="form" uri="http://www.springframework.org/tags/form"%>

  8. <%@ taglib prefix="fn" uri="http://java.sun.com/jsp/jstl/functions"%>

  9. <html>

  10. <head>

  11.  
  12. <link href="${pageContext.request.contextPath}/css/login.css"

  13. rel="stylesheet" type="text/css" />

  14. <link href="${pageContext.request.contextPath}/css/login_form.css"

  15. rel="stylesheet" type="text/css" />

  16.  
  17. <script language="javascript">

  18. var relativePath="<%=request.getContextPath()%>";

  19. </script>

  20. <title>CAS SSO登陸</title>

  21. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

  22. </head>

  23. <body id="cas">

  24. <div style="text-align: center;">

  25.  
  26. </div>

  27. <form:form method="post" id="fm1" commandName="${commandName}"

  28. htmlEscape="true" style="height:300px">

  29. <div class="login_div" id="login">

  30. <table border="0" cellspacing="0" cellpadding="0">

  31. <tr>

  32. <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>

  33. </tr>

  34. <tr>

  35. <td width="175" class="label"> 用戶名:</td>

  36. <td width="405">

  37. <c:if test="${empty sessionScope.openIdLocalId}">

  38. <spring:message code="screen.welcome.label.netid.accesskey"

  39. var="userNameAccessKey" />

  40. <form:input onblur="refreshOrgList();" id="username"

  41. tabindex="1" accesskey="${userNameAccessKey}" path="username"/>

  42. </c:if>

  43. </td>

  44. </tr>

  45. <tr>

  46. <td class="label">密碼:</td>

  47. <td><form:password cssClass="required" cssErrorClass="error"

  48. id="password" size="25" tabindex="2" path="password"

  49. accesskey="${passwordAccessKey}" autocomplete="off" />

  50. </td>

  51. </tr>

  52.  
  53. <tr>

  54. <td class="label">公司ID:</td>

  55. <td>

  56. <select id="companyid" name="companyid" >

  57. <option value="101" selected>上海煤氣公司</option>

  58. <option value="102" selected>上海自來水廠</option>

  59. <option value="103" selected>FBI</option>

  60. <option value="104" selected>神盾局</option>

  61. </select>

  62. </td>

  63. </tr>

  64. <tr>

  65. <td class="label"></td>

  66. <td><font color="red"><form:errors id="msg" class="errors" /> </font></td>

  67. </tr>

  68. </table>

  69. </div>

  70. <div class="but_div">

  71. <input type="hidden" name="lt" value="${loginTicket}" />

  72. <input type="hidden" name="execution" value="${flowExecutionKey}" />

  73. <input type="hidden" name="_eventId" value="submit" />

  74. <input name="submit" accesskey="l" class="login_but" value="<spring:message code="screen.welcome.button.login" />"

  75. tabindex="4" type="submit" />

  76. <input name="button2" type="reset" class="cancel_but" id="button2" value="取 消" />

  77.  
  78. </div>

  79. </form:form>

  80. <div class="loginbottom_div">

  81. <div>Copyright  &copy; 紅腸啃殭屍 reserved.</div>

  82. </div>

  83. </body>

 

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

 

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

CAS SSO中這個jsp是用於在用戶登陸成功後把用戶登陸成功後的信息組成一個map傳給客戶端調用的,即客戶端能夠經過以下代碼:

 
  1. Map attributes = principal.getAttributes();

  2. String email = (String) attributes.get("email");

 

它是經過CAS SSO服務端的attributeDao來取得相關的LDAP中的其他信息的,可是它默認只帶username到客戶端 ,所以爲了讓客戶端可以取得如下這些額外的信息:

 
  1. <property name="resultAttributeMapping">

  2. <map>

  3. <entry key="uid" value="loginid" />

  4. <entry key="mail" value="email" />

  5. </map>

  6. </property>

 

咱們須要更改這個jsp代碼,打開該JSP,加入以下的這段代碼:

 
  1. <!-- return more attributes from attributeRepository start -->

  2. <c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes) > 0}">

  3.  
  4. <cas:attributes>

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

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

  9.  
  10. </c:forEach>

  11.  
  12. </cas:attributes>

  13.  
  14. </c:if>

  15. <!-- return more attributes from attributeRepository end -->


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

 

 
  1. <%@ page session="false" %>

  2. <%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

  3. <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="fn" %>

  4. <cas:serviceResponse xmlns:cas='http://www.yale.edu/tp/cas'>

  5. <cas:authenticationSuccess>

  6. <cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}

  7. </cas:user>

  8.  
  9. <!-- return more attributes from attributeRepository start -->

  10. <c:if test="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes) > 0}">

  11.  
  12. <cas:attributes>

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

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

  17.  
  18. </c:forEach>

  19.  
  20. </cas:attributes>

  21.  
  22. </c:if>

  23. <!-- return more attributes from attributeRepository end -->

  24.  
  25. <c:if test="${not empty pgtIou}">

  26. <cas:proxyGrantingTicket>${pgtIou}</cas:proxyGrantingTicket>

  27. </c:if>

  28. <c:if test="${fn:length(assertion.chainedAuthentications) > 1}">

  29. <cas:proxies>

  30. <c:forEach var="proxy" items="${assertion.chainedAuthentications}" varStatus="loopStatus" begin="0" end="${fn:length(assertion.chainedAuthentications)-2}" step="1">

  31. <cas:proxy>${fn:escapeXml(proxy.principal.id)}</cas:proxy>

  32. </c:forEach>

  33. </cas:proxies>

  34. </c:if>

  35. </cas:authenticationSuccess>

  36. </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

 
  1. package org.sky.framework.session;

  2.  
  3. import java.io.Serializable;

  4.  
  5. public class UserSession implements Serializable {

  6.  
  7. private String companyId = "";

  8. private String userName = "";

  9. private String userEmail = "";

  10.  
  11. public String getCompanyId() {

  12. return companyId;

  13. }

  14.  
  15. public void setCompanyId(String companyId) {

  16. this.companyId = companyId;

  17. }

  18.  
  19. public String getUserName() {

  20. return userName;

  21. }

  22.  
  23. public void setUserName(String userName) {

  24. this.userName = userName;

  25. }

  26.  
  27. public String getUserEmail() {

  28. return userEmail;

  29. }

  30.  
  31. public void setUserEmail(String userEmail) {

  32. this.userEmail = userEmail;

  33. }

  34.  
  35. }

 

AppSessionListener

 
  1. package org.sky.framework.session;

  2.  
  3. import javax.servlet.http.HttpSessionEvent;

  4. import javax.servlet.http.HttpSessionListener;

  5. import org.slf4j.Logger;

  6. import org.slf4j.LoggerFactory;

  7. import javax.servlet.ServletContext;

  8. import javax.servlet.ServletRequestEvent;

  9. import javax.servlet.ServletRequestListener;

  10. import javax.servlet.http.HttpServletRequest;

  11. import javax.servlet.http.HttpSession;

  12.  
  13. public class AppSessionListener implements HttpSessionListener {

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

  16.  
  17. @Override

  18. public void sessionCreated(HttpSessionEvent se) {

  19. HttpSession session = null;

  20. try {

  21. session = se.getSession();

  22. // get value

  23. ServletContext context = session.getServletContext();

  24. String timeoutValue = context.getInitParameter("sessionTimeout");

  25. int timeout = Integer.valueOf(timeoutValue);

  26. // set value

  27. session.setMaxInactiveInterval(timeout);

  28. logger.info(">>>>>>session max inactive interval has been set to "

  29. + timeout + " seconds.");

  30. } catch (Exception ex) {

  31. ex.printStackTrace();

  32. }

  33.  
  34. }

  35.  
  36. @Override

  37. public void sessionDestroyed(HttpSessionEvent arg0) {

  38. // TODO Auto-generated method stub

  39.  
  40. }

  41.  
  42. }

 

咱們的filter SampleSSOSessionFilter

 
  1. package org.sky.framework.session;

  2.  
  3. import javax.servlet.Filter;

  4. import javax.servlet.FilterChain;

  5. import javax.servlet.FilterConfig;

  6. import javax.servlet.RequestDispatcher;

  7. import javax.servlet.ServletContext;

  8. import javax.servlet.ServletException;

  9. import javax.servlet.ServletRequest;

  10. import javax.servlet.ServletResponse;

  11. import javax.servlet.http.HttpServletRequest;

  12. import javax.servlet.http.HttpServletResponse;

  13. import javax.servlet.http.HttpSession;

  14. import java.io.IOException;

  15. import java.io.PrintWriter;

  16. import java.util.Enumeration;

  17. import java.util.HashMap;

  18. import java.util.Map;

  19.  
  20. import org.jasig.cas.client.authentication.AttributePrincipal;

  21. import org.jasig.cas.client.util.AssertionHolder;

  22. import org.jasig.cas.client.validation.Assertion;

  23. import org.sky.util.WebConstants;

  24. import org.slf4j.Logger;

  25. import org.slf4j.LoggerFactory;

  26.  
  27. public class SampleSSOSessionFilter implements Filter {

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

  29. private String excluded;

  30. private static final String EXCLUDE = "exclude";

  31. private boolean no_init = true;

  32. private ServletContext context = null;

  33. private FilterConfig config;

  34. String url = "";

  35. String actionName = "";

  36.  
  37. public void setFilterConfig(FilterConfig paramFilterConfig) {

  38. if (this.no_init) {

  39. this.no_init = false;

  40. this.config = paramFilterConfig;

  41. if ((this.excluded = paramFilterConfig.getInitParameter("exclude")) != null)

  42. this.excluded += ",";

  43. }

  44. }

  45.  
  46. private String getActionName(String actionPath) {

  47. logger.debug("filter actionPath====" + actionPath);

  48. StringBuffer actionName = new StringBuffer();

  49. try {

  50. int begin = actionPath.lastIndexOf("/");

  51. if (begin >= 0) {

  52. actionName.append(actionPath.substring(begin, actionPath.length()));

  53. }

  54. } catch (Exception e) {

  55. }

  56. return actionName.toString();

  57. }

  58.  
  59. private boolean excluded(String paramString) {

  60. // logger.info("paramString====" + paramString);

  61. // logger.info("excluded====" + this.excluded);

  62. // logger.info(this.excluded.indexOf(paramString + ","));

  63. if ((paramString == null) || (this.excluded == null))

  64. return false;

  65. return (this.excluded.indexOf(paramString + ",") >= 0);

  66. }

  67.  
  68. @Override

  69. public void destroy() {

  70. // TODO Auto-generated method stub

  71.  
  72. }

  73.  
  74. @Override

  75. public void doFilter(ServletRequest request, ServletResponse response, FilterChain arg2) throws IOException, ServletException {

  76. HttpServletRequest req = (HttpServletRequest) request;

  77. HttpServletResponse resp = (HttpServletResponse) response;

  78. UserSession uinfo = new UserSession();

  79. HttpSession se = req.getSession();

  80.  
  81. url = req.getRequestURI();

  82. actionName = getActionName(url);

  83. //actionName = url;

  84. logger.debug(">>>>>>>>>>>>>>>>>>>>SampleSSOSessionFilter: request actionname" + actionName);

  85. if (!excluded(actionName)) {

  86. try {

  87. uinfo = (UserSession) se.getAttribute(WebConstants.USER_SESSION_OBJECT);

  88. AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal();

  89. String userName = principal.getName();

  90. logger.info("userName: " + userName);

  91. if (userName != null && userName.length() > 0 && uinfo == null) {

  92. Map attributes = principal.getAttributes();

  93. String email = (String) attributes.get("email");

  94. uinfo = new UserSession();

  95. String[] userAttri = userName.split(",");

  96. uinfo.setUserName(userAttri[0]);

  97. uinfo.setCompanyId(userAttri[1]);

  98. uinfo.setUserEmail(email);

  99. se.setAttribute(WebConstants.USER_SESSION_OBJECT, uinfo);

  100.  
  101. }

  102. } catch (Exception e) {

  103. logger.error("SampleSSOSessionFilter error:" + e.getMessage(), e);

  104. resp.sendRedirect(req.getContextPath() + "/syserror.jsp");

  105. return;

  106. }

  107. } else {

  108. arg2.doFilter(request, response);

  109. return;

  110. }

  111. try {

  112. arg2.doFilter(request, response);

  113. return;

  114. } catch (Exception e) {

  115. logger.error("SampleSSOSessionFilter fault: " + e.getMessage(), e);

  116. }

  117. }

  118.  
  119. @Override

  120. public void init(FilterConfig config) throws ServletException {

  121. // TODO Auto-generated method stub

  122. this.config = config;

  123. if ((this.excluded = config.getInitParameter("exclude")) != null)

  124. this.excluded += ",";

  125. this.no_init = false;

  126. }

  127. }

 

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

咱們對於這兩個CAS客戶端工程的web.xml文件所作出的修改以下

 

  1. 將原有的9090(由於原來咱們的cas-server是放在tomcat裏的,當時設的端口號爲9090,那是爲了不端口號和咱們的jboss中的8080重複。而如今,咱們能夠把全部的9090改回成8080了)。
  2. 增長如下這段代碼
 
  1. <filter>

  2. <filter-name>SampleSSOSessionFilter</filter-name>

  3. <filter-class>org.sky.framework.session.SampleSSOSessionFilter</filter-class>

  4. <init-param>

  5. <param-name>exclude</param-name>

  6. <param-value>/syserror.jsp

  7. </param-value>

  8. </init-param>

  9. </filter>

  10. <filter-mapping>

  11. <filter-name>SampleSSOSessionFilter</filter-name>

  12. <url-pattern>*</url-pattern>

  13. </filter-mapping>

 

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

 

由於咱們爲兩個CAS客戶端工程增長了一個syserror.jsp,以用於在獲取CAS SERVER端出錯時進行重定向用,而這個syserror.jsp是不須要通過什麼登陸、什麼記錄websession用的,因此,它必須是被「excluded」掉的,對吧,具體它是怎麼實現的,你們能夠本身跟一下代碼。

 

主要是注意看SampleSSOSessionFilter中如下這段代碼:

 
  1. uinfo = (UserSession) se.getAttribute(WebConstants.USER_SESSION_OBJECT);

  2. AttributePrincipal principal = (AttributePrincipal) req.getUserPrincipal();

  3. String userName = principal.getName();

  4. logger.info("userName: " + userName);

  5. if (userName != null && userName.length() > 0 && uinfo == null) {

  6. Map attributes = principal.getAttributes();

  7. String email = (String) attributes.get("email");

  8. uinfo = new UserSession();

  9. String[] userAttri = userName.split(",");

  10. uinfo.setUserName(userAttri[0]);

  11. uinfo.setCompanyId(userAttri[1]);

  12. uinfo.setUserEmail(email);

  13. se.setAttribute(WebConstants.USER_SESSION_OBJECT, uinfo);

  14.  
  15. }


好了,咱們如今要作的就是爲cas-sample-site1/2各配上一個用於顯示咱們是否可以成功從cas-server端傳過來登陸成功後用戶信息的index.jsp了。

 

cas-sample-site1/index.jsp

 
  1. <%@ page language="java" contentType="text/html; charset=utf-8"

  2. pageEncoding="utf-8"%>

  3. <%@ page import="org.sky.framework.session.UserSession, org.sky.util.WebConstants" %>

  4. <%

  5. UserSession us=(UserSession)session.getAttribute(WebConstants.USER_SESSION_OBJECT);

  6. String uname=us.getUserName();

  7. String email=us.getUserEmail();

  8. String companyId=us.getCompanyId();

  9. %>

  10. <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

  11. <html>

  12. <head>

  13. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

  14. <title>cas sample site1</title>

  15. </head>

  16. <body>

  17. <h1>cas sample site1 Hello: <%=uname%>(<%=email%>) u are@Company: <%=companyId%></h1>

  18. </p>

  19. <a href="http://localhost:8080/cas-sample-site2/index.jsp">cas-sample-site2</a>

  20.  
  21. </br>

  22. <a href="http://localhost:8080/cas-server/logout">退出</a>

  23. </body>

  24. </html>

cas-sample-site2/index.jsp

 
  1. <%@ page language="java" contentType="text/html; charset=utf-8"

  2. pageEncoding="utf-8"%>

  3. <%@ page import="org.sky.framework.session.UserSession, org.sky.util.WebConstants" %>

  4. <%

  5. UserSession us=(UserSession)session.getAttribute(WebConstants.USER_SESSION_OBJECT);

  6. String uname=us.getUserName();

  7. String email=us.getUserEmail();

  8. String companyId=us.getCompanyId();

  9. %>

  10. <!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">

  11. <html>

  12. <head>

  13. <meta http-equiv="Content-Type" content="text/html; charset=utf-8">

  14. <title>cas sample site2</title>

  15. </head>

  16. <body>

  17. <h1>cas sample site2 Hello: <%=uname%>(<%=email%>) u are@Company: <%=companyId%></h1>

  18. <a href="http://localhost:8080/cas-sample-site1/index.jsp">cas-sample-site1</a>

  19. </br>

  20. <a href="http://localhost:8080/cas-server/logout">退出</a>

  21. </body>

  22. </html>

 

運行今天全部的例子

測試用ldap中全部用戶的ldif代碼:

 

 
  1. dn: dc=sky,dc=org

  2. dc: sky

  3. objectClass: top

  4. objectClass: domain

  5.  
  6. dn: o=company,dc=sky,dc=org

  7. objectClass: organization

  8. o: company

  9.  
  10. dn: ou=members,o=company,dc=sky,dc=org

  11. objectClass: organizationalUnit

  12. ou: members

  13.  
  14. dn: cn=user1,ou=members,o=company,dc=sky,dc=org

  15. sn: user1

  16. cn: user1

  17. userPassword: aaaaaa

  18. objectClass: organizationalPerson

  19.  
  20. dn: cn=user2,ou=members,o=company,dc=sky,dc=org

  21. sn: user2

  22. cn: user2

  23. userPassword: abcdefg

  24. objectClass: organizationalPerson

  25.  
  26. dn: uid=mk,ou=members,o=company,dc=sky,dc=org

  27. objectClass: posixAccount

  28. objectClass: top

  29. objectClass: inetOrgPerson

  30. gidNumber: 0

  31. givenName: Yuan

  32. sn: MingKai

  33. displayName: YuanMingKai

  34. uid: mk

  35. homeDirectory: e:\user

  36. mail: mk.yuan@nttdata.com

  37. cn: YuanMingKai

  38. uidNumber: 13599

  39. userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=

  40.  
  41. dn: o=101,o=company,dc=sky,dc=org

  42. o: 101

  43. objectClass: organization

  44.  
  45. dn: o=102,o=company,dc=sky,dc=org

  46. o: 102

  47. objectClass: organization

  48.  
  49. dn: o=103,o=company,dc=sky,dc=org

  50. o: 103

  51. objectClass: organization

  52.  
  53. dn: o=104,o=company,dc=sky,dc=org

  54. o: 104

  55. objectClass: organization

  56.  
  57. dn: uid=marious,o=101,o=company,dc=sky,dc=org

  58. objectClass: posixAccount

  59. objectClass: top

  60. objectClass: inetOrgPerson

  61. gidNumber: 0

  62. givenName: Wang

  63. sn: LiMing

  64. displayName: WangLiMing

  65. uid: marious

  66. homeDirectory: d:\

  67. cn: WangLiMing

  68. uidNumber: 47967

  69. userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=

  70. mail: aaa@a.net

  71.  
  72. dn: uid=sky,o=101,o=company,dc=sky,dc=org

  73. objectClass: posixAccount

  74. objectClass: top

  75. objectClass: inetOrgPerson

  76. gidNumber: 0

  77. givenName: Yuan

  78. sn: Tao

  79. displayName: YuanTao

  80. uid: sky

  81. homeDirectory: d:\

  82. cn: YuanTao

  83. uidNumber: 26422

  84. userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=

  85. mail: bbb@b.net

  86.  
  87. dn: uid=jason,o=102,o=company,dc=sky,dc=org

  88. objectClass: posixAccount

  89. objectClass: top

  90. objectClass: inetOrgPerson

  91. gidNumber: 0

  92. givenName: zhang

  93. sn: lei

  94. displayName: zhanglei

  95. uid: jason

  96. homeDirectory: d:\

  97. cn: zhanglei

  98. uidNumber: 62360

  99. userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=

  100. mail: jason@abc.net

  101.  
  102. dn: uid=andy.li,o=103,o=company,dc=sky,dc=org

  103. objectClass: posixAccount

  104. objectClass: top

  105. objectClass: inetOrgPerson

  106. gidNumber: 0

  107. givenName: Li

  108. sn: Jun

  109. displayName: LiJun

  110. uid: andy.li

  111. homeDirectory: d:\

  112. cn: LiJun

  113. uidNumber: 51204

  114. userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=

  115. mail: andy.li@jesus.chris

  116.  
  117. dn: uid=pitt,o=104,o=company,dc=sky,dc=org

  118. objectClass: posixAccount

  119. objectClass: top

  120. objectClass: inetOrgPerson

  121. gidNumber: 0

  122. givenName: Brad

  123. sn: Pitt

  124. displayName: Brad Pitt

  125. uid: pitt

  126. homeDirectory: d:\

  127. cn: Brad Pitt

  128. uidNumber: 64650

  129. userPassword: {SHA}96niR3fsIyEsVNejULxb6lR3/bs=

  130. 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是什麼:

 
  1. <select id="companyid" name="companyid" >

  2. <option value="101" selected>上海煤氣公司</option>

  3. <option value="102" selected>上海自來水廠</option>

  4. <option value="103" selected>FBI</option>

  5. <option value="104" selected>神盾局</option>

  6. </select>

是102,說明咱們的傳值傳對了。

 

咱們如今再用debug模式來調試一下這個用例。

 

 

 

好了,結束今天的課程。

 

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

 

  1. 自定義CAS SSO登陸界面
  2. 在CAS SSO登陸界面增長咱們自定義的登陸用元素
  3. 使用LDAP帶出登陸用戶在LDAP內存儲的其它更多的信息
  4. 實現了CAS SSO支持多租戶登陸的功能

雖然這一過程很痛苦,很變態,可是經過這樣的一個案例,咱們完成了一件了不得的事情,同時對於這種國外開源軟件的customization咱們也有了一個認識,就是歐美的一些軟件,它的自定義都是經過擴展、繼承、插件式的方式來實現的,這說明他們的軟件在設計之初就考慮到了這些擴展。

 

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

 

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

 

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

 

作IT的必定要多play with it,要否則,你很難有本身的感性上的認識,沒有了感性認識的基礎,那也就談不上什麼「理性認識」和「昇華」了,呵呵!

 

在咱們從此的教程中,咱們動手改代碼或者集成其它開源產品的機會還有不少、不少。。。。。。甚至還會涉及到JDK裏的一些東西,讓咱們一塊兒慢慢來吧。

相關文章
相關標籤/搜索