CAS配置

         CAS认证框架提供了高级的配置和与CAS服务的数据交换。在本节中,我们将会介绍CAS集成的高级配置。在我们觉得重要的地方将会包含相关的CAS配置指令,但是要记住的是CAS配置是很复杂的并超出了本书的范围。

从CAS  assertion中获取属性

         在CAS服务器传递ticket校验结果时,可以将基于CAS认证时查询到的信息进行传递(给CAS服务)。这些信息以键值对的方式进行传递,并可以包含用户相关的任何数据。我们将会使用这个功能在CAS响应中传递用户的属性,包括GrantedAuthority信息。

CAS内部如何工作

         在进入CAS配置之前,我们简单介绍CAS认证过程的标准行为。下图将会帮助你理解CAS与嵌入式LDAP服务器交互的配置步骤:

 上图描述了CAS服务器内部的认证流程,如果你要实现Spring Security与CAS的集成,你可能要修改CAS服务器的配置。所以,理解CAS认证的整体流程如何工作是很重要的。

         CAS服务器的org.jasig.cas.authentication.AuthenticationManager(不要与SpringSecurity的同名类相混淆)负责基于提供的凭证信息进行用户认证。与Spring Security很相似,实际的认证委托给了一个(或更多)实现了org.jasig.cas.authentication.handler.AuthenticationHandler接口的处理类(在Spring Security中对应的接口是AuthenticationProvider)。

         最后,一个org.jasig.cas.authentication.principal.CredentialsToPrincipalResolver用来将传递进来的安全实体信息转换成完整的org.jasig.cas.authentication.principal.Principal(类似于Spring Security中UserDetailsService实现所作的那样)。

         尽管没有没有完整介绍CAS服务器的完整功能,但是这些也能够帮助你理解下面几个练习中的配置步骤。我们建议你阅读CAS的源码并学习网络上的文档,在JA-SIG CAS wiki上,地址为: http://www.ja-sig.org/wiki/display/CAS。

CAS连接嵌入式LDAP服务器

         CAS默认配置的org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver并不允许我们在与Spring Security CAS集成时,传递回属性信息,所以我们建议使用一个允许我们这样做的实现。

         一个很容易配置和使用的认证处理(尤其是你学习了前章中的LDAP练习)是org.jasig.cas.authentication.handler.AuthenticationHandler,它会与我们前章中的嵌入式LDAP服务器通信。在后文中,我们将会介绍配置CAS返回LDAP属性。

         所有CAS的配置都会在CAS安装后的WEB-INF/deployerConfigContext.xml文件中,将会涉及到插入类声明到已经存在的配置文件片段中。

         【如果这个文件的内容你感到熟悉,那是因为CAS使用Spring框架进行配置,就像JBCP Pets!如果你想深入了解这些配置是做什么的,我们建议你使用一个较好的IDE来方便的查看CAS的源码。记住在本节及其余所有章节中,我们提到的WEB-INF/deployerConfigContext.xml,指的是CAS安装环境中而不是JBCP Pets。】

         首先,我们需要添加一个AuthenticationHandler来代替SimpleTestUsernamePasswordAuthenticationHandler,它将会试图绑定用户到LDAP上(就像我们在第九章:LDAP目录服务中做的那样)。AuthenticationHandler要放在authenticationManager  bean的authenticationHandlers属性中:

 

<property name="authenticationHandlers">  <list>
<!-- ... -->
  <bean class="org.jasig.cas.adaptors.ldap.BindLdapAuthenticationHandler">
    <property name="filter" value="uid=%u" />
    <property name="searchBase" value="ou=Users,dc=jbcppets,dc=com" />
    <property name="contextSource" ref="contextSource" />
  </bean>
 
SimpleTestUsernamePasswordAuthenticationHandler,或者至少将其定义移到BindLdapAuthenticationHandler后面,否则你的CAS认证不会使用LDAP而是会使用这个默认实现。
         你可能会意识到这个bean引用了一个contextSource bean——它定义了org.springframework.ldap.core.ContextSource实现,而CAS将会用它与LDAP交互(对,CAS也使用了Spring LDAP)。我们将会在文件的最后进行定义,如下:
 
 
<bean id="contextSource" class="org.springframework.ldap.core.support.LdapContextSource"> <property name="urls">
  <list>
   <value>ldap://127.0.0.1:33389</value>
  </list>
 </property>
 <property name="userDn"
 value="uid=ldapadmin,ou=Administrators,ou=Users,dc=jbcppets,dc=com"/>
 <property name="password" value="password"/>
 <property name="baseEnvironmentProperties">
  <map>
   <entry>
     <key>
      <value>java.naming.security.authentication</value>
     </key>
     <value>simple</value>
   </entry>
  </map>
 </property>
</bean>
 
Spring Security连接外部的(非嵌入式的)LDAP服务器很类似,CAS需要一个用户DN作为管理用户以首先绑定到LDAP目录上。在本例中,我们使用的管理员是在第九章的JBCPPets.ldif启动练习中定义的。URL  ldap://127.0.0.1:33389包含了端口33389,是Spring Security嵌入式LDAP服务器默认使用的。正如我们在第九章讨论的,在产品配置中,你可能更会使用LDAPS以确保CAS LDAP请求的安全。
         最后,我们需要配置一个新的org.jasig.cas.authentication.principal.CredentialsToPrincipalResolver,它负责将用户提供的凭证(CAS已经使用BindLdapAuthenticationHandler进行了认证)转换成完整的org.jasig.cas.authentication.principal.Principal认证实体。你会发现在这个类中有很多配置选项,我们会将其略过,但是欢迎你深入了解CAS。
         在CAS authenticationManager bean中的credentialsToPrincipalResolvers属性中,添加如下的内联bean定义:
 
 
<property name="credentialsToPrincipalResolvers">  <list>
    <bean class="org.jasig.cas.authentication.principal.CredentialsToLDAPAttributePrincipalResolver">
      <property name="credentialsToPrincipalResolver">
        <bean class="org.jasig.cas.authentication.principal.UsernamePasswordCredentialsToPrincipalResolver" />
      </property>
      <property name="filter" value="(uid=%u)" />
      <property name="principalAttributeName" value="uid" />
      <property name="searchBase" value="ou=Users,dc=jbcppets,dc=com" />
      <property name="contextSource" ref="contextSource" />
      <property name="attributeRepository" ref bean="attributeRepository" />
    </bean>
 
Spring Security LDAP配置,相同的行为也在CAS中存在即基于DN中的目录子树根据属性匹配搜索安全实体。
         注意,我们还没有配置attributeRepository,它引用了org.jasig.services.persondir.IpersonAttributeDao的实现。CAS提供了默认的配置,包含了这个接口的简单实现org.jasig.services.persondir.support.StubPersonAttributeDao,这在LDAP属性的练习前是足够的(稍后会讲到)。
         所以,现在我们已经在CAS中配置了基本的LDAP认证,你能够重启CAS,启动JBCP Pets(如果它没有在运行的话)并认证任何在第九章中使用的LDAP用户(用户名ldapguest,密码password就不错)。但是在返回应用时,你会看到一个丑陋的403访问拒绝页面,这是因为我们的AuthenticationUserDetailsService在数据库中没有找到LDAP用户。现在让我们来解决这个问题!
CAS  assertion中获取UserDetails
         当首次建立CAS与Spring Security集成时,我们配置过UserDetailsByNameServiceWrapper,它会将CAS提供的用户名转换成UserDetails,这是提过使用我们引用的UserDetailsService对象(我们的例子中,就是JdbcDaoImpl)。现在CAS引用了LDAP服务器,我们可以使用LdapUserDetailsService就像我们在第九章结束时讨论的那样,功能就会好用了。
         但是在这里,我们体验Spring Security CAS集成的另外一种能力,即从CAS assertion中填充UserDetails。这只需简单的将AuthenticationUserDetailsService实现切换至o.s.s.cas.userdetails.GrantedAuthorityFromAssertionAttributesUserDetailsService就可以了,它的工作就是读取CAS assertion、寻找特定的属性并将属性值与用户的GrantedAuthority进行匹配。假设assertion返回了一个名为role的属性。我们只需要在dogstore-base.xml中简单配置一个新的authenticationUserDetailsService bean:
 
 
<bean id="authenticationUserDetailsService" class="org.springframework.security.cas.userdetails.GrantedAuthorityFromAssertionAttributesUserDetailsService">
  <constructor-arg>
    <array>
      <value>role</value>
    </array>
  </constructor-arg>
</bean>

assertion中的内容。

CAS  assertion

         为了辅助查询CAS返回给JBCP Pets应用的信息,我们修改AccountController以展现CAS登录用户的信息以及CAS为我们提供的用户信息。首先,我们添加一个简单的URL处理方法到AccountController中:

 

@RequestMapping(value="/account/viewCasUserProfile.do",method=RequestMethod.GET)
public void showViewCasUserProfilePage(ModelMap model) {
  final Authentication auth = SecurityContextHolder.getContext().getAuthentication();
  model.addAttribute("auth", auth);
  if(auth instanceof CasAuthenticationToken) {
    model.addAttribute("isCasAuthentication", Boolean.TRUE);
  } 
}

JSP,它会展现CasAuthenticationToken的一些信息,这个对象代表CAS认证过的用户。它会放在WEB-INF/views/account/viewCasUserProfile.jsp。

 

<!-- Common Header and Footer Omitted -->
<h1>View Profile</h1>
<p>
  Some information about you, from CAS:
</p>
<ul>
  <li><strong>Auth:</strong> ${auth}</li>
  <li><strong>Username:</strong> ${auth.principal}</li>
  <li><strong>Credentials:</strong> ${auth.credentials}</li>
  <c:if test="${isCasAuthentication}">
    <li><strong>Assertion:</strong> ${auth.assertion}</li>
    <li><strong>Assertion Attributes:</strong>
    <c:forEach items="${auth.assertion.attributes}" var="attr">
      ${attr.key}:${attr.value}<br />
    </c:forEach>
</li>
<li><strong>Assertion Attribute Principal:</strong> ${auth.assertion.principal}</li>
<li><strong>Assertion Principal Attributes:</strong>
    <c:forEach items="${auth.assertion.principal.attributes}" var="attr">
      ${attr.key}:${attr.value}<br />
    </c:forEach>
</li>
</c:if>
</ul>

WEB-INF/views/account/home.jsp中:

 

<h1>Welcome to Your Account</h1>
<!-- omitted -->
<ul>
  <li><a href="viewCasUserProfile.do">View CAS User Profile</a></li>

assertion属性认证好用之前,我们需要(临时的)将这个页面进行授权检查。(Finally, we'll have to (temporarily) disable authorization checks for this page, until we get assertion attribute-based authorization working. )要做到这样只需要简单调整dogstore-security.xml,所以任何具有GrantedAuthority的登录用户都能访问这个页面以及“My Account”页面:

 

<intercept-url pattern="/home.do" access="permitAll"/><intercept-url pattern="/account/home.do" access="!anonymous"/>
<intercept-url pattern="/account/view*Profile.do" 
access="!anonymous"/>
<intercept-url pattern="/account/*.do" access="hasRole('ROLE_USER')"/>

UI更新后,重启JBCP Pets应用,并试图访问这个页面。你会发现在页面提供了很多assertion包含的信息。

LDAP属性到CAS属性

         最后一个揭开的谜底是需要我们匹配LDAP属性到CAS assertion中(包括我们希望在GrantedAuthority中包含的role属性)。

         我们将会在CAS deployerConfigContext.xml中添加一点其它的配置。这些新的配置将会指导CAS怎样从CAS Principal对象到CAS IPersonAttributes对象匹配属性,而后者将会最终序列化为ticket校验的一部分。这个bean的配置应该替代同名的bean即attributeRepository。

 

<bean id="attributeRepository" class="org.jasig.services.persondir.support.ldap.LdapPersonAttributeDao">
  <property name="contextSource" ref="contextSource" />
  <property name="requireAllQueryAttributes" value="true" />
  <property name="baseDN" value="ou=Users,dc=jbcppets,dc=com" />
  <property name="queryAttributeMapping">
    <map>
      <entry key="username" value="uid" />
    </map>
  </property>
  <property name="resultAttributeMapping">
    <map>
      <entry key="cn" value="FullName" />
      <entry key="sn" value="LastName" />
      <entry key="description" value="role" />
    </map>
  </property>
</bean>

Principal与后端的LDAP目录进行匹配(这就是queryAttributeMapping属性,将Principal的username域与LDAP查询的uid属性相匹配)。提供的baseDN属性要使用LDAP进行查询(uid=ldapguest) ,并且属性要从匹配的条目进行读取。 匹配到Principal属性使用resultAttributeMapping属性中的键值对——我们将LDAP的cn和sn属性匹配到有意义的名字,而description属性匹配到role属性,而这个role属性就是GrantedAuthorityFromAssertionAttributesUserDetailsService要进行查找的。

         造成这样的复杂性很大程度上是因为这个功能的一部分被另外一个项目进行了封装即Person Directory(http://www.ja-sig.org/wiki/display/PD/Home),它的目的是从多个源收集关于某个人的信息并集成到单个视图上。Person Directory的设计并没有直接关联到CAS服务器上,所以能够重用在其它应用中。这种设计选择的不足之处在于它会使得CAS集成的配置比预想的更复杂。

【一些CAS中已有的LDAP属性。我们可能愿意建立与第九章Spring Security LDAP相同类型的LDAP查询,即能够匹配Principal到一个完整的LDAP标识名,而后用这个DN来查询组信息(使用基本的uniqueMember属性到一个groupOfUniqueNames条目)。但是,CAS LDAP代码并没有这种灵活性,这就使得更复杂的LDAP匹配需要扩展CAS中的基本类。】

         还是感到迷惑?我们承认这对我们来说也有些迷惑,因为在LDAP数据和CAS数据之间的来回交互中有不同的方式。CAS的邮件列表和社区wiki(http://www.ja-sig.org/wiki/display/CASUM/Home)是了解别人经验和与有知识的读者询问问题的好地方。

CAS  assertion中的属性

         遗憾的是,CAS 2.0协议并没有明确定义返回数据的标准格式。在CAS的JIRA缺陷跟踪系统中,有很多尝试实现这个功能,但是被拒绝了,因为CAS2.0是稳定的所以在最近并不会修改它。

         也就是说,很多用户需要扩展CAS的响应来包含属性。最终,CAS到客户应用的ticket校验响应是通过一个JSP渲染的,所以很容易修改你的CAS安装来包含一个响应,这个响应符合Cas20ServiceTicketValidator的属性解析。在你的CAS部署中,编辑WEB-INF/view/jsp/protocol/2.0/casServiceValidationSuccess.jsp,并添加以下内容:

 

<cas:authenticationSuccess>  <cas:user>${fn:escapeXml(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.id)}</cas:user>
  <cas:attributes>
  <c:forEach var="attr" items="${assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes}"
  varStatus="loopStatus" begin="0"
  end="${fn:length(assertion.chainedAuthentications[fn:length(assertion.chainedAuthentications)-1].principal.attributes)-1}" step="1">
  <cas:${fn:escapeXml(attr.key)}>${fn:escapeXml(attr.value)}</cas:${fn:escapeXml(attr.key)}>
  </c:forEach>
  </cas:attributes>
 
CAS响应格式:
 
 
<cas:serviceResponse xmlns:cas="http://www.yale.edu/tp/cas">  <cas:authenticationSuccess>
    <cas:user>ldapguest</cas:user>
    <cas:attributes>
      <cas:FullName>LDAP Guest</cas:FullName>
      <cas:role>ROLE_USER</cas:role>
      <cas:LastName>Guest</cas:LastName>
    </cas:attributes>
  </cas:authenticationSuccess>
</cas:serviceResponse>

CAS和JBCP Pets,你应该能够用ldapguest登录并访问使用ROLE_USER保护的区域。另外,你应该看一下“View CAS Profile”页面,它展现了CAS assertion返回的属性。

【注意——我们可以看到LdapPersonAttributeDao.resultAttributeMapping提供的属性名被直接用到CAS响应的XML中。这意味着它们不能是任意的,必须符合XML元素命名规则(例如,不能包含空格)。如果你想修改这种行为,你可能需要比这里更复杂的JSP代码。】

         干的漂亮——在这两个复杂的产品间有很多的配置工作。休息一下喝杯咖啡吧!

         一些CAS开发人员选择的另一种方式是扩展Cas20ServiceTicketValidator(在Spring Security这一端)来对CAS返回的响应进行自定义的处理。

SAML 1.1进行替代的票据认证

         SAML是一个标准的、跨平台的协议,它通过结构化的XML assertion进行身份校验。SAML被很多的产品所支持,包括CAS(实际上,我们将会在稍后的章节中看到Spring Security里面对SAML的支持)。

         SAML的安全assertion XML语法解决了我们前面讲到的CAS响应协议传递属性的问题。令人高兴的是,切换CAS ticket校验和SAML ticket校验只需要修改dogstore-base.xml中的TicketValidator实现即可。添加一个bean如下:

 

<bean id="samlTicketValidator" class="org.jasig.cas.client.validation.Saml11TicketValidator">  <constructor-arg value="http://localhost:8080/cas/"/>
</bean>

CasAuthenticationProvider使用这个TicketValidator:

 

<bean id="casAuthenticationProvider" class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
  <property name="ticketValidator" ref="samlTicketValidator"/>
  <property name="serviceProperties" ref="casService"/>

JBCP Pets应用就会使用SAML响应进行ticket校验了。注意的是,CAS ticket响应并没有被Spring Security进行日志记录,所以你要么对JA-SIG CAS客户端类启用日志(在log4j中对org.jasig启用日志)要么使用调试查看响应的不同。

         总之,建议使用SAML ticket校验而不是CAS 2.0 ticket校验,因为前者提供了更多的防治重造的功能,包括时间戳校验并以标准的方式解决了属性问题。

属性查询的用处

         要记住的是CAS为我们的应用提供了一层抽象,移除了我们引用对用户存储的直接访问,作为替代所有这样的访问通过CAS作为代理来进行。

         这个功能很强大!这意味着我们的应用不再关心用户在什么类型的存储中,也不用关心怎样访问它们的细节——只需与CAS确认一个用户有权限访问应用。对于系统管理员来说,这意味着,如果LDAP改名、转移位置或者修改的话,他们只需要在一个位置进行重新配置——CAS。通过CAS进行集中访问使得组织中安全架构更高级别的灵活性和适应性成为可能。

         扩展这个话题到从CAS获取属性的用处——现在,所有通过CAS认证的应用对用户都有了相同的视角(译者注:即获取的信息是一致的),从而能够在任何使用CAS的环境下以一致的格式显示信息。

         注意的是,一旦认证完成,Spring Security CAS不会再次查询CAS除非用户需要重新认证。这意味着,存储在应用本地用户Authentication对象中的属性和其它用户信息可能会失效且可能与CAS服务器不一致。注意要设置session的超时时间以避免这种潜在的问题。

其它的CAS功能

         除了通过Spring Security CAS包装暴露出来的功能,CAS还提供了高级配置功能。其中一些如下:

l  对访问多个使用CAS安全的应用提供了透明的单点登录功能,这要在一个可配置(在CAS服务器端)的时间内。通过在TicketValidator中将renew属性设置为true,应用可以强制用户到CAS进行认证——你可能希望在自定义代码中有条件的设置这个属性,如当用户试图访问应用中高安全性的区域时;

l  为不能直接访问CAS的二级应用提供了ticket代理功能。当从CAS请求一个ticket时,web应用可能通过二级代理应用来请求授权ticket。关于这个的更多功能可以阅读CAS网站(http://www.jasig.org/cas/proxy-authentication);

l  协调拥有活跃CAS session的多个参与应用间的单点登出(这不是通过Spring Security直接支持的,而是CAS客户端通过联合使用HttpSessionListener和servlet过滤器)。

         我们建议你去探索CAS客户端和服务端的所有功能,并在JA-SIG社区论坛中向大家提问题。

小结

         在本章中,我们学习了中心认证服务(CAS)单点登录门户,以及它怎样与Spring Security集成,在本章中,我们学到了如下内容:

         CAS架构以及在使用CAS的环境中各个参与角色的通信;

         使用CAS的应用给开发人员和系统管理员带来的好处;

         配置JBCP Pets与基本的CAS安装交互;

         更新CAS与LDAP交互并实现LDAP与使用CAS的应用共享数据;

         使用修改过的CAS 2.0协议和工业标准的SAML协议实现与CAS的属性交换。

         我们希望本章的内容是关于单点登录的一个有趣开端。在市场上还有很多其它的单点登录系统,它们中大多数是商用的,但是CAS在开源SSO领域无疑是领导者,并且是在任何组织中构建单点登录的绝佳平台。

         在下一章中,我们将会转回标准认证,但是这次我们将会移除用户名和密码而是依赖一种新的认证机制。有趣吗?那就开始吧!