跳到主要内容

CAS 认证

QWen Max 中英对照 CAS CAS Authentication

概述

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

您可以在此处了解有关 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 有一个简单的方法,该方法返回一个布尔值,表示给定的一组 Credentials 是否有效。你的 AuthenticationHandler 实现需要链接到某种类型的后端认证存储库,例如 LDAP 服务器或数据库。CAS 本身自带了众多 AuthenticationHandler 来帮助实现这一点。当你下载并部署服务器 war 文件时,它被设置为能够成功认证输入的密码与其用户名匹配的用户,这对于测试非常有用。

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

Spring Security 和 CAS 交互序列

浏览器、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,例如: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。它将返回一个 TicketResponseCasAuthenticationProvider,其中包括用户名(必填)、代理列表(如果有涉及)和代理授予票据 IOU(如果请求了代理回调)。

  • 接下来,CasAuthenticationProvider 将调用配置的 CasProxyDeciderCasProxyDecider 指示 TicketResponse 中的代理列表是否被服务接受。Spring Security 提供了几种实现:RejectProxyTicketsAcceptAnyCasProxyNamedCasProxyDecider。这些名称基本上是自解释的,除了 NamedCasProxyDecider,它允许提供一个受信任代理的 List

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

  • 如果没有问题,CasAuthenticationProvider 将构造一个包含 TicketResponse 中详细信息和 GrantedAuthorityCasAuthenticationToken

  • 控制权随后返回到 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://localhost:8443/cas-sample/login/cas"/>
<property name="sendRenew" value="false"/>
</bean>
xml

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>
xml

为了使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>
xml

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

如果参考CAS 如何工作部分,这些 beans 都是相当不言自明的。

这完成了 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>
xml

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 (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>
xml

在使用 SingleSignOutFilter 时,您可能会遇到一些编码问题。因此,建议添加 CharacterEncodingFilter 以确保在使用 SingleSignOutFilter 时字符编码是正确的。有关详细信息,请再次参阅 Apereo CAS 的文档。SingleSignOutHttpSessionListener 确保当 HttpSession 过期时,用于单点登出的映射会被移除。

使用 CAS 对无状态服务进行身份验证

本节描述如何使用 CAS 对服务进行身份验证。换句话说,本节讨论如何设置一个使用通过 CAS 进行身份验证的服务的客户端。下一节将描述如何设置一个无状态服务以使用 CAS 进行身份验证。

配置 CAS 以获取代理授权票据

为了认证到一个无状态服务,应用程序需要获取一个代理授予票据(PGT)。本节描述了如何配置 Spring Security 以基于 cas-st [服务票据认证] 配置来获取 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.apereo.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>
xml

下一步是更新 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>
xml

最后一步是更新 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>
xml

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

现在Spring Security已经获取了PGT,你可以使用它们来创建代理票据(proxy tickets),这些票据可以用来向无状态服务进行身份验证。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");
...
}
java

代理票据认证

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>
xml

下一步是为 CasAuthenticationFilter 指定 servicePropertiesauthenticationDetailsSourceserviceProperties 属性指示 CasAuthenticationFilter 尝试对所有工件进行身份验证,而不仅仅是 filterProcessesUrl 上存在的工件。ServiceAuthenticationDetailsSource 创建一个 ServiceAuthenticationDetails,它确保基于 HttpServletRequest 的当前 URL 在验证票据时用作服务 URL。可以通过注入一个自定义的 AuthenticationDetailsSource 来自定义生成服务 URL 的方法,该源返回一个自定义的 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>
xml

您还需要更新 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>
xml