34. CAS 认证

34.1 概述

JA-SIG 产生一个企业级的单点登录系统,称为 CAS。与其他活动不同,JA-SIG 的中央认证服务是开源的,广泛使用,易于理解,独立于平台,并且支持代理功能。Spring Security 完全支持 CAS,并提供了从 Spring Security 的单个应用程序部署到由企业范围的 CAS 服务器保护的多个应用程序部署的简单迁移路径。

你可以在 http://www.ja-sig.org/cas 中了解更多关于 CAS 的信息。你还需要访问该站点来下载 CAS 服务器文件。

34.2 CAS 如何工作

尽管 CAS 网站包含详细描述 CAS 体系结构的文档,我们在这里再次在 Spring Security 上下文中给出一般概述。Spring Security 3.x 支持 CAS 3。在编写时,CAS 服务器处于版本 3.4。

在企业中的某个地方,你需要安装一个 CAS 服务器。CAS 服务器只是一个标准的 WAR 文件,所以设置服务器没有什么困难。在 WAR 文件中,你将定制登录和其他显示给用户的单点登录页面。

在部署 CAS 3.4 服务器时,还需要在包含 CAS 的 deployerConfigContext.xml 中指定 AuthenticationHandler。AuthenticationHandler 有一个简单的方法,返回一个 boolean,以确定给定的凭证集合是否有效。AuthenticationHandler 实现将需要链接到某种类型的后端身份验证存储库,例如 LDAP 服务器或数据库。CAS 本身包含大量的 AuthenticationHandler,以帮助实现这一点。在下载和部署服务器 war 文件时,设置该文件是为了成功验证输入与其用户名匹配的密码的用户,这对于测试很有用。

除了 CAS 服务器本身,其他关键角色当然是部署在企业中的安全 web 应用程序。这些 web 应用程序被称为 "services"。有三种类型的服务。那些认证服务票,那些可以获得代理票据的人,以及那些认证代理票的人。代理票据的认证不同,因为代理的列表必须被验证,并且通常可以重用代理票据。

34.2.1 Spring Security 和 CAS 交互序列

web 浏览器、CAS 服务器和 Spring Security 安全服务之间的基本交互如下:

  • web 用户正在浏览服务的公共页面。不涉及 CAS 或 Spring Security。
  • 用户最终请求一个页面是保护的,或者它使用的 bean 之一是保护的。Spring Security 的 ExceptionTranslationFilter 将检测到 AccessDeniedException 或 AuthenticationException。
  • 因为用户的 Authentication 对象(或其缺失)导致 AuthenticationException,所以 ExceptionTranslationFilter 将调用配置好的 AuthenticationEntryPoint。如果使用 CAS,这将是 CasAuthenticationEntryPoint 类。
  • CasAuthenticationEntryPoint 将将用户的浏览器重定向到 CAS 服务器。它还将指示一个 service 参数,该参数是 Spring Security 服务(应用程序)的回调 URL。例如,浏览器重定向的 URL 可能是 https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas。
  • 在用户浏览器重定向到 CAS 之后,将提示他们输入用户名和密码。如果用户呈现一个会话 cookie,该 cookie 指示他们先前已经登录,则不会提示他们再次登录(此过程有例外,我们将在后面介绍)。CAS 将使用上面讨论的 PasswordHandler(或者如果使用 CAS 3.0,则使用 AuthenticationHandler)来确定用户名和密码是否有效。
  • 登录成功后,CAS 会将用户的浏览器重定向回原来的服务。它还包括一个 ticket 参数,它是表示 "service ticket" 的不透明字符串。继续我们前面的例子,浏览器重定向的 URL 可能是 https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ。
  • 回到服务 web 应用程序,CasAuthenticationFilter 总是监听对 /login/cas 的请求(这是可配置的,但是在本介绍中我们将使用默认值)。处理过滤器将构建一个表示服务票据的 UsernamePasswordAuthenticationToken。主体将等于 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而凭证将是服务票据不透明值。然后将此认证请求交给已配置的 AuthenticationManager。
  • AuthenticationManager 实现将是 ProviderManager,它又由 CasAuthenticationProvider 配置。CasAuthenticationProvider 仅对包含 CAS 特定主体(如 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)和 CasAuthenticationToken(稍后讨论)的 UsernamePasswordAuthenticationToken 作出响应。
  • CasAuthenticationProvider 将使用 TicketValidator 实现实现服务票据的验证。这通常是 Cas20ServiceTicketValidator,它是 CAS 客户端库中包含的类之一。在应用程序需要验证代理票据的情况下,使用 Cas20ProxyTicketValidator。TicketValidator 向 CAS 服务器发出 HTTP 请求,以便验证服务票据。它还可以包括代理回调 URL,它包含在这个例子中:https://my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptor
  • 回到 CAS 服务器,将接收验证请求。如果呈现的服务票据与签发票据的服务 URL 匹配,CAS 将以 XML 提供肯定响应,指示用户名。如果在认证中涉及到任何代理(下面讨论),代理列表也将包含在 XML 响应中。
  • [可选的] 如果对 CAS 验证服务的请求包括代理回调 URL(在 pgtUrl 参数中),CAS 将在 XML 响应中包括 pgtIou 字符串。这个 pgtIou 代表代理授予票据 IOU。然后,CAS 服务器将创建自己的 HTTPS 连接返回到 pgtUrl。这是为了相互认证 CAS 服务器和所声称的服务 URL。HTTPS 连接将用于向原始 web 应用程序发送代理授予票据。例如,https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH
  • Cas20TicketValidator 将解析从 CAS 服务器接收到的 XML。它将向 CasAuthenticationProvider 返回 TicketResponse, 其中包括用户名(强制)、代理列表(如果有的话)和代理授权票据 IOU(如果请求代理回调)。
  • 接下来,CasAuthenticationProvider 将调用一个配置的 CasProxyDecider。CasProxyDecider 指示在 TicketResponse 中的代理列表是否可以接受该服务。Spring Security 提供了几种实现:RejectProxyTickets、AcceptAnyCasProxy 和 NamedCasProxyDecider。这些名称基本上是自解释的,除了 NamedCasProxyDecider,允许提供信任代理的列表。
  • CasAuthenticationProvider 接下来将请求 AuthenticationUserDetailsService 加载应用于 Assertion 中包含的用户的 GrantedAuthority 对象。
  • 如果没有问题,CasAuthenticationProvider 将构造 CasAuthenticationToken ,包括 TicketResponse 和 GrantedAuthority 中包含的细节。
  • 然后,控件返回到 CasAuthenticationFilter,将创建的 CasAuthenticationToken 放置在安全上下文中。
  • 用户的浏览器被重定向到导致 AuthenticationException(或者根据配置的自定义目的地)的原始页面。

你还在这里真是太好了!现在让我们看看这是如何配置的

34.3 CAS 客户端的配置

由于 Spring Security,CAS 的 web 应用侧变得很容易。假设你已经知道使用 Spring Security 的基础知识,因此下面不再介绍这些内容。我们将假设使用基于命名空间的配置,并根据需要添加到 CAS bean 中。每个部分都建立在前一节上。在 Spring Security 示例中可以找到完整的 CAS 示例应用程序 。

34.3.1 服务票据认证

本节介绍如何设置 Spring Security 来验证服务票据。通常情况下,这都是 web 应用程序需要的。你需要在应用程序上下文中添加 ServiceProperties bean。这代表你的 CAS 服务:

<bean id="serviceProperties"
	class="org.springframework.security.cas.ServiceProperties">
<property name="service"
	value="https://localhost:8443/cas-sample/login/cas"/>
<property name="sendRenew" value="false"/>
</bean>

service 必须等于一个 URL,该 URL 将由 CasAuthenticationFilter 监视。sendRenew 默认为 false,但如果应用程序特别敏感,则应该设置为 true。此参数所做的是告诉 CAS 登录服务,单点登录登录是不可接受的。相反,用户需要重新输入用户名和密码以获得对服务的访问。

应该配置以下 bean 以开始 CAS 认证过程(假设你正在使用命名空间配置):

<security:http entry-point-ref="casEntryPoint">
...
<security:custom-filter position="CAS_FILTER" ref="casFilter" />
</security:http>

<bean id="casFilter"
	class="org.springframework.security.cas.web.CasAuthenticationFilter">
<property name="authenticationManager" ref="authenticationManager"/>
</bean>

<bean id="casEntryPoint"
	class="org.springframework.security.cas.web.CasAuthenticationEntryPoint">
<property name="loginUrl" value="https://localhost:9443/cas/login"/>
<property name="serviceProperties" ref="serviceProperties"/>
</bean>

为了操作 CAS,ExceptionTranslationFilter 必须将其 authenticationEntryPoint 属性设置为 CasAuthenticationEntryPoint bean。这可以很容易地使用 entry-point-ref,如上面的例子所做的那样。CasAuthenticationEntryPoint 必须引用 ServiceProperties bean(上面讨论过),该 bean 为企业的 CAS 登录服务器提供 URL。这是用户浏览器将被重定向的地方。

CasAuthenticationFilter 与 UsernamePasswordAuthenticationFilter(用于基于表单的登录)具有非常相似的属性。你可以使用这些属性定制诸如认证成功和失败的行为。

接下来需要添加一个 CasAuthenticationProvider 及其合作者:

<security:authentication-manager alias="authenticationManager">
<security:authentication-provider ref="casAuthenticationProvider" />
</security:authentication-manager>

<bean id="casAuthenticationProvider"
	class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
<property name="authenticationUserDetailsService">
	<bean class="org.springframework.security.core.userdetails.UserDetailsByNameServiceWrapper">
	<constructor-arg ref="userService" />
	</bean>
</property>
<property name="serviceProperties" ref="serviceProperties" />
<property name="ticketValidator">
	<bean class="org.jasig.cas.client.validation.Cas20ServiceTicketValidator">
	<constructor-arg index="0" value="https://localhost:9443/cas" />
	</bean>
</property>
<property name="key" value="an_id_for_this_auth_provider_only"/>
</bean>

<security:user-service id="userService">
<!-- Password is prefixed with {noop} to indicate to DelegatingPasswordEncoder that
NoOpPasswordEncoder should be used.
This is not safe for production, but makes reading
in samples easier.
Normally passwords should be hashed using BCrypt -->
<security:user name="joe" password="{noop}joe" authorities="ROLE_USER" />
...
</security:user-service>

CasAuthenticationProvider 使用 UserDetailsService 实例为用户加载权限,只要用户已经通过 CAS 身份验证。我们在这里展示了一个简单的内存设置。注意,CasAuthenticationProvider 并不实际使用密码进行身份验证,但它确实使用了权限。

如果你参考 CAS 如何工作 部分,bean 是合理的自我解释的。

这就完成了 CAS 最基本的配置。如果你没有犯任何错误,你的 web 应用程序应该在 CAS 单点登录的框架内愉快地工作。 Spring Security 的其他部分不必关心事实上 CAS 处理的身份验证。在下面的部分中,我们将讨论一些(可选的)更高级的配置。

34.3.2 单点注销

CAS 协议支持单点注销,并且可以很容易地添加到 Spring Security 配置中。下面是处理单点注销的 Spring Security 配置的更新

<security:http entry-point-ref="casEntryPoint">
...
<security:logout logout-success-url="/cas-logout.jsp"/>
<security:custom-filter ref="requestSingleLogoutFilter" before="LOGOUT_FILTER"/>
<security:custom-filter ref="singleLogoutFilter" before="CAS_FILTER"/>
</security:http>

<!-- This filter handles a Single Logout Request from the CAS Server -->
<bean id="singleLogoutFilter" class="org.jasig.cas.client.session.SingleSignOutFilter"/>

<!-- This filter redirects to the CAS Server to signal Single Logout should be performed -->
<bean id="requestSingleLogoutFilter"
	class="org.springframework.security.web.authentication.logout.LogoutFilter">
<constructor-arg value="https://localhost:9443/cas/logout"/>
<constructor-arg>
	<bean class=
		"org.springframework.security.web.authentication.logout.SecurityContextLogoutHandler"/>
</constructor-arg>
<property name="filterProcessesUrl" value="/logout/cas"/>
</bean>

logout 元素将用户从本地应用程序中注销,但不终止与 CAS 服务器或任何其他已登录的应用程序的会话。requestSingleLogoutFilter 过滤器将允许请求 /spring_security_cas_logout 的 URL 将应用程序重定向到配置的 CAS 服务器注销 URL。然后 CAS 服务器将向登录的所有服务发送一个注销请求。singleLogoutFilter 通过在静态 Map 中查找 HttpSession 来处理单个注销请求,然后将其无效。

可能需要混淆 logout 元素和 singleLogoutFilter 两者。首先在本地注销被认为是最佳实践,因为 SingleSignOutFilter 只是将 HttpSession 存储在静态 Map 中,以便在其上调用无效。通过上面的配置,注销的流程将是:

  • 用户请求 /logout,将用户从本地应用程序注销,并将用户发送到注销成功页面。
  • 注销成功页面,/cas-logout.jsp,应该指示用户单击指向 /logout/cas 的链接,以便注销所有应用程序。
  • 当用户点击链接时,用户被重定向到 CAS 单点注销 URL(https://localhost:9443/cas/logout)。
  • 在 CAS 服务器端,CAS 单点注销 URL 向所有 CAS 服务提交单点注销请求。在 CAS 服务端,JASIG 的 SingleSignOutFilter 通过注销原始会话来处理注销请求。

下一步是将以下内容添加到 web.xml 中

<filter>
<filter-name>characterEncodingFilter</filter-name>
<filter-class>
	org.springframework.web.filter.CharacterEncodingFilter
</filter-class>
<init-param>
	<param-name>encoding</param-name>
	<param-value>UTF-8</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>characterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
<listener>
<listener-class>
	org.jasig.cas.client.session.SingleSignOutHttpSessionListener
</listener-class>
</listener>

当使用 SingleSignOutFilter 时,你可能会遇到一些编码问题。因此,建议添加 CharacterEncodingFilter,以确保在使用 SingleSignOutFilter 时字符编码是正确的。再次,请参阅 JASIG 的文档以获取详细信息。SingleSignOutHttpSessionListener 确保当 HttpSession 到期时,用于单当注销的映射将被删除。

34.3.3 使用 CAS 对无状态服务进行认证

本节描述如何使用 CAS 对服务进行认证。换句话说,本节讨论如何设置使用与 CAS 进行认证的服务的客户端。下一节描述了如何使用 CAS 来设置无状态服务来进行认证。

配置 CAS 获取代理授予票据

为了对无状态服务进行认证,应用程序需要获得代理授予票据(PGT)。本节描述如何配置 Spring Security 以在 thencas-st[Service Ticket Authentication] 配置的基础上获得 PGT 构建。

第一步是在你的 Spring Security 配置中包含一个 ProxyGrantingTicketStorage。这用于存储由 CasAuthenticationFilter 获得的 PGT,以便它们可以用来获取代理票据。下面展示了一个示例配置

<!--
NOTE: In a real application you should not use an in memory implementation.
You will also want to ensure to clean up expired tickets by calling
ProxyGrantingTicketStorage.cleanup()
-->
<bean id="pgtStorage" class="org.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>

下一步是更新 CasAuthenticationProvider 以获得代理票据。为此,用 Cas20ProxyTicketValidator 替换 Cas20ServiceTicketValidator。应该将 proxyCallbackUrl 设置为应用程序将接收 PGT 的 URL。最后,配置还应参考 ProxyGrantingTicketStorage,以便它可以使用 PGT 获得代理票据。你可以找到下面应该做的配置更改的示例。

<bean id="casAuthenticationProvider"
	class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
	<bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator">
	<constructor-arg value="https://localhost:9443/cas"/>
		<property name="proxyCallbackUrl"
		value="https://localhost:8443/cas-sample/login/cas/proxyreceptor"/>
	<property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
	</bean>
</property>
</bean>

最后一步是更新 CasAuthenticationFilter 以接受 PGT 并将它们存储在 ProxyGrantingTicketStorage 中。重要的是,proxyReceptorUrl 与 Cas20ProxyTicketValidator 的 proxyCallbackUrl 相匹配。下面展示了一个示例配置。

<bean id="casFilter"
		class="org.springframework.security.cas.web.CasAuthenticationFilter">
	...
	<property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
	<property name="proxyReceptorUrl" value="/login/cas/proxyreceptor"/>
</bean>

使用代理票据调用无状态服务

现在 Spring Security 获得了 PGT,你可以使用它们来创建代理票据,该代理票据可用于对无状态服务进行认证。CAS 示例应用程序包含在 ProxyTicketSampleServlet 中的一个工作示例。示例代码可以在下面找到:

protected void doGet(HttpServletRequest request, HttpServletResponse response)
	throws ServletException, IOException {
// NOTE: The CasAuthenticationToken can also be obtained using
// SecurityContextHolder.getContext().getAuthentication()
final CasAuthenticationToken token = (CasAuthenticationToken) request.getUserPrincipal();
// proxyTicket could be reused to make calls to the CAS service even if the
// target url differs
final String proxyTicket = token.getAssertion().getPrincipal().getProxyTicketFor(targetUrl);

// Make a remote call using the proxy ticket
final String serviceUrl = targetUrl+"?ticket="+URLEncoder.encode(proxyTicket, "UTF-8");
String proxyResponse = CommonUtils.getResponseFromServer(serviceUrl, "UTF-8");
...
}

34.3.4 代理票据认证

CasAuthenticationProvider 区分有状态的和无状态的客户端。状态客户端被认为是提交给 CasAuthenticationFilter 的 filterProcessUrl 的任何客户端。无状态客户机是指在除了 filterProcessUrl 之外的 URL 上向 CasAuthenticationFilter 呈现认证请求的客户机。

因为远程协议无法在 HttpSession 的上下文中呈现自己,所以不可能依赖于在请求之间的会话中存储安全上下文的默认实践。此外,因为 CAS 服务器在票据被 TicketValidator 验证之后使票据无效,所以在随后的请求上呈现相同的代理票据将不起作用。

一个明显的选择是根本不使用 CAS 来远程处理协议客户端。然而,这将消除 CAS 的许多理想特征。作为中间层,CasAuthenticationProvider 使用 StatelessTicketCache 缓存。这仅用于无状态客户机,它使用的主体等于 CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER。接下来, CasAuthenticationProvider 将把得到的 CasAuthenticationToken 存储在 StatelessTicketCache 中,并在代理票据上键入。因此,远程协议客户端可以呈现相同的代理票据,CasAuthenticationProvider 不需要联系 CAS 服务器进行验证(除了第一个请求)。一旦经过认证,代理票据就可以用于除了原始目标服务之外的 URL。

本节基于前面的章节来容纳代理票据验证。第一步是指定对所有工件进行认证,如下所示。

<bean id="serviceProperties"
	class="org.springframework.security.cas.ServiceProperties">
...
<property name="authenticateAllArtifacts" value="true"/>
</bean>

下一步是指定 CasAuthenticationFilter 的 serviceProperties 和 authenticationDetailsSource。serviceProperties 属性指示 CasAuthenticationFilter 尝试对所有工件进行认证,而不是仅对 filterProcessUrl 上存在的工件进行认证。ServiceAuthenticationDetailsSource 创建一个 ServiceAuthenticationDetails,它确保在验证票据时使用基于 HttpServletRequest 的当前 URL 作为服务 URL。可以通过注入返回自定义 ServiceAuthenticationDetails 的自定义 AuthenticationDetailsSource 来定制生成服务 URL 的方法。

<bean id="casFilter"
	class="org.springframework.security.cas.web.CasAuthenticationFilter">
...
<property name="serviceProperties" ref="serviceProperties"/>
<property name="authenticationDetailsSource">
	<bean class=
	"org.springframework.security.cas.web.authentication.ServiceAuthenticationDetailsSource">
	<constructor-arg ref="serviceProperties"/>
	</bean>
</property>
</bean>

你还需要更新 CasAuthenticationProvider 来处理代理票据。为此,用 Cas20ProxyTicketValidator 替换 Cas20ServiceTicketValidator。你需要配置 statelessTicketCache 和你希望接受的代理。你可以找到一个更新的例子来接受下面的所有代理。

<bean id="casAuthenticationProvider"
	class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
	<bean class="org.jasig.cas.client.validation.Cas20ProxyTicketValidator">
	<constructor-arg value="https://localhost:9443/cas"/>
	<property name="acceptAnyProxy" value="true"/>
	</bean>
</property>
<property name="statelessTicketCache">
	<bean class="org.springframework.security.cas.authentication.EhCacheBasedTicketCache">
	<property name="cache">
		<bean class="net.sf.ehcache.Cache"
			init-method="initialise" destroy-method="dispose">
		<constructor-arg value="casTickets"/>
		<constructor-arg value="50"/>
		<constructor-arg value="true"/>
		<constructor-arg value="false"/>
		<constructor-arg value="3600"/>
		<constructor-arg value="900"/>
		</bean>
	</property>
	</bean>
</property>
</bean>