此版本仍在开发中,尚未被视为稳定版本。对于最新的稳定版本,请使用 Spring Security 6.4.3! |
CAS 身份验证
概述
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 服务器时,您还需要指定AuthenticationHandler
在deployerConfigContext.xml
包含在 CAS 中。
这AuthenticationHandler
具有一个简单的方法,该方法返回一个布尔值,用于判断给定的 Credentials 集是否有效。
你AuthenticationHandler
实现需要链接到某种类型的后端身份验证存储库,例如 LDAP 服务器或数据库。
CAS 本身包括许多AuthenticationHandler
s 来协助解决这个问题。
当您下载和部署服务器 war 文件时,它被设置为成功验证输入与其用户名匹配的密码的用户,这对于测试很有用。
除了 CAS 服务器本身之外,其他关键参与者当然是部署在整个企业中的安全 Web 应用程序。 这些 Web 应用程序称为 “服务”。 有三种类型的服务。 对服务票证进行身份验证的命令、可以获取代理票证的命令以及对代理票证进行身份验证的命令。 对代理票证进行身份验证有所不同,因为必须验证代理列表,并且通常可以重复使用代理票证。
Spring Security 和 CAS 交互序列
Web 浏览器、CAS 服务器和 Spring Security 安全服务之间的基本交互如下:
-
Web 用户正在浏览服务的公共页面。 不涉及 CAS 或 Spring Security。
-
用户最终请求一个安全的页面,或者它使用的 bean 之一是安全的。 Spring Security 的
ExceptionTranslationFilter
将检测AccessDeniedException
或AuthenticationException
. -
由于用户的
Authentication
object(或缺少 object)导致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
(或AuthenticationHandler
如果使用 CAS 3.0)来确定用户名和密码是否有效。 -
成功登录后,CAS 会将用户的浏览器重定向回原始服务。 它还将包括一个
ticket
参数,该参数是表示 “service ticket” 的不透明字符串。 继续前面的示例,浏览器重定向到的 URL 可能是 server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ。 -
回到服务 Web 应用程序中,
CasAuthenticationFilter
始终监听对/login/cas
(这是可配置的,但我们将在本简介中使用默认值)。 处理过滤器将构造一个UsernamePasswordAuthenticationToken
表示服务工单。 principal 将等于CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
,而凭证将是服务票证的不透明值。 然后,此身份验证请求将传递给配置的AuthenticationManager
. -
这
AuthenticationManager
implementation 将是ProviderManager
,而CasAuthenticationProvider
. 这CasAuthenticationProvider
仅响应UsernamePasswordAuthenticationToken
s 中包含特定于 CAS 的主体(例如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER
) 和CasAuthenticationToken
s(稍后讨论)。 -
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/proxyreceptor。 -
返回 CAS 服务器,将收到验证请求。 如果提供的服务票证与票证的颁发服务地址匹配,CAS 将以 XML 格式提供肯定的响应,指示用户名。 如果身份验证中涉及任何代理(如下所述),则 XML 响应中还包括代理列表。
-
[可选]如果对 CAS 验证服务的请求包含代理回调 URL(在
pgtUrl
参数),则 CAS 将包含一个pgtIou
string 在 XML 响应中。 这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。 它将返回到CasAuthenticationProvider
一个TicketResponse
,其中包括用户名(必填)、代理列表(如果涉及)和代理授予票证 IOU(如果请求了代理回调)。 -
下一个
CasAuthenticationProvider
将调用已配置的CasProxyDecider
. 这CasProxyDecider
指示TicketResponse
服务可接受。 Spring Security 提供了几种实现:RejectProxyTickets
,AcceptAnyCasProxy
和NamedCasProxyDecider
. 这些名称在很大程度上是不言自明的,除了NamedCasProxyDecider
这允许List
的可信代理。 -
CasAuthenticationProvider
接下来将请求一个AuthenticationUserDetailsService
加载GrantedAuthority
应用于 User 的对象包含在Assertion
. -
如果没有问题,
CasAuthenticationProvider
构造一个CasAuthenticationToken
包括TicketResponse
和GrantedAuthority
s. -
然后,Control 返回到
CasAuthenticationFilter
,它将创建的CasAuthenticationToken
在安全上下文中。 -
用户的浏览器被重定向到导致
AuthenticationException
(或自定义目标,具体取决于配置)。
你还在这里真是太好了! 现在让我们看看它是如何配置的
CAS 客户端的配置
由于 Spring Security,CAS 的 Web 应用程序端变得容易。 假设您已经了解使用 Spring Security 的基础知识,因此下面不再介绍这些基础知识。 我们假设正在使用基于名称空间的配置,并根据需要添加 CAS bean。 每个部分都基于上一节。 完整的 CAS 示例应用程序可以在 Spring Security Samples 中找到。
服务工单鉴权
本节介绍如何设置 Spring Security 以验证 Service Tickets。
通常,这就是 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
.
这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
豆。
这可以很容易地使用 entry-point-ref 来完成,就像上面的例子中所做的那样。
这CasAuthenticationEntryPoint
必须引用ServiceProperties
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.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
实际上并不使用密码进行身份验证,但它确实使用权限。
如果您回顾一下 CAS 的工作原理部分,这些 bean 都是不言自明的。
这样就完成了 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.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 服务器或已登录的任何其他应用程序的会话。
这requestSingleLogoutFilter
filter 将允许/spring_security_cas_logout
请求将应用程序重定向到配置的 CAS 服务器注销 URL。
然后,CAS 服务器将向已登录的所有服务发送 Single Logout 请求。
这singleLogoutFilter
通过查找HttpSession
在静态Map
然后使它无效。
这可能会令人困惑为什么logout
元素和singleLogoutFilter
都需要。
最佳做法是先在本地注销,因为SingleSignOutFilter
只存储HttpSession
在静态Map
以便对其调用 invalidate。
使用上述配置,注销流程为:
-
用户请求
/logout
这会将用户从本地应用程序中注销,并将用户发送到 Logout Success 页面。 -
注销成功页面
/cas-logout.jsp
应指示用户单击指向/logout/cas
以便注销所有应用程序。 -
当用户单击该链接时,用户将被重定向到 CAS 单点注销 URL (localhost:9443/cas/logout)。
-
然后,在 CAS 服务器端,CAS 单点注销 URL 将向所有 CAS 服务提交单点注销请求。 在 CAS 服务端,JASIG 的
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.jasig.cas.client.session.SingleSignOutHttpSessionListener
</listener-class>
</listener>
使用 SingleSignOutFilter 时,您可能会遇到一些编码问题。
因此,建议添加CharacterEncodingFilter
确保在使用SingleSignOutFilter
.
同样,有关详细信息,请参阅 JASIG 的文档。
这SingleSignOutHttpSessionListener
确保当HttpSession
expires,则用于单点注销的映射将被删除。
使用 CAS 对无状态服务进行身份验证
本节介绍如何使用 CAS 对服务进行身份验证。 换句话说,本节讨论如何设置使用通过 CAS 进行身份验证的服务的客户端。 下一节介绍如何设置无状态服务以使用 CAS 进行身份验证。
配置 CAS 以获取代理授予票证
为了对无状态服务进行身份验证,应用程序需要获取代理授予票证 (PGT)。 本节介绍如何配置 Spring Security 以在 thencas-st[Service Ticket Authentication] 配置上获取 PGT 构建。
第一步是包含一个ProxyGrantingTicketStorage
在 Spring Security 配置中。
这用于存储由CasAuthenticationFilter
,以便它们可用于获取代理票证。
示例配置如下所示
<!--
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 获取代理票证。
您可以在下面找到应进行的配置更改的示例。
<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
匹配proxyCallbackUrl
的Cas20ProxyTicketValidator
.
示例配置如下所示。
<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
.
示例代码如下:
-
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")
}
代理票证身份验证
这CasAuthenticationProvider
区分有状态客户端和无状态客户端。
有状态客户端被视为提交到filterProcessUrl
的CasAuthenticationFilter
.
无状态客户端是向CasAuthenticationFilter
在 URL 上的filterProcessUrl
.
因为远程处理协议无法在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>
下一步是指定serviceProperties
和authenticationDetailsSource
对于CasAuthenticationFilter
.
这serviceProperties
property 指示CasAuthenticationFilter
尝试验证所有构件,而不仅仅是filterProcessUrl
.
这ServiceAuthenticationDetailsSource
创建一个ServiceAuthenticationDetails
,它确保当前 URL,基于HttpServletRequest
,在验证工单时用作服务 URL。
生成服务 URL 的方法可以通过注入自定义AuthenticationDetailsSource
返回一个自定义的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.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>