跳到主要内容
版本:7.0.2

CAS 认证

DeepSeek V3 中英对照 CAS CAS Authentication

概述

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

您可以在 www.apereo.org 了解更多关于 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 有一个简单的方法,用于返回给定的凭据集是否有效的布尔值。您的 AuthenticationHandler 实现需要连接到某种类型的后端认证存储库,例如 LDAP 服务器或数据库。CAS 本身内置了多种 AuthenticationHandler 来协助完成此任务。当您下载并部署服务器 war 文件时,它默认设置为成功认证那些输入密码与用户名匹配的用户,这对于测试非常有用。

除了CAS服务器本身,其他关键角色当然是在整个企业内部署的安全Web应用程序。这些Web应用程序被称为"服务"。服务分为三种类型:验证服务票据的服务、能够获取代理票据的服务,以及验证代理票据的服务。验证代理票据有所不同,因为必须验证代理列表,并且代理票据通常可以重复使用。

Spring Security 与 CAS 交互时序

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

  • 用户正在浏览服务的公开页面。此时尚未涉及 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。其主体将等于 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,其中包含 TicketResponse 中的详细信息以及至少包含 FACTOR_BEARER 的一组 GrantedAuthority

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

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

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

CAS 客户端的配置

CAS的Web应用端因Spring Security而变得简单。假设您已经掌握了使用Spring Security的基础知识,因此下文不再赘述这些内容。我们将假设使用基于命名空间的配置,并根据需要添加CAS相关的Bean。每一节的内容都建立在前一节的基础上。完整的CAS示例应用程序可以在Spring Security的示例中找到。

服务票据认证

本节介绍如何配置 Spring Security 以验证服务票据(Service Tickets)。通常,这是 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 实例来加载用户的权限,一旦用户通过 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://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 (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)。本节将介绍如何在基于ncas-st[服务票据认证]配置的基础上,配置Spring Security以获取PGT。

第一步是在你的Spring Security配置中包含一个ProxyGrantingTicketStorage。它用于存储由CasAuthenticationFilter获取的PGT,以便这些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 中。重要的是 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>

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

既然 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");
...
}

代理票据认证

CasAuthenticationProvider 区分有状态客户端和无状态客户端。任何向 CasAuthenticationFilterfilterProcessesUrl 提交请求的客户端都被视为有状态客户端。任何在非 filterProcessesUrl 的 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指定servicePropertiesauthenticationDetailsSourceserviceProperties属性指示CasAuthenticationFilter尝试对所有凭证进行认证,而不仅仅是存在于filterProcessesUrl上的凭证。ServiceAuthenticationDetailsSource会创建一个ServiceAuthenticationDetails,确保在验证票据时,基于HttpServletRequest的当前URL被用作服务URL。生成服务URL的方法可以通过注入一个自定义的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.SpringCacheBasedTicketCache">
<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>