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 中指定一个 AuthenticationHandlerAuthenticationHandler 有一个简单的方法,它返回一个布尔值,指示给定的 Credentials(凭据)集是否有效。您的 AuthenticationHandler 实现需要链接到某种后端认证存储库,例如 LDAP 服务器或数据库。CAS 本身内置了许多 AuthenticationHandler 以协助完成此任务。当您下载并部署服务器 war 文件时,它被配置为成功认证输入密码与其用户名匹配的用户,这对于测试很有用。

除了 CAS 服务器本身,其他关键角色当然是您企业中部署的安全 Web 应用程序。这些 Web 应用程序被称为“服务”。服务有三种类型。认证服务票据(service tickets)的服务,能够获取代理票据(proxy tickets)的服务,以及认证代理票据的服务。认证代理票据有所不同,因为必须验证代理列表,而且代理票据通常可以重复使用。

Spring Security 与 CAS 交互序列

Web 浏览器、CAS 服务器和由 Spring Security 保护的服务之间的基本交互如下所示:

  • Web 用户正在浏览服务的公共页面。CAS 或 Spring Security 不参与其中。

  • 用户最终请求一个安全页面或其使用的 Bean 之一是安全的。Spring Security 的 ExceptionTranslationFilter 将检测到 AccessDeniedExceptionAuthenticationException

  • 由于用户的 Authentication 对象(或缺少该对象)导致了 AuthenticationExceptionExceptionTranslationFilter 将调用配置的 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。principal(主体)将等于 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而 credentials(凭据)将是服务票据的不透明值。然后,此认证请求将交给配置的 AuthenticationManager

  • AuthenticationManager 实现将是 ProviderManager,它又配置了 CasAuthenticationProviderCasAuthenticationProvider 只响应包含 CAS 特定 principal(例如 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)的 UsernamePasswordAuthenticationTokenCasAuthenticationToken(稍后讨论)。

  • CasAuthenticationProvider 将使用 TicketValidator 实现验证服务票据。这通常是 Cas20ServiceTicketValidator,它是 CAS 客户端库中包含的类之一。如果应用程序需要验证代理票据,则使用 Cas20ProxyTicketValidatorTicketValidator 向 CAS 服务器发送 HTTPS 请求以验证服务票据。它也可能包含一个代理回调 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,其中包括 TicketResponse 中包含的详细信息和 GrantedAuthority

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

  • 用户的浏览器被重定向回导致 AuthenticationException 的原始页面(或取决于配置的自定义目标)。

很高兴您还在!现在我们来看看如何配置。

CAS 客户端配置

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

Service Ticket 认证

本节描述如何设置 Spring Security 以认证 Service Ticket。通常,这已经是 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 工作,ExceptionTranslationFilterauthenticationEntryPoint 属性必须设置为 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 实例来加载用户的权限(authorities),一旦他们通过 CAS 认证。这里我们展示了一个简单的内存配置。请注意,CasAuthenticationProvider 实际上不使用密码进行认证,但它会使用权限。

如果您回顾 CAS 工作原理 部分,这些 Bean 都相当容易理解。

这完成了 CAS 最基本的配置。如果您没有犯任何错误,您的 Web 应用程序应该可以在 CAS 单点登录框架内正常工作。Spring Security 的其他部分无需关心 CAS 处理了认证的事实。在以下部分,我们将讨论一些(可选的)更高级的配置。

单点注销

CAS 协议支持单点注销 (Single Logout),并且可以轻松添加到您的 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 过滤器将允许请求 URL /spring_security_cas_logout 以将应用程序重定向到配置的 CAS Server 注销 URL。然后,CAS Server 将向所有已登录的服务发送单点注销请求。singleLogoutFilter 通过在静态 Map 中查找 HttpSession,然后使其失效来处理单点注销请求。

可能会令人困惑的是为什么同时需要 logout 元素和 singleLogoutFilter。最好先在本地注销,因为 SingleSignOutFilter 只将 HttpSession 存储在一个静态 Map 中,以便对其调用 invalidate。使用上述配置,注销流程将是:

  • 用户请求 /logout,这将使该用户从本地应用程序中注销,并将用户发送到注销成功页面。

  • 注销成功页面 /cas-logout.jsp 应指导用户单击指向 /logout/cas 的链接,以注销所有应用程序。

  • 当用户单击链接时,用户将被重定向到 CAS 单点注销 URL (localhost:9443/cas/logout)。

  • 在 CAS Server 端,CAS 单点注销 URL 然后向所有 CAS 服务提交单点注销请求。在 CAS Service 端,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 以获取代理授权票据 (Proxy Granting Ticket)

为了向无状态服务进行认证,应用程序需要获取一个代理授权票据 (PGT)。本节描述如何在 Spring Security 中配置以获取 PGT,并以上一节的 CAS Service Ticket 认证配置为基础。

第一步是在您的 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://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 中。重要的是 proxyReceptorUrlCas20ProxyTicketValidatorproxyCallbackUrl 匹配。配置示例如下所示。

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

使用代理票据 (Proxy Ticket) 调用无状态服务

现在 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")
}

Proxy Ticket 认证

CasAuthenticationProvider 区分有状态和无状态客户端。有状态客户端被认为是提交到 CasAuthenticationFilterfilterProcessesUrl 的任何客户端。无状态客户端是向 CasAuthenticationFilter 在非 filterProcessesUrl 的 URL 上提交认证请求的任何客户端。

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

一个显而易见的选择是完全不使用 CAS 进行远程调用协议客户端。然而,这将消除 CAS 的许多可取功能。作为折中方案,CasAuthenticationProvider 使用了一个 StatelessTicketCache。这仅用于使用等于 CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER 的 principal 的无状态客户端。发生的情况是 CasAuthenticationProvider 将 resulting 的 CasAuthenticationToken 存储在 StatelessTicketCache 中,以代理票据为键。因此,远程调用协议客户端可以呈现相同的代理票据,并且 CasAuthenticationProvider 无需联系 CAS 服务器进行验证(除了第一个请求)。一旦认证成功,代理票据可以用于原始目标服务之外的 URL。

本节在前面的部分的基础上构建,以适应代理票据认证。第一步是指定认证所有 artifact,如下所示。

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

下一步是为 CasAuthenticationFilter 指定 servicePropertiesauthenticationDetailsSourceserviceProperties 属性指示 CasAuthenticationFilter 尝试认证所有 artifact,而不仅仅是出现在 filterProcessesUrl 上的 artifact。ServiceAuthenticationDetailsSource 创建一个 ServiceAuthenticationDetails,该对象确保基于 HttpServletRequest 的当前 URL 用作验证票据时的服务 URL。生成服务 URL 的方法可以通过注入自定义的 AuthenticationDetailsSource 来自定义,该 AuthenticationDetailsSource 返回自定义的 ServiceAuthenticationDetails

<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://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>