跳到主要内容
版本:6.4.3

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

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

Spring Security 和 CAS 交互序列​

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

  • 网络用户正在浏览服务的公共页面。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,它又配置了 CasAuthenticationProvider。CasAuthenticationProvider 仅响应包含 CAS 特定主体(如 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)的 UsernamePasswordAuthenticationToken 和 CasAuthenticationToken(稍后讨论)。

  • CasAuthenticationProvider 将使用 TicketValidator 实现来验证服务票据。这通常是一个 Cas20ServiceTicketValidator,它是 CAS 客户端库中包含的一个类。如果应用程序需要验证代理票据,则使用 Cas20ProxyTicketValidator。TicketValidator 会向 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。它将返回一个 TicketResponse 给 CasAuthenticationProvider,其中包括用户名(必填)、代理列表(如果有涉及)和代理授予票据 IOU(如果请求了代理回调)。

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

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

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

  • 控制权随后返回到 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>

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 实际上并不使用密码进行身份验证,但它确实使用了权限。

如果参考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>

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>

在使用 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"/>

下一步是更新 CasAuthenticationProvider 以能够获取代理票据。为此,将 Cas20ServiceTicketValidator 替换为 Cas20ProxyTicketValidator。proxyCallbackUrl 应设置为应用程序将接收 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 必须与 Cas20ProxyTicketValidator 的 proxyCallbackUrl 匹配。下面是一个示例配置。

<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,你可以使用它们来创建代理票据(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");
...
}

代理票据认证​

CasAuthenticationProvider 区分有状态和无状态的客户端。有状态的客户端是指任何向 CasAuthenticationFilter 的 filterProcessesUrl 提交请求的客户端。无状态的客户端是指在 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 指定 serviceProperties 和 authenticationDetailsSource。serviceProperties 属性指示 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>

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