CAS Authentication

Overview

JA-SIG 生成一个企业范围的单点登录系统,称为 CAS。与其他计划不同,JA-SIG 的中央认证服务是开源的、被广泛使用、容易理解、与平台无关且支持代理功能。Spring Security 完全支持 CAS,并提供一条从 Spring Security 的单一应用程序部署到受企业级 CAS 服务器保护的多应用程序部署的轻松迁移路径。

您可以在 [role="bare"][role="bare"]https://www.apereo.org 中了解有关 CAS 的更多信息。您还需要访问该网站来下载 CAS 服务器文件。

How CAS Works

CAS 网站包含详细介绍 CAS 架构的文档,但我们在此 Spring Security 的上下文中再次介绍其概述。Spring Security 3.x 支持 CAS 3。撰写本文时,CAS 服务器的版本为 3.4。

您需要在企业中的某个位置设置 CAS 服务器。CAS 服务器只是一个标准 WAR 文件,因此设置服务器没有任何困难。在 WAR 文件内部,您可以自定义登录和其他显示给用户的单点登录页面。

部署 CAS 3.4 服务器时,您还需在 CAS 附带的 deployerConfigContext.xml 中指定一个 AuthenticationHandlerAuthenticationHandler 有一个简单的方法,可返回布尔值来指示给定的一组凭据是否有效。您的 AuthenticationHandler 实现需要链接到某种后端身份验证信息库,例如 LDAP 服务器或数据库。CAS 本身包含很多开箱即用的 AuthenticationHandler 来帮助实现此操作。下载并部署服务器 war 文件后,会将其设置为成功验证输入的密码与相应用户名匹配的用户,这在进行测试时很有用。

除了 CAS 服务器本身,其他主要参与者当然是企业中部署的安全的 Web 应用程序。这些 Web 应用程序称为“服务”。服务有三种类型:身份验证服务票证、可获取代理票证以及身份验证代理票证。验证代理票证有所不同,因为必须验证代理列表,而且通常可以重复利用代理票证。

Spring Security and CAS Interaction Sequence

Web 浏览器、CAS 服务器和受 Spring Security 保护的服务之间的基本交互过程如下:

  • Web 用户正在浏览服务的公共页面。CAS 或 Spring Security 没有任何参与。

  • 最终,用户请求一个安全的页面,或者它使用的 bean 之一也是安全的。Spring Security 的 ExceptionTranslationFilter 会检测 AccessDeniedExceptionAuthenticationException

  • 由于用户的 Authentication 对象(或其缺少)导致 AuthenticationException ,因此 ExceptionTranslationFilter 会调用已配置的 AuthenticationEntryPoint 。如果使用 CAS,这将是 CasAuthenticationEntryPoint 类。

  • CasAuthenticationEntryPoint 会将用户的浏览器重定向至 CAS 服务器。它还将指示 service 参数,即 Spring Security 服务(您的应用程序)的回调 URL。例如,浏览器重定向的 URL 可能为 [role="bare"][role="bare"]https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas

  • 在用户的浏览器重定向到 CAS 之后,他们将被提示输入用户名和密码。如果用户出示了一个会话 cookie,表示其之前已经登录,则不会再次提示他们登录(此步骤有一个例外,我们稍后会介绍)。CAS 将使用上面讨论的 PasswordHandler (或如果使用 CAS 3.0 则 AuthenticationHandler )来决定用户名和密码是否有效。

  • 登录成功后,CAS 会将用户的浏览器重定向回原来的服务。它还将包括 ticket 参数,即一个代表“服务票证”的不透明字符串。继续之前的示例,浏览器重定向的 URL 可能是 [role="bare"][role="bare"]https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ

  • 回到服务 Web 应用程序, CasAuthenticationFilter 始终侦听对 /login/cas 的请求(这是可配置的,但我们将在本简介中使用默认值)。处理过滤器将构造一个表示服务票证的 UsernamePasswordAuthenticationToken 。主体将等于 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER ,凭据将为服务票证不透明值。然后此身份验证请求将被提交到配置的 AuthenticationManager

  • AuthenticationManager 实现将是 ProviderManager ,而 ProviderManager 又使用 CasAuthenticationProvider 配置。 CasAuthenticationProvider 仅响应 UsernamePasswordAuthenticationToken`s containing the CAS-specific principal (such as `CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER )和 CasAuthenticationToken (稍后讨论)。

  • CasAuthenticationProvider 将使用 TicketValidator 实现来验证服务票证。这通常为 Cas20ServiceTicketValidator ,它是由 CAS 客户端库中包含的一个类。在应用程序需要验证代理票证的情况下,将会使用 Cas20ProxyTicketValidatorTicketValidator 向 CAS 服务器发出 HTTPS 请求,以便验证服务票证。它还可能包括代理回调 URL,其中包含此示例:[role="bare"][role="bare"]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 应用程序发送代理票证授予。例如, [role="bare"][role="bare"]https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH

  • Cas20TicketValidator 将解析从 CAS 服务器接收到的 XML。它将返回到 CasAuthenticationProvider 一个 TicketResponse ,其中包括用户名(必填)、代理列表(如果参与)、以及代理票证授予的 IOU(如果请求代理回调)。

  • 接下来, CasAuthenticationProvider 将调用一个已配置的 CasProxyDeciderCasProxyDecider 指示 TicketResponse 中的代理列表是否可被服务接受。Spring Security 提供了多个实现: RejectProxyTicketsAcceptAnyCasProxyNamedCasProxyDecider 。这些名字大部分都是不言自明的,除了 NamedCasProxyDecider ,它允许提供 List 的受信任代理。

  • 接下来 CasAuthenticationProvider 将请求 AuthenticationUserDetailsService 来加载适用于 Assertion 中包含用户的 GrantedAuthority 对象。

  • 如果没有问题, CasAuthenticationProvider 会使用 TicketResponseGrantedAuthority 中包含的详细信息构造 CasAuthenticationToken

  • 之后控件返回到 CasAuthenticationFilter ,它将创建的 CasAuthenticationToken 放置在安全上下文中。

  • 用户的浏览器被重定向到导致 AuthenticationException 的原始页面(或根据配置的不同而重定向到自定义目标)。

很高兴您还在关注!让我们现在了解如何进行此配置

Configuration of CAS Client

由于 Spring Security,CAS 的 Web 应用程序端变得简单。我们认为您已经知道如何使用 Spring Security 的基础知识,因此下面不再涉及这些知识。我们将假设使用基于命名空间的配置并在需要时添加 CAS Bean。每个部分都构建在前一部分的基础上。可在 Spring Security Samples中找到一个完整的 CAS 示例应用程序。

Service Ticket Authentication

本部分介绍如何设置 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 必须等于 CasAuthenticationFilter 将要监视的 URL。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.apereo.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 实际上并不使用密码进行身份验证,但它确实会使用权限。

如果您参阅 How CAS Works 部分,您会发现所有的 bean 都解释得相当清楚。

至此,CAS 的最基本配置已经完成。如果您没有犯任何错误,那么您的 Web 应用程序应该乐于在 CAS 单点登录的框架内工作。Spring Security 的其他部分都无需关注 CAS 已处理的身份验证。以下几部分,我们将讨论一些(可选的)更高级的配置。

Single Logout

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.apereo.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 中以便调用 invalidate,因此最好先在本地注销。通过上述配置,注销流程如下:

  • 用户请求 /logout ,它会将用户注销本地应用程序,然后将用户发送到注销成功页面。

  • 注销成功的页面 /cas-logout.jsp 应指示用户点击指向 /logout/cas 的链接来注销所有应用程序。

  • 当用户点击链接时,用户将被重定向到 CAS 单一注销 URL ( [role="bare"][role="bare"]https://localhost:9443/cas/logout )。

  • 在 CAS 服务器端,CAS 单一注销 URL 然后提交单一注销请求到所有 CAS 服务。在 CAS 服务端,Apereo 的 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.apereo.cas.client.session.SingleSignOutHttpSessionListener
</listener-class>
</listener>

在使用 SingleSignOutFilter 时, 您可能会遇到一些编码问题。因此, 建议添加 CharacterEncodingFilter 以确保在使用 SingleSignOutFilter 时字符编码正确。同样, 请参阅 Apereo CAS 的文档以获取详细信息。SingleSignOutHttpSessionListener 确保在 HttpSession 过期时, 用于单点注销的映射已删除。

Authenticating to a Stateless Service with CAS

本节介绍如何使用 CAS 身份验证服务。换句话说, 本节讨论如何设置使用 CAS 身份验证服务的客户端。下一节介绍如何设置使用 CAS 身份验证的无状态服务。

Configuring CAS to Obtain Proxy Granting Tickets

为了向无状态服务进行身份验证,应用程序需要获取代理授予票证 (PGT)。本节介绍如何配置 Spring Security 以获取 PGT,它建立在 thencas-st[服务票证身份验证] 配置的基础之上。

第一步是在您的 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.apereo.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>

下一步是更新 CasAuthenticationProvider 以便能够获取代理票证。要执行此操作, 请用 Cas20ProxyTicketValidator 替换 Cas20ServiceTicketValidatorproxyCallbackUrl 应设置为应用程序将在其中收到 PGT 的 URL。最后, 该配置还应引用 ProxyGrantingTicketStorage , 这样它才能使用 PGT 来获取代理票证。您可以在下面找到应进行的配置更改示例。

<bean id="casAuthenticationProvider"
	class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
	<bean class="org.apereo.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 匹配 Cas20ProxyTicketValidatorproxyCallbackUrl 很重要。示例配置如下所示。

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

Calling a Stateless Service Using a Proxy Ticket

现在,Spring Security 已获得 PGT,您可以使用它们来创建代理票证,这些票证可用于向无状态服务进行身份验证。CAS sample application包含 `ProxyTicketSampleServlet`中的一个工作示例。示例代码如下:

  • Java

  • Kotlin

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");
...
}
protected fun doGet(request: HttpServletRequest, response: HttpServletResponse?) {
    // NOTE: The CasAuthenticationToken can also be obtained using
    // SecurityContextHolder.getContext().getAuthentication()
    val token = request.userPrincipal as CasAuthenticationToken
    // proxyTicket could be reused to make calls to the CAS service even if the
    // target url differs
    val proxyTicket = token.assertion.principal.getProxyTicketFor(targetUrl)

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

Proxy Ticket Authentication

CasAuthenticationProvider 区分有状态和无状态客户端。有状态客户端被认为是任何提交给 CasAuthenticationFilterfilterProcessesUrl 的客户端。无状态客户端是指在 CasAuthenticationFilter 上提交身份验证请求且 URL 并非 filterProcessesUrl 的任何客户端。

因为远程调用协议无法在 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 指定 servicePropertiesauthenticationDetailsSourceserviceProperties 属性指示 CasAuthenticationFilter 尝试对所有构件进行身份验证, 而不只是 filterProcessesUrl 上存在的构件。ServiceAuthenticationDetailsSource 创建 ServiceAuthenticationDetails , 该 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.apereo.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>