对于最新的稳定版本,请使用 Spring Security 6.5.3spring-doc.cadn.net.cn

CAS 身份验证

概述

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

您可以在 www.apereo.org 了解有关 CAS 的更多信息。 您还需要访问此站点以下载 CAS 服务器文件。spring-doc.cadn.net.cn

CAS 的工作原理

虽然 CAS 网站包含详细介绍 CAS 架构的文档,但我们在 Spring Security 的上下文中再次介绍了总体概述。 Spring Security 3.x 支持 CAS 3。 在撰写本文时,CAS 服务器的版本为 3.4。spring-doc.cadn.net.cn

在企业的某个地方,您需要设置 CAS 服务器。 CAS 服务器只是一个标准的 WAR 文件,因此设置服务器没有任何困难。 在 WAR 文件中,您将自定义向用户显示的登录和其他单点登录页面。spring-doc.cadn.net.cn

部署 CAS 3.4 服务器时,还需要指定AuthenticationHandlerdeployerConfigContext.xml包含在 CAS 中。 这AuthenticationHandler有一个简单的方法,该方法返回一个布尔值,用于确定给定的 Credentials 集是否有效。 你AuthenticationHandler实现需要链接到某种类型的后端身份验证存储库,例如 LDAP 服务器或数据库。 CAS 本身包括许多AuthenticationHandler开箱即用以协助解决这个问题。 当您下载并部署服务器 war 文件时,它会设置为成功验证输入与其用户名匹配的密码的用户,这对于测试很有用。spring-doc.cadn.net.cn

除了 CAS 服务器本身之外,其他关键参与者当然是部署在整个企业中的安全 Web 应用程序。 这些 Web 应用程序称为“服务”。 服务分为三种类型。 对服务票证进行认证的,可以获取代理票证的,以及对代理票证进行认证的。 对代理票证进行身份验证是不同的,因为必须验证代理列表,并且通常可以重复使用代理票证。spring-doc.cadn.net.cn

Spring Security 和 CAS 交互序列

Web 浏览器、CAS 服务器和 Spring Security 安全服务之间的基本交互如下:spring-doc.cadn.net.cn

  • Web 用户正在浏览服务的公共页面。 不涉及 CAS 或 Spring Security。spring-doc.cadn.net.cn

  • 用户最终请求一个安全的页面,或者它使用的 Bean 之一是安全的。 Spring Security 的ExceptionTranslationFilter将检测到AccessDeniedExceptionAuthenticationException.spring-doc.cadn.net.cn

  • 因为用户的Authentication对象(或缺少对象)导致AuthenticationExceptionExceptionTranslationFilter将调用配置的AuthenticationEntryPoint. 如果使用 CAS,这将是CasAuthenticationEntryPoint类。spring-doc.cadn.net.cn

  • CasAuthenticationEntryPoint会将用户的浏览器重定向到 CAS 服务器。 它还将指示一个service参数,这是 Spring Security 服务(您的应用程序)的回调 URL。 例如,浏览器重定向到的 URL 可能是 my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/casspring-doc.cadn.net.cn

  • 用户的浏览器重定向到 CAS 后,系统将提示他们输入用户名和密码。 如果用户提供会话 cookie 表明他们之前已登录,则不会提示他们再次登录(此过程有一个例外,我们将在后面介绍)。 CAS 将使用PasswordHandler(或AuthenticationHandler如果使用 CAS 3.0),则确定用户名和密码是否有效。spring-doc.cadn.net.cn

  • 成功登录后,CAS 会将用户的浏览器重定向回原始服务。 它还将包括一个ticket参数,这是一个表示“服务票证”的不透明字符串。 继续我们前面的示例,浏览器重定向到的 URL 可能是 server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZspring-doc.cadn.net.cn

  • 回到服务 Web 应用程序中,CasAuthenticationFilter始终侦听请求/login/cas(这是可配置的,但我们将在本介绍中使用默认值)。 处理过滤器将构造一个UsernamePasswordAuthenticationToken表示服务票证。 本金将等于CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而凭据将是服务票证的不透明值。 然后,此身份验证请求将传递给配置的AuthenticationManager.spring-doc.cadn.net.cn

  • AuthenticationManager实现将是ProviderManager,它又配置了CasAuthenticationProvider. 这CasAuthenticationProvider仅响应UsernamePasswordAuthenticationToken包含特定于 CAS 的主体(例如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER) 和CasAuthenticationTokens(稍后讨论)。spring-doc.cadn.net.cn

  • CasAuthenticationProvider将使用TicketValidator实现。 这通常是Cas20ServiceTicketValidator这是 CAS 客户端库中包含的类之一。 如果应用程序需要验证代理票证,则Cas20ProxyTicketValidator被使用。 这TicketValidator向 CAS 服务器发出 HTTPS 请求以验证服务票证。 它还可能包含一个代理回调 URL,该 URL 包含在以下示例中:my.company.com/cas/proxyValidate?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas&ticket=ST-0-ER94xMJmn6pha35CQRoZ&gtUrl=https://server3.company.com/webapp/login/cas/proxyreceptorspring-doc.cadn.net.cn

  • 返回 CAS 服务器,将收到验证请求。 如果提供的服务票证与票证的签发服务 URL 匹配,则 CAS 将以 XML 形式提供肯定的响应,指示用户名。 如果身份验证中涉及任何代理(如下所述),则代理列表也包含在 XML 响应中。spring-doc.cadn.net.cn

  • [可选]如果对 CAS 验证服务的请求包含代理回调 URL(在pgtUrl参数),CAS 将包含一个pgtIou字符串。 这pgtIou表示代理授予ticket借条。 然后,CAS 服务器将创建自己的 HTTPS 连接,回到pgtUrl. 这是为了相互验证 CAS 服务器和声明的服务 URL。 HTTPS 连接将用于向原始 Web 应用程序发送授予代理票证。 例如,server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKHspring-doc.cadn.net.cn

  • Cas20TicketValidator将解析从 CAS 服务器接收的 XML。 它将返回到CasAuthenticationProvider一个TicketResponse,其中包括用户名(必填)、代理列表(如果涉及)和代理授予票证 IOU(如果请求代理回调)。spring-doc.cadn.net.cn

  • 下一个CasAuthenticationProvider将调用已配置的CasProxyDecider. 这CasProxyDecider指示TicketResponse是服务可以接受的。 Spring Security 提供了多种实现:RejectProxyTickets,AcceptAnyCasProxyNamedCasProxyDecider. 这些名称在很大程度上是不言自明的,除了NamedCasProxyDecider这允许List提供可信代理的数量。spring-doc.cadn.net.cn

  • CasAuthenticationProvider接下来将请求一个AuthenticationUserDetailsService加载GrantedAuthority应用于用户的对象包含在Assertion.spring-doc.cadn.net.cn

  • 如果没有问题,CasAuthenticationProvider构造一个CasAuthenticationToken包括TicketResponseGrantedAuthoritys.spring-doc.cadn.net.cn

  • 然后控制权返回到CasAuthenticationFilter,将创建的CasAuthenticationToken在安全上下文中。spring-doc.cadn.net.cn

  • 用户的浏览器将重定向到导致AuthenticationException(或自定义目标,具体取决于配置)。spring-doc.cadn.net.cn

你还在这里真是太好了! 现在让我们看看它是如何配置的spring-doc.cadn.net.cn

CAS 客户端的配置

由于 Spring Security,CAS 的 Web 应用程序方面变得简单。 假设您已经了解使用 Spring Security 的基础知识,因此下面不再介绍这些内容。 我们将假设正在使用基于命名空间的配置,并根据需要添加 CAS bean。 每个部分都建立在上一节的基础上。 可以在 Spring Security 示例中找到完整的 CAS 示例应用程序。spring-doc.cadn.net.cn

服务票证认证

本节介绍如何设置 Spring Security 以验证服务票证。 通常,这就是 Web 应用程序所需要的全部内容。 您需要添加一个ServicePropertiesbean 到您的应用程序上下文。 这表示您的 CAS 服务:spring-doc.cadn.net.cn

<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必须等于 URL 将由CasAuthenticationFilter. 这sendRenew默认为 false,但如果您的应用程序特别敏感,则应设置为 true。 此参数的作用是告知 CAS 登录服务单点登录是不可接受的。 相反,用户需要重新输入他们的用户名和密码才能访问该服务。spring-doc.cadn.net.cn

应配置以下 bean 以启动 CAS 身份验证过程(假设您使用的是命名空间配置):spring-doc.cadn.net.cn

<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豆。 这可以使用 entry-point-ref 轻松完成,如上例所示。 这CasAuthenticationEntryPoint必须参考ServicePropertiesbean(上面讨论过),它提供了企业 CAS 登录服务器的 URL。 这是用户浏览器将被重定向的位置。spring-doc.cadn.net.cn

CasAuthenticationFilter具有与UsernamePasswordAuthenticationFilter(用于基于表单的登录)。 可以使用这些属性来自定义身份验证成功和失败的行为等内容。spring-doc.cadn.net.cn

接下来,您需要添加一个CasAuthenticationProvider及其合作者:spring-doc.cadn.net.cn

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

如果您返回 CAS 如何工作部分,这些 bean 都是不言自明的。spring-doc.cadn.net.cn

这样就完成了 CAS 的最基本配置。 如果您没有犯任何错误,您的 Web 应用程序应该可以在 CAS 单点登录的框架内愉快地工作。 Spring Security 的其他部分无需担心 CAS 处理身份验证的事实。 在以下部分中,我们将讨论一些(可选的)更高级的配置。spring-doc.cadn.net.cn

单次注销

CAS 协议支持单次注销,可以轻松添加到 Spring Security 配置中。 以下是处理单次注销的 Spring Security 配置的更新spring-doc.cadn.net.cn

<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.jasig.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 服务器或已登录的任何其他应用程序的会话。 这requestSingleLogoutFilterfilter 将允许/spring_security_cas_logout请求将应用程序重定向到配置的 CAS 服务器注销 URL。 然后,CAS 服务器将向所有已登录的服务发送单次注销请求。 这singleLogoutFilter通过查找HttpSession在静态Map然后使它无效。spring-doc.cadn.net.cn

这可能会令人困惑,为什么logout元素和singleLogoutFilter是需要的。 首先在本地注销被认为是最佳实践,因为SingleSignOutFilter只是存储HttpSession在静态Map以调用 invalidate 对其。 使用上述配置,注销流程将为:spring-doc.cadn.net.cn

  • 用户请求/logout这会将用户注销出本地应用程序,并将用户发送到注销成功页面。spring-doc.cadn.net.cn

  • 注销成功页面/cas-logout.jsp,应指示用户单击指向/logout/cas以注销所有应用程序。spring-doc.cadn.net.cn

  • 当用户单击该链接时,用户将被重定向到 CAS 单注销 URL (localhost:9443/cas/logout)。spring-doc.cadn.net.cn

  • 在 CAS 服务器端,CAS 单注销 URL 然后向所有 CAS 服务提交单注销请求。在 CAS 服务端,JASIG 的SingleSignOutFilter通过使原始会话无效来处理注销请求。spring-doc.cadn.net.cn

下一步是将以下内容添加到您的web.xmlspring-doc.cadn.net.cn

<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.jasig.cas.client.session.SingleSignOutHttpSessionListener
</listener-class>
</listener>

使用 SingleSignOutFilter 时,可能会遇到一些编码问题。因此,建议将CharacterEncodingFilter以确保在使用SingleSignOutFilter. 同样,有关详细信息,请参阅 JASIG 的文档。 这SingleSignOutHttpSessionListener确保当HttpSession过期时,将删除用于单次注销的映射。spring-doc.cadn.net.cn

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

本节介绍如何使用 CAS 对服务进行身份验证。换句话说,本部分讨论如何设置使用使用 CAS 进行身份验证的服务的客户端。下一节介绍如何设置无状态服务以使用 CAS 进行身份验证。spring-doc.cadn.net.cn

配置 CAS 以获取代理授予票证

为了对无状态服务进行身份验证,应用程序需要获取代理授予票证(PGT)。本节介绍如何配置Spring Security以获取基于thencas-st[Service Ticket Authentication]配置构建的PGT。spring-doc.cadn.net.cn

第一步是包含一个ProxyGrantingTicketStorage在您的 Spring Security 配置中。 这用于存储由CasAuthenticationFilter以便它们可用于获取代理票证。 示例配置如下所示spring-doc.cadn.net.cn

<!--
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.jasig.cas.client.proxy.ProxyGrantingTicketStorageImpl"/>

下一步是更新CasAuthenticationProvider能够获得代理票证。 为此,将Cas20ServiceTicketValidator使用Cas20ProxyTicketValidator. 这proxyCallbackUrl应设置为应用程序将接收 PGT 的 URL。 最后,配置还应引用ProxyGrantingTicketStorage因此它可以使用 PGT 来获取代理票证。 您可以在下面找到应进行的配置更改的示例。spring-doc.cadn.net.cn

<bean id="casAuthenticationProvider"
	class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
	<bean class="org.jasig.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匹配proxyCallbackUrlCas20ProxyTicketValidator. 下面显示了一个示例配置。spring-doc.cadn.net.cn

<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. 示例代码如下:spring-doc.cadn.net.cn

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")
}

代理工单认证

CasAuthenticationProvider区分有状态客户端和无状态客户端。 有状态客户端被视为提交给filterProcessUrlCasAuthenticationFilter. 无状态客户端是向CasAuthenticationFilter在 URL 上,而不是filterProcessUrl.spring-doc.cadn.net.cn

因为远程处理协议无法在HttpSession,则无法依赖在请求之间的会话中存储安全上下文的默认做法。 此外,由于 CAS 服务器在票证经过验证后使票证失效TicketValidator,在后续请求中提供相同的代理票证将不起作用。spring-doc.cadn.net.cn

一个明显的选择是根本不将 CAS 用于远程协议客户端。 然而,这将消除 CAS 的许多理想功能。 作为中间地带,CasAuthenticationProvider使用StatelessTicketCache. 这仅用于使用等于CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER. 发生的情况是CasAuthenticationProvider将存储生成的CasAuthenticationTokenStatelessTicketCache,键入代理票证。 因此,远程协议客户端可以提供相同的代理票证和CasAuthenticationProvider不需要联系 CAS 服务器进行验证(第一个请求除外)。 经过身份验证后,代理票证可用于原始目标服务以外的 URL。spring-doc.cadn.net.cn

本节以前面的部分为基础,以适应代理票证身份验证。 第一步是指定对所有工件进行身份验证,如下所示。spring-doc.cadn.net.cn

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

下一步是指定servicePropertiesauthenticationDetailsSource对于CasAuthenticationFilter. 这serviceProperties属性指示CasAuthenticationFilter尝试验证所有工件,而不是仅验证filterProcessUrl. 这ServiceAuthenticationDetailsSource创建一个ServiceAuthenticationDetails确保当前 URL,基于HttpServletRequest,在验证工单时用作服务 URL。 生成服务URL的方法可以通过注入自定义AuthenticationDetailsSource返回自定义ServiceAuthenticationDetails.spring-doc.cadn.net.cn

<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以及您要接受哪些代理。 您可以在下面找到接受所有代理所需的更新示例。spring-doc.cadn.net.cn

<bean id="casAuthenticationProvider"
	class="org.springframework.security.cas.authentication.CasAuthenticationProvider">
...
<property name="ticketValidator">
	<bean class="org.jasig.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>