# CAS 认证 ## 概述 JA-SIG 公司生产一种 Enterprise 范围内的单点登录系统,称为 CAS。与其他计划不同,JA-SIG 的中央身份验证服务是开源的,广泛使用,易于理解,独立于平台,并支持代理功能。 Spring 安全性完全支持 CAS,并提供了一种从 Spring 安全性的单应用程序部署到由 Enterprise 范围的 CAS 服务器保护的多应用程序部署的简单迁移路径。 你可以在[https://www.apereo.org](https://www.apereo.org)上了解有关 CAS 的更多信息。你还需要访问此站点来下载 CAS 服务器文件。 ## CAS 的工作原理 虽然 CAS Web 站点包含详细介绍 CAS 体系结构的文档,但我们在 Spring 安全性的上下文中再次介绍一般概述。 Spring Security3.x 支持 Cas3。在编写本文时,CAS 服务器处于 3.4 版本。 在你的 Enterprise 的某个地方,你将需要设置一个 CAS 服务器。CAS 服务器只是一个标准的 WAR 文件,因此设置服务器没有什么困难。在 WAR 文件中,你将在显示给用户的页面上自定义登录和其他单个签名。 部署 CAS3.4 服务器时,还需要在 CAS 包含的`deployerConfigContext.xml`中指定`AuthenticationHandler`。`AuthenticationHandler`有一个简单的方法,该方法返回一个关于给定的凭据集是否有效的布尔值。你的`AuthenticationHandler`实现将需要链接到某种类型的后端身份验证存储库,例如 LDAP 服务器或数据库。CAS 本身包括许多开箱即用的`AuthenticationHandler`s,以帮助实现这一点。当你下载和部署服务器 WAR 文件时,设置它可以成功地对输入与其用户名匹配的密码的用户进行身份验证,这对于测试非常有用。 除了 CAS 服务器本身,其他关键的参与者当然是部署在整个 Enterprise 中的安全 Web 应用程序。这些 Web 应用程序被称为“服务”。有三种类型的服务。对服务票进行身份验证的,可以获得代理票的,以及对代理票进行身份验证的。对代理票据进行身份验证是不同的,因为必须对代理票据列表进行验证,并且通常情况下代理票据可以重复使用。 ### Spring 安全性与 CAS 交互序列 Web 浏览器、CAS 服务器和 Spring 安全保护服务之间的基本交互如下: * 网络用户正在浏览该服务的公共页面。不涉及 CAS 或 Spring 安全性。 * 用户最终会请求一个安全的页面,或者它使用的某个 bean 是安全的。 Spring 安全性的`ExceptionTranslationFilter`将检测到`AccessDeniedException`或`AuthenticationException`。 * 因为用户的`Authentication`对象(或缺少它)导致了`AuthenticationException`,所以`ExceptionTranslationFilter`将调用已配置的`AuthenticationEntryPoint`。如果使用 CAS,这将是`CasAuthenticationEntryPoint`类。 * `CasAuthenticationEntryPoint`将把用户的浏览器重定向到 CAS 服务器。它还将指示一个`service`参数,这是 Spring 安全服务(你的应用程序)的回调 URL。例如,浏览器被重定向到的 URL 可能是[https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas](https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas)。 * 用户的浏览器重定向到 CAS 后,会提示他们输入用户名和密码。如果用户提供了一个会话cookie,表明他们以前已经登录过,那么将不会提示他们再次登录(这个过程有一个例外,我们将在后面进行讨论)。CAS 将使用上面讨论的`PasswordHandler`(如果使用 CAS3.0,则使用`AuthenticationHandler`)来决定用户名和密码是否有效。 * 成功登录后,CAS 将把用户的浏览器重定向到原始服务。它还将包括一个`ticket`参数,这是一个表示“服务票据”的不透明字符串。继续前面的示例,浏览器被重定向到的 URL 可能是[https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ](https://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 特定主体的`UsernamePasswordAuthenticationToken`s(例如`CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER`)和`CasAuthenticationToken`s(稍后讨论)。 * `CasAuthenticationProvider`将使用`TicketValidator`实现来验证服务票。这通常是`Cas20ServiceTicketValidator`,它是 CAS 客户库中包含的类之一。如果应用程序需要验证代理票,则使用`Cas20ProxyTicketValidator`。`TicketValidator`向 CAS 服务器发出 HTTPS 请求,以验证服务票据。它还可以包括一个代理回调 URL,它包含在这个示例中:[https://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](https://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`表示代理授予票据借据。然后,CAS 服务器将创建自己的 HTTPS 连接,返回到`pgtUrl`。这是为了相互验证 CAS 服务器和声明的服务 URL。HTTPS 连接将用于向原始 Web 应用程序发送代理授权票。例如,[https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH](https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH)。 * `Cas20TicketValidator`将解析从 CAS 服务器接收的 XML。它将返回`CasAuthenticationProvider`a`TicketResponse`,其中包括用户名(强制使用)、代理列表(如果涉及任何代理)和代理授予票 iou(如果请求代理回调)。 * 接下来`CasAuthenticationProvider`将调用已配置的`CasProxyDecider`。`CasProxyDecider`指示`TicketResponse`中的代理列表是否为服务所接受。 Spring 安全性提供了几种实现方式:`RejectProxyTickets`、`AcceptAnyCasProxy`和`NamedCasProxyDecider`。除了`NamedCasProxyDecider`允许提供`List`的可信代理之外,这些名称基本上是不言自明的。 * `CasAuthenticationProvider`下一步将请求一个`AuthenticationUserDetailsService`来加载`GrantedAuthority`对象,该对象应用于`Assertion`中包含的用户。 * 如果没有问题,`CasAuthenticationProvider`构造一个`CasAuthenticationToken`,包括`TicketResponse`和`GrantedAuthority`s 中包含的细节。 * 控件然后返回`CasAuthenticationFilter`,这将创建的`CasAuthenticationToken`置于安全上下文中。 * 用户的浏览器被重定向到导致`AuthenticationException`的原始页面(或根据配置自定义的目标)。 你还在这儿真好!现在让我们来看一下如何配置 ## CAS 客户端的配置 Spring 由于安全性,CAS 的 Web 应用程序端变得很容易。假设你已经了解了使用 Spring 安全性的基本知识,因此下面不再讨论这些内容。我们将假设正在使用基于名称空间的配置,并根据需要添加 CAS bean。每一节都是在前一节的基础上发展起来的。完整的 CAS 示例应用程序可以在 Spring Security[Samples](../../samples.html#samples)中找到。 ### 服务票认证 本节描述如何设置 Spring 安全性以对服务票据进行身份验证。通常情况下,这就是 Web 应用程序所需要的。你将需要在应用程序上下文中添加`ServiceProperties` Bean。这表示你的 CAS 服务: ``` ``` `service`必须等于将由`CasAuthenticationFilter`监视的 URL。`sendRenew`默认为 false,但如果你的应用程序特别敏感,则应将其设置为 true。这个参数的作用是告诉 CAS 登录服务,登录时的单一签名是不可接受的。相反,用户将需要重新输入他们的用户名和密码,以便获得对该服务的访问权限。 应该配置以下 bean 以启动 CAS 身份验证过程(假设你使用的是名称空间配置): ``` ... ``` 对于 CAS 操作,`ExceptionTranslationFilter`必须将其`authenticationEntryPoint`属性设置为`CasAuthenticationEntryPoint` Bean。使用[入口点-参考](../appendix/namespace/http.html#nsa-http-entry-point-ref)就可以很容易地做到这一点,就像上面示例中所做的那样。`CasAuthenticationEntryPoint`必须指`ServiceProperties` Bean(在上面讨论过),它为 Enterprise 的 CAS 登录服务器提供 URL。这就是用户的浏览器将被重定向的地方。 `CasAuthenticationFilter`具有与`UsernamePasswordAuthenticationFilter`(用于基于表单的登录)非常相似的属性。你可以使用这些属性来自定义一些事情,比如验证成功和失败的行为。 接下来,你需要添加一个`CasAuthenticationProvider`及其协作者: ``` ... ``` `CasAuthenticationProvider`使用`UserDetailsService`实例为用户加载权限,一旦这些权限已被 CAS 认证。我们在这里展示了一个简单的内存设置。请注意,`CasAuthenticationProvider`实际上并不使用密码进行身份验证,但它确实使用了授权。 如果你回顾[CAS 的工作原理](#cas-how-it-works)部分,那么这些 bean 都是合理的不言自明的。 这就完成了 CAS 的最基本配置。如果你没有犯任何错误,那么你的 Web 应用程序应该在 CAS 单点登录的框架内愉快地工作。 Spring 安全性的任何其他部分都不需要关注 CAS 处理身份验证的事实。在下面的小节中,我们将讨论一些(可选的)更高级的配置。 ### 单次注销 CAS 协议支持单注销,并且可以很容易地添加到你的 Spring 安全配置中。下面是处理单注销的 Spring 安全配置的更新 ``` ... ``` `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([https://localhost:9443/cas/logout](https://localhost:9443/cas/logout))。 * 在 CAS 服务器端,CAS 单注销 URL 然后向所有 CAS 服务提交单注销请求。在 CAS 服务端,Jasig 的`SingleSignOutFilter`通过使原始会话无效来处理注销请求。 下一步是将以下内容添加到 web.xml 中 ``` characterEncodingFilter org.springframework.web.filter.CharacterEncodingFilter encoding UTF-8 characterEncodingFilter /* org.jasig.cas.client.session.SingleSignOutHttpSessionListener ``` 当使用 SingleSignoutFilter 时,你可能会遇到一些编码问题。因此,建议添加`CharacterEncodingFilter`,以确保在使用`SingleSignOutFilter`时字符编码是正确的。同样,请参考 Jasig 的文档了解详细信息。`SingleSignOutHttpSessionListener`确保当`HttpSession`过期时,用于单注销的映射将被删除。 ### 使用 CAS 对无状态服务进行身份验证 本节描述如何使用 CAS 对服务进行身份验证。换句话说,本节将讨论如何设置一个使用 CAS 进行身份验证的服务的客户机。下一节介绍如何设置无状态服务以使用 CAS 进行身份验证。 #### 配置 CAS 以获得代理授予票 为了对无状态服务进行身份验证,应用程序需要获得代理授予票。本节描述如何配置 Spring 安全性,以便在 cas-st[Service Ticket Authentication]配置的基础上获得 PGT 构建。 第一步是在 Spring 安全性配置中包含`ProxyGrantingTicketStorage`。这用于存储由`CasAuthenticationFilter`获得的 PGT’s,以便它们可以用于获得代理票。下面显示了一个示例配置。 ``` ``` 下一步是更新`CasAuthenticationProvider`,以便能够获得代理票。为此,将`Cas20ServiceTicketValidator`替换为`Cas20ProxyTicketValidator`。应该将`proxyCallbackUrl`设置为应用程序将在某个位置接收 PGT 的 URL。最后,配置还应该引用`ProxyGrantingTicketStorage`,以便它可以使用 PGT 来获取代理票。你可以在下面找到应该进行的配置更改的示例。 ``` ... ``` 最后一步是更新`CasAuthenticationFilter`以接受 PGT 并将其存储在`ProxyGrantingTicketStorage`中。重要的是`proxyReceptorUrl`与`Cas20ProxyTicketValidator`的`proxyCallbackUrl`匹配。下面显示了一个配置示例。 ``` ... ``` #### 使用代理票据调用无状态服务 既然 Spring Security 获得了 PGTs,那么你就可以使用它们来创建代理票,该代理票可用于对无状态服务进行身份验证。cas[示例应用程序](../../samples.html#samples)在`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"); ... } ``` Kotlin ``` 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`区分了有状态客户机和无状态客户机。有状态客户机被认为是任何提交到`CasAuthenticationFilter`的`filterProcessUrl`的客户机。无状态客户端是指在`filterProcessUrl`以外的 URL 上向`CasAuthenticationFilter`提出身份验证请求的任何客户端。 由于远程处理协议无法在`HttpSession`的上下文中呈现自己,因此不可能依赖于在请求之间的会话中存储安全上下文的默认实践。此外,由于 CAS 服务器在票证经过`TicketValidator`验证后会使其无效,因此在后续请求中呈现相同的代理票证将不会工作。 一个明显的选择是完全不使用 CAS 来远程处理协议客户机。然而,这将消除 CAS 的许多理想特性。作为中间立场,`CasAuthenticationProvider`使用`StatelessTicketCache`。这仅用于使用等于`CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER`的本金的无状态客户机。发生的情况是`CasAuthenticationProvider`将在`StatelessTicketCache`中存储结果`CasAuthenticationToken`,并在代理票上键入。因此,远程协议客户端可以呈现与`CasAuthenticationProvider`相同的代理票并且将不需要与 CAS 服务器联系以进行验证(除了第一个请求)。经过身份验证后,代理票据可以用于原始目标服务以外的 URL。 本节建立在前几节的基础上,以容纳代理票据身份验证。第一步是指定对所有工件进行身份验证,如下所示。 ``` ... ``` 下一步是为`serviceProperties`指定`authenticationDetailsSource`,并为`CasAuthenticationFilter`指定`authenticationDetailsSource`。`serviceProperties`属性指示`CasAuthenticationFilter`尝试对所有工件进行身份验证,而不是仅对`filterProcessUrl`上存在的工件进行身份验证。`ServiceAuthenticationDetailsSource`创建了一个`ServiceAuthenticationDetails`,以确保在验证票据时使用基于`HttpServletRequest`的当前 URL 作为服务 URL。生成服务 URL 的方法可以通过注入一个自定义`AuthenticationDetailsSource`来定制,该方法返回一个自定义`ServiceAuthenticationDetails`。 ``` ... ``` 你还需要更新`CasAuthenticationProvider`以处理代理票。为此,将`Cas20ServiceTicketValidator`替换为`Cas20ProxyTicketValidator`。你将需要配置`statelessTicketCache`以及要接受的代理。你可以在下面找到一个接受所有代理所需的更新示例。 ``` ... ``` [JAAS](jaas.html)[X509](x509.html)