CAS 认证

概述

JA-SIG 开发了一个名为 CAS 的企业级单点登录系统。与其他项目不同,JA-SIG 的中央认证服务是开源的,使用广泛,易于理解,平台无关,并支持代理功能。Spring Security 完全支持 CAS,并提供从 Spring Security 的单应用程序部署到由企业级 CAS 服务器保护的多应用程序部署的轻松迁移路径。

您可以在 www.apereo.org 上了解更多关于 CAS 的信息。您还需要访问此网站下载 CAS 服务器文件。

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` 有一个简单的方法,它返回一个布尔值,表示给定的凭据集是否有效。您的 `AuthenticationHandler` 实现需要链接到某种类型的后端身份验证存储库,例如 LDAP 服务器或数据库。CAS 本身包含许多开箱即用的 `AuthenticationHandler` 来帮助您完成此操作。当您下载并部署服务器 war 文件时,它被设置为成功验证输入与用户名匹配的密码的用户,这对于测试很有用。

除了 CAS 服务器本身之外,其他关键参与者当然是在整个企业中部署的安全 Web 应用程序。这些 Web 应用程序被称为“服务”。服务有三种类型。那些验证服务票证的,那些可以获取代理票证的,以及那些验证代理票证的。验证代理票证有所不同,因为必须验证代理列表,并且代理票证通常可以重复使用。

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 可能是 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 可能是 server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ

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

  • AuthenticationManager 实现将是 ProviderManager,它反过来配置了 CasAuthenticationProviderCasAuthenticationProvider 仅响应包含 CAS 特定主体(如 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)的 UsernamePasswordAuthenticationTokenCasAuthenticationToken(稍后讨论)。

  • CasAuthenticationProvider 将使用 TicketValidator 实现来验证服务票据。这通常是一个 Cas20ServiceTicketValidator,它是 CAS 客户端库中包含的类之一。如果应用程序需要验证代理票据,则使用 Cas20ProxyTicketValidatorTicketValidator 向 CAS 服务器发送 HTTPS 请求以验证服务票据。它还可以包含一个代理回调 URL,该 URL 包含在本示例中: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 应用程序。例如,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 将构建一个 CasAuthenticationToken,其中包含 TicketResponseGrantedAuthority 中的详细信息。

  • 然后控制权返回到 CasAuthenticationFilter,它将创建的 CasAuthenticationToken 放入安全上下文。

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

很高兴你还在!现在让我们看看如何配置它。

CAS 客户端配置

由于 Spring Security,CAS 的 Web 应用程序端变得非常容易。假设您已经了解 Spring Security 的基本知识,因此以下不再介绍这些知识。我们将假设使用基于命名空间的配置,并根据需要添加 CAS bean。每个部分都建立在前面的部分之上。完整的 CAS 示例应用程序可以在 Spring Security 示例 中找到。

服务票证身份验证

本节介绍如何设置 Spring Security 以验证服务票证。通常情况下,这正是 Web 应用程序所需的一切。您需要在应用程序上下文中添加一个 ServiceProperties bean。它代表您的 CAS 服务。

<bean id="serviceProperties"
	class="org.springframework.security.cas.ServiceProperties">
<property name="service"
	value="https://127.0.0.1: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://127.0.0.1:9443/cas/login"/>
<property name="serviceProperties" ref="serviceProperties"/>
</bean>

为了使 CAS 正常运行,ExceptionTranslationFilter 必须将其 authenticationEntryPoint 属性设置为 CasAuthenticationEntryPoint bean。这可以通过使用 entry-point-ref 很容易地完成,如上面的示例所示。CasAuthenticationEntryPoint 必须引用 ServiceProperties 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://127.0.0.1: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 处理身份验证的事实。在接下来的部分中,我们将讨论一些(可选的)更高级的配置。

单点注销

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://127.0.0.1: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 (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 过期时,用于单点注销的映射将被删除。

使用 CAS 认证无状态服务

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

配置 CAS 以获取代理授予票证

为了认证无状态服务,应用程序需要获取代理授予票证 (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 以便能够获取代理票证。为此,请将 Cas20ServiceTicketValidator 替换为 Cas20ProxyTicketValidatorproxyCallbackUrl 应设置为应用程序将接收 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://127.0.0.1:9443/cas"/>
		<property name="proxyCallbackUrl"
		value="https://127.0.0.1:8443/cas-sample/login/cas/proxyreceptor"/>
	<property name="proxyGrantingTicketStorage" ref="pgtStorage"/>
	</bean>
</property>
</bean>

最后一步是更新 CasAuthenticationFilter 以接受 PGT 并将其存储在 ProxyGrantingTicketStorage 中。重要的是 proxyReceptorUrlCas20ProxyTicketValidatorproxyCallbackUrl 相匹配。下面显示了一个示例配置。

<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 中包含一个工作示例。示例代码如下所示

  • 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")
}

代理票据身份验证

CasAuthenticationProvider 区分有状态和无状态客户端。有状态客户端是指任何提交到 CasAuthenticationFilterfilterProcessesUrl 的客户端。无状态客户端是指任何在 CasAuthenticationFilter 上的 URL(非 filterProcessesUrl)上呈现身份验证请求的客户端。

由于远程协议无法在 HttpSession 的上下文中呈现自身,因此无法依赖在请求之间将安全上下文存储在会话中的默认做法。此外,由于 CAS 服务器在 TicketValidator 验证票据后会将其失效,因此在后续请求中呈现相同的代理票据将不起作用。

一个显而易见的选项是根本不为远程协议客户端使用 CAS。但是,这将消除 CAS 的许多理想功能。作为折衷方案,CasAuthenticationProvider 使用 StatelessTicketCache。这仅用于使用与 CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER 相等的 principal 的无状态客户端。发生的情况是 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,它确保在验证票证时使用基于 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 以处理代理票证。为此,请将 Cas20ServiceTicketValidator 替换为 Cas20ProxyTicketValidator。您需要配置 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://127.0.0.1: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>