CAS 认证

本站(springdoc.cn)中的内容来源于 spring.io ,原始版权归属于 spring.io。由 springdoc.cn 进行翻译,整理。可供个人学习、研究,未经许可,不得进行任何转载、商用或与之相关的行为。 商标声明:Spring 是 Pivotal Software, Inc. 在美国以及其他国家的商标。

概览

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 有一个简单的方法,可以返回一个布尔值,说明一组给定的证书是否有效。你的 AuthenticationHandler 实现将需要链接到某种类型的后端认证库,如 LDAP 服务器或数据库。CAS本身就包括了大量的 AuthenticationHandler 来帮助实现这个目标。当你下载和部署服务器war文件时,它被设置为成功地验证那些输入与他们的用户名相匹配的密码的用户,这对测试很有用。

除了CAS服务器本身,其他的关键角色当然是部署在你的企业中的安全web应用。这些web应用被称为 "service"。有三种类型的服务。那些验证 service ticket 的,那些可以获得 proxy ticket 的,以及那些验证 proxy ticket 的。验证 proxy ticket 的不同之处在于,必须验证 proxy 列表,而且很多时候 proxy ticket 可以重复使用。

Spring Security 和 CAS 交互序列

web浏览器、CAS服务器和 Spring Security 安全服务之间的基本互动如下:

  • web用户正在浏览该服务的公共页面。CAS或Spring Security没有参与。

  • 用户最终请求的页面要么是 secure 的,要么是其使用的 bean 之一是 secure 的。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后,他们将被提示输入用户名和密码。如果用户出示了一个 session cookie,表明他们之前已经登录过,他们将不会被提示再次登录(这个过程有一个例外,我们将在后面介绍)。CAS 将使用上面讨论的 PasswordHandler(如果使用CAS 3.0,则为 AuthenticationHandler)来决定用户名和密码是否有效。

  • 登录成功后,CAS将把用户的浏览器重定向到原始服务。它还将包括一个 ticket 参数,这是一个不透明的字符串,代表 "service ticket"。继续我们前面的例子,浏览器被重定向到的URL可能是 server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ。

  • 回到服务web应用中,CasAuthenticationFilter 总是在监听对 /login/cas 的请求(这是可以配置的,但在本介绍中我们将使用默认值)。处理 filter 将构建一个代表 service ticket 的 UsernamePasswordAuthenticationToken。principal 将等于 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而凭证将是 service ticket opaque 值。然后,这个认证请求将被交给配置的 AuthenticationManager

  • AuthenticationManager 的实现将是 ProviderManager,它又被配置为 CasAuthenticationProviderCasAuthenticationProvider 只对包含CAS特定 principal(如 CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)和 CasAuthenticationToken(后面讨论)的 UsernamePasswordAuthenticationTokens 做出响应。

  • CasAuthenticationProvider 将使用 TicketValidator 实现来验证 service ticket。这通常是一个 Cas20ServiceTicketValidator,它是CAS客户端库中的一个类。如果应用程序需要验证 proxy ticket,则使用 Cas20ProxyTicketValidatorTicketValidator 向CAS服务器发出HTTPS请求,以验证 service ticket。它还可以包括一个 proxy 回调 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服务器上,将收到验证请求。如果提交的 service ticket 与 ticket 发出的服务URL相匹配,CAS将在XML中提供一个肯定的响应,表明用户名。如果任何 proxy 参与了验证(在下面讨论),proxy 的列表也包括在XML响应中。

  • [可选] 如果对CAS验证服务的请求包括 proxy 回调 URL(在 pgtUrl 参数中),CAS将在XML响应中包括一个 pgtIou 字符串。这个 pgtIou 代表一个 proxy 授予的 ticket IOU。然后CAS服务器将创建自己的HTTPS连接回到 pgtUrl。这是为了对CAS服务器和声明的服务URL进行相互认证。HTTPS 连接将被用来向原始 web 应用程序发送 granting ticket。例如, server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH。

  • Cas20TicketValidator 将解析从 CAS 服务器收到的XML。它将向 CasAuthenticationProvider 返回一个 TicketResponse,其中包括用户名(强制)、proxy 列表(如果涉及任何代理)和 proxy grant ticket IOU(如果要求 proxy callback)。

  • 接下来 CasAuthenticationProvider 将调用一个配置的 CasProxyDeciderCasProxyDecider 表明 TicketResponse 中的 proxy 列表是否为服务所接受。Spring Security 提供了几种实现方式: RejectProxyTicketsAcceptAnyCasProxyNamedCasProxyDecider。这些名称基本上是不言自明的,除了 NamedCasProxyDecider,它允许提供一个受信任的 proxy 列表。

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

  • 如果没有问题,CasAuthenticationProvider 会构建一个 CasAuthenticationToken,包括 TicketResponseGrantedAuthority 中包含的细节。

  • 然后控制权返回到 CasAuthenticationFilter,它将创建的 CasAuthenticationToken 放入 security context。

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

现在让我们来看看这是如何配置的

CAS客户端的配置

由于 Spring Security 的存在,CAS的Web应用方面变得很容易。我们假设你已经知道了使用 Spring Security 的基本知识,所以下面就不再介绍这些了。我们将假设正在使用基于命名空间的配置,并根据需要添加CAS bean。每一节都建立在前一节的基础上。完整的CAS示例应用程序可以在 Spring Security Samples 中找到。

Service Ticket 认证

本节介绍了如何设置 Spring Security 来验证 Service Ticket。很多时候,这就是一个Web应用所需要的全部。你将需要在你的 application context 中添加一个 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(上面讨论过),它提供了企业CAS登录服务器的URL。这就是用户的浏览器将被重定向的地方。

CasAuthenticationFilterUsernamePasswordAuthenticationFilter(用于基于表单的登录)的属性非常相似。你可以使用这些属性来定制诸如认证成功和失败的行为。

接下来你需要添加一个 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 配置中。

<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以获得代理授权票据(Proxy Granting Tickets)

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

第一步是在你的 Spring Security 配置中包含一个 ProxyGrantingTicketStorage。这是用来存储由 CasAuthenticationFilter 获得的PGT,以便它们可以被用来获得代理票据(proxy ticket)。下面是一个配置的例子

<!--
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,使其能够获得 proxy ticket。为此,用 Cas20ProxyTicketValidator 替换 Cas20ServiceTicketValidatorproxyCallbackUrl 应该被设置为应用程序将接收 PGT 的 URL。最后,配置还应该引用 ProxyGrantingTicketStorage,这样它就可以使用PGT来获得 proxy ticket。你可以在下面找到一个应该进行配置修改的例子。

<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,你可以用它们来创建 proxy ticket,可以用来验证无状态服务。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 的客户端。无状态客户端是指在 filterProcessesUrl 以外的 URL 上向 CasAuthenticationFilter 提出认证请求的任何客户端。

因为远程协议没有办法在 HttpSession 的上下文中展示自己,所以不可能依靠在请求之间将 security context 存储在会话中的默认做法。此外,由于CAS服务器在 ticket 被 TicketValidator 验证后就会失效,所以在随后的请求中呈现相同的 proxy ticket 是不可行的。

一个明显的选择是根本不对远程协议客户端使用CAS。然而,这将消除CAS的许多理想功能。作为一个中间方案,CasAuthenticationProvider 使用了一个 StatelessTicketCache。这只用于无状态客户端,这些客户端使用一个等于 CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER 的 principal。发生的情况是, CasAuthenticationProvider 将把产生的 CasAuthenticationToken 存储在 StatelessTicketCache 中,以 proxy ticket 为关键。因此,远程协议客户端可以提出相同的 proxy ticket,CasAuthenticationProvider 将不需要联系CAS服务器进行验证(除了第一个请求)。一旦通过验证,proxy ticket 可以用于原始目标服务以外的URL。

本节建立在前几节的基础上,以适应 proxy ticket 认证。第一步是指定对所有工件进行认证,如下图所示。

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

下一步是为 CasAuthenticationFilter 指定 servicePropertiesauthenticationDetailsSourceserviceProperties 属性指示 CasAuthenticationFilter 尝试认证所有工件,而不是只认证 filterProcessesUrl 上的工件。ServiceAuthenticationDetailsSource 创建一个 ServiceAuthenticationDetails,以确保在验证 ticket 时,基于 HttpServletRequest 的当前URL被用作 service URL。生成 service URL 的方法可以通过注入一个返回自定义 ServiceAuthenticationDetailsAuthenticationDetailsSource 来定制。

<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 以处理 proxy ticket。为此,要用 Cas20ProxyTicketValidator 替换 Cas20ServiceTicketValidator。你需要配置 statelessTicketCache 和你想接受的 proxy。你可以在下面找到一个接受所有 proxy 所需的更新的例子。

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