此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.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 本身包括许多AuthenticationHandlers 来协助解决这个问题。 当您下载和部署服务器 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

  • 由于用户的Authenticationobject(或缺少 object)导致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参数,该参数是表示 “service ticket” 的不透明字符串。 继续前面的示例,浏览器重定向到的 URL 可能是 server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZspring-doc.cadn.net.cn

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

  • AuthenticationManagerimplementation 将是ProviderManager,而CasAuthenticationProvider. 这CasAuthenticationProvider仅响应UsernamePasswordAuthenticationTokens 中包含特定于 CAS 的主体(例如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER) 和CasAuthenticationTokens(稍后讨论)。spring-doc.cadn.net.cn

  • CasAuthenticationProvider将使用TicketValidator实现。 这通常是一个Cas20ServiceTicketValidator这是 CAS 客户端库中包含的类之一。 如果应用程序需要验证代理ticket,则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&pgtUrl=https://server3.company.com/webapp/login/cas/proxyreceptorspring-doc.cadn.net.cn

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

  • [可选]如果对 CAS 验证服务的请求包含代理回调 URL(在pgtUrl参数),则 CAS 将包含一个pgtIoustring 在 XML 响应中。 这pgtIou表示代理授权工单 IOU。 然后,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应用于 User 的对象包含在Assertion.spring-doc.cadn.net.cn

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

  • 然后,Control 返回到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。 每个部分都基于上一节。 完整的 CAS 示例应用程序可以在 Spring Security Samples 中找到。spring-doc.cadn.net.cn

服务工单鉴权

本节介绍如何设置 Spring Security 以验证 Service Tickets。 通常,这就是 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必须等于将由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 服务器将向已登录的所有服务发送 Single Logout 请求。 这singleLogoutFilter通过查找HttpSession在静态Map然后使它无效。spring-doc.cadn.net.cn

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

  • 用户请求/logout这会将用户从本地应用程序中注销,并将用户发送到 Logout Success 页面。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确保当HttpSessionexpires,则用于单点注销的映射将被删除。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. 这servicePropertiesproperty 指示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>