spring-security的基本概念和原理

前言

Servlet安全性:宏观视图

本节讨论基于Servlet的应用程序中的Spring Security高层架构。我们在引用的“身份验证、授权、防止攻击保护”部分中构建这种高层次的理解。

Filters的回顾

Spring Security的Servlet支持基于Servlet Filter,因此首先了解Filter的作用是有帮助的。下图显示了单个HTTP请求的处理程序的典型分层。
spring-security的基本概念和原理_第1张图片
客户端向应用程序发送请求,容器创建一个FilterChain,其中包含FilterServlet,该Servlet应该根据请求URI的路径处理HttpServletRequest。在Spring MVC应用程序中,ServletDispatcherServlet的一个实例。一个Servlet最多只能处理一个HttpServletRequestHttpServletResponse。然而,可以使用多个Filter:

  • 防止下游FilterServlet被调用。在这个实例中,Filter通常会编写HttpServletResponse
  • 修改下游FilterServlet使用的HttpServletRequestHttpServletResponse

Filter的力量来自传递给它的FilterChain

FilterChain用法示例

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    // do something before the rest of the application
    chain.doFilter(request, response); // invoke the rest of the application
    // do something after the rest of the application
}

由于Filter只影响下游FilterServlet,所以调用每个Filter的顺序非常重要。

DelegatingFilterProxy

Spring提供了一个名为DelegatingFilterProxy的过滤器实现,它允许在Servlet容器的生命周期和Spring的ApplicationContext之间建立桥接。Servlet容器允许使用自己的标准注册Filter,但是它不知道Spring定义的bean。可以通过标准的Servlet容器机制注册DelegatingFilterProxy,但将所有工作委托给实现Filter的Spring Bean。

下面是关于DelegatingFilterProxy如何适合FilterFilterChain的图片。
spring-security的基本概念和原理_第2张图片
DelegatingFilterProxyApplicationContext查找Bean Filter0,然后调用Bean Filter0。DelegatingFilterProxy的伪代码如下所示。

public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
    // Lazily get Filter that was registered as a Spring Bean
    // For the example in DelegatingFilterProxy delegate is an instance of Bean Filter0
    Filter delegate = getFilterBean(someBeanName);
    // delegate work to the Spring Bean
    delegate.doFilter(request, response);
}

DelegatingFilterProxy的另一个好处是它允许延迟查找Filter bean实例。这一点很重要,因为容器需要在启动之前注册Filter实例。然而,Spring通常使用ContextLoaderListener来加载Spring bean,直到需要注册Filter实例之后才会加载。

FilterChainProxy

Spring Security的Servlet支持包含在FilterChainProxy中。FilterChainProxy是Spring Security提供的一个特殊Filter,它允许通过SecurityFilterChain委托给许多Filter实例。因为FilterChainProxy是一个Bean,所以它通常封装在DelegatingFilterProxy中。
spring-security的基本概念和原理_第3张图片

SecurityFilterChain

SecurityFilterChainFilterChainProxy用来确定应该为这个请求调用哪个Spring安全Filter

spring-security的基本概念和原理_第4张图片
SecurityFilterChain中的安全过滤器通常是bean,但它们在FilterChainProxy中注册,而不是在DelegatingFilterProxy中注册。FilterChainProxy提供了许多直接注册Servlet容器或DelegatingFilterProxy的优点。首先,它为所有Spring Security的Servlet支持提供了一个起点。由于这个原因,如果您试图排除Spring Security的Servlet支持的故障,那么在FilterChainProxy中添加一个调试点是一个很好的起点。

其次,由于FilterChainProxy是Spring安全使用的核心,它可以执行非可选的任务。例如,它清除SecurityContext以避免内存泄漏。它还应用Spring Security的HttpFirewall来保护应用程序免受某些类型的攻击。

此外,它在确定何时应该调用SecurityFilterChain方面提供了更大的灵活性。在Servlet容器中,只根据URL调用Filter。然而,FilterChainProxy可以通过利用RequestMatcher接口,基于HttpServletRequest中的任何内容来确定调用。

事实上,FilterChainProxy可以用来确定应该使用哪个SecurityFilterChain。这允许为应用程序的不同部分提供完全独立的配置。
spring-security的基本概念和原理_第5张图片
在多个SecurityFilterChain图中,FilterChainProxy决定使用哪个SecurityFilterChain。只有第一个匹配的SecurityFilterChain才会被调用。如果请求的URL为/api/messages/,它将首先匹配SecurityFilterChain0的模式/api/**,所以只有SecurityFilterChain0会被调用,即使它也匹配SecurityFilterChainn。如果URL/messages/被请求,它将不匹配SecurityFilterChain0的模式/api/**,所以FilterChainProxy将继续尝试每个SecurityFilterChain。假设没有其他匹配SecurityFilterChain的实例,SecurityFilterChainn将被调用。

注意SecurityFilterChain0只配置了三个安全Filter实例。但是,SecurityFilterChainn配置了四个安全Filter。需要注意的是,每个SecurityFilterChain都可以是唯一的,并且可以单独配置。实际上,如果应用程序希望Spring Security忽略某些请求,SecurityFilterChain可能没有任何安全Filter

Security过滤器

安全过滤器通过SecurityFilterChain API插入到FilterChainProxy中。过滤器的顺序很重要。通常不需要知道Spring Security Filter的顺序。然而,有时候知道顺序是有益的

以下是Spring安全过滤器排序的全面列表:

  • ChannelProcessingFilter

  • WebAsyncManagerIntegrationFilter

  • SecurityContextPersistenceFilter

  • HeaderWriterFilter

  • CorsFilter

  • CsrfFilter

  • LogoutFilter

  • OAuth2AuthorizationRequestRedirectFilter

  • Saml2WebSsoAuthenticationRequestFilter

  • X509AuthenticationFilter

  • AbstractPreAuthenticatedProcessingFilter

  • CasAuthenticationFilter

  • OAuth2LoginAuthenticationFilter

  • Saml2WebSsoAuthenticationFilter

  • UsernamePasswordAuthenticationFilter

  • OpenIDAuthenticationFilter

  • DefaultLoginPageGeneratingFilter

  • DefaultLogoutPageGeneratingFilter

  • ConcurrentSessionFilter

  • DigestAuthenticationFilter

  • BearerTokenAuthenticationFilter

  • BasicAuthenticationFilter

  • RequestCacheAwareFilter

  • SecurityContextHolderAwareRequestFilter

  • JaasApiIntegrationFilter

  • RememberMeAuthenticationFilter

  • AnonymousAuthenticationFilter

  • OAuth2AuthorizationCodeGrantFilter

  • SessionManagementFilter

  • ExceptionTranslationFilter

  • FilterSecurityInterceptor

  • SwitchUserFilter

处理安全异常

ExceptionTranslationFilter允许将AccessDeniedException和AuthenticationException转换为HTTP响应。

ExceptionTranslationFilter作为安全过滤器之一插入到FilterChainProxy中。
spring-security的基本概念和原理_第6张图片

  • 首先,ExceptionTranslationFilter调用FilterChain.doFilter(request, response)来调用应用程序的其余部分。

  • 如果用户未被认证,或者是AuthenticationException,则启动认证。
    SecurityContextHolder被清除
    HttpServletRequest保存在RequestCache中。当用户成功身份验证时,将使用RequestCache重播原始请求。
    AuthenticationEntryPoint用于从客户端请求凭据。例如,它可能重定向到登录页面或发送WWW-Authenticate标头。

  • 否则,如果是AccessDeniedException,则拒绝访问。调用AccessDeniedHandler来处理拒绝访问。

如果应用程序没有抛出AccessDeniedExceptionAuthenticationException,那么ExceptionTranslationFilter不会做任何事情。

ExceptionTranslationFilter的伪代码是这样的:

ExceptionTranslationFilter伪代码

try {
    filterChain.doFilter(request, response); 
} catch (AccessDeniedException | AuthenticationException ex) {
    if (!authenticated || ex instanceof AuthenticationException) {
        startAuthentication(); 
    } else {
        accessDenied(); 
    }
}
  • 通过对筛选器的回顾,您可以回想起调用FilterChain.doFilter(request, response)等价于调用应用程序的其余部分。
    这意味着,如果应用程序的另一部分(例如FilterSecurityInterceptor或method security)抛出AuthenticationExceptionAccessDeniedException,它将在这里被捕捉和处理。
  • 如果用户未被认证,或者是AuthenticationException,则启动认证。
  • 否则,拒绝访问

认证

Spring Security为身份验证提供了全面的支持。本节讨论:

架构组件

本节描述在Servlet身份验证中使用Spring Security的主要体系结构组件。如果您需要解释这些部分如何组合在一起的具体流,请查看身份验证机制特定部分。

  • SecurityContextHolder——SecurityContextHolder是Spring Security存储被身份验证的详细信息的地方。
  • SecurityContext——从SecurityContextHolder获得,包含当前认证用户的Authentication
  • Authentication——可以是AuthenticationManager的输入,以提供用户提供的用于身份验证的凭据,也可以是SecurityContext中的当前用户。
  • GrantedAuthority——授予Authentication主体的权限(即角色、范围等)。
  • AuthenticationManager——定义Spring Security过滤器如何执行身份验证的API。
  • ProviderManager——AuthenticationManager最常见的实现。
  • AuthenticationProvider——由ProviderManager用于执行特定类型的身份验证。
  • 使用AuthenticationEntryPoint请求凭证——用于从客户端请求凭证(例如,重定向到登录页面,发送一个WWW-Authenticate响应,等等)
  • AbstractAuthenticationProcessingFilter——用于身份验证的基本Filter。这也很好地了解了高层次的身份验证流程以及各个部件如何一起工作。

用户认证机制

  • 用户名和密码—如何使用用户名/密码进行身份验证
  • OAuth 2.0登录—使用OpenID Connect登录OAuth 2.0,使用非标准OAuth 2.0登录(即GitHub)
  • 登录SAML 2.0—登录SAML 2.0
  • CAS (Central Authentication Server)—支持CAS
  • 记住我——如何记住用户过去的会话过期
  • JAAS认证——使用JAAS进行认证
  • OpenID - OpenID身份验证(不要与OpenID连接混淆)
  • 预认证场景——使用外部机制(如SiteMinder或Java EE安全)进行认证,但仍然使用Spring安全进行授权和保护,防止常见的攻击。
  • X509认证—X509认证

SecurityContextHolder

Spring Security身份验证模型的核心是SecurityContextHolder。它包含SecurityContext
spring-security的基本概念和原理_第7张图片
SecurityContextHolder是Spring Security存储身份验证的详细信息的地方。Spring Security并不关心如何填充SecurityContextHolder。如果它包含一个值,那么它将被用作当前经过身份验证的用户。

表示用户已经过身份验证的最简单方法是直接设置SecurityContextHolder

SecurityContext context = SecurityContextHolder.createEmptyContext(); 
Authentication authentication =
    new TestingAuthenticationToken("username", "password", "ROLE_USER"); 
context.setAuthentication(authentication);

SecurityContextHolder.setContext(context); 
  • 我们首先创建一个空的SecurityContext
    重要的是创建一个新的SecurityContext实例,而不是使用SecurityContextHolder.getContext().setAuthentication(Authentication)来避免多个线程之间的竞争条件。
  • 接下来,我们创建一个新的Authentication对象。
    Spring Security并不关心在SecurityContext上设置了什么类型的Authentication实现。
    这里我们使用TestingAuthenticationToken,因为它非常简单。
    更常见的生产场景是UsernamePasswordAuthenticationToken(userDetails、password、authorities)
  • 最后,我们在SecurityContextHolder上设置SecurityContext
    Spring Security将使用此信息进行授权。

如果希望获得关于经过身份验证的主体的信息,可以通过访问SecurityContextHolder来实现。

访问当前认证用户

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
String username = authentication.getName();
Object principal = authentication.getPrincipal();
Collection authorities = authentication.getAuthorities();

默认情况下,SecurityContextHolder使用ThreadLocal来存储这些详细信息,这意味着SecurityContext对于同一个线程中的方法总是可用的,即使SecurityContext没有显式地作为参数传递给这些方法。以这种方式使用ThreadLocal是相当安全的,如果在处理当前主体的请求后小心地清除线程。Spring Security的FilterChainProxy确保SecurityContext总是被清除。

有些应用程序并不完全适合使用ThreadLocal,因为它们处理线程的特定方式。例如,Swing客户机可能希望Java虚拟机中的所有线程都使用相同的安全上下文。SecurityContextHolder可以在启动时配置一个策略,以指定您希望如何存储上下文。对于独立的应用程序,您将使用SecurityContextHolder.MODE_GLOBAL策略。其他应用程序可能希望安全线程生成的线程也采用相同的安全标识。这是通过使用SecurityContextHolder.MODE_INHERITABLETHREADLOCAL实现的。您可以更改默认的SecurityContextHolder.MODE_THREADLOCAL模式有两种方式。第一个是设置系统属性,第二个是调用SecurityContextHolder上的静态方法。大多数应用程序不需要改变默认设置,但如果需要,请查看SecurityContextHolder的JavaDoc以了解更多信息。

SecurityContext

SecurityContextSecurityContextHolder中获取。SecurityContext包含一个Authentication对象。

Authentication

在Spring Security中,Authentication有两个主要目的:

  • AuthenticationManager的输入,用于提供用户为进行身份验证而提供的凭据。在此场景中使用时,isAuthenticated()返回false
  • 表示当前通过身份验证的用户。当前的Authentication可以从SecurityContext中获取。

Authentication包含:

  • principal—标识用户。当使用用户名/密码进行身份验证时,这通常是UserDetails的一个实例。
  • credentials—通常是密码。在许多情况下,这将在用户身份验证后清除,以确保不会泄漏。
  • authoritiesGrantedAuthority为用户被授予的高级权限。一些例子是角色或作用域。

GrantedAuthority

GrantedAuthority为用户被授予的高级权限。一些例子是角色或作用域。

GrantedAuthority可以从Authentication.getAuthorities()方法获得。此方法提供了一个GrantedAuthority对象集合。GrantedAuthority是授予主体的权限,这并不奇怪。这样的权限通常是“角色”,例如ROLE_ADMINISTRATORROLE_HR_SUPERVISOR。稍后将为web授权、方法授权和域对象授权配置这些角色。Spring Security的其他部分能够解释这些权限,并期望它们存在。当使用基于用户名/密码的身份验证时,GrantedAuthority通常由UserDetailsService加载。

通常,GrantedAuthority对象是应用程序范围的权限。它们不是特定于给定的域对象。因此,您不太可能拥有一个GrantedAuthority来表示对Employee对象编号54的权限,因为如果有数千个这样的权限,您将很快耗尽内存(或者至少导致应用程序花很长时间来验证用户)。当然,Spring Security是专门设计来处理这一常见需求的,但是您可以使用项目的域对象安全功能来实现这一目的。

AuthenticationManager

AuthenticationManager是定义Spring Security的过滤器如何执行身份验证的API。然后,调用AuthenticationManager的控制器(即Spring Security的Filters)在SecurityContextHolder上设置返回的Authentication。如果你没有集成Spring Security的过滤器,你可以直接设置SecurityContextHolder,而不需要使用AuthenticationManager

虽然AuthenticationManager的实现可以是任何东西,但最常见的实现是ProviderManager。

ProviderManager

ProviderManager是AuthenticationManager最常用的实现。ProviderManager委托给AuthenticationProvider列表。每个AuthenticationProvider都有机会表明身份验证应该是成功的,失败的,或者表明它不能做出决定,并允许下游的AuthenticationProvider来做出决定。如果没有配置AuthenticationProvider可以进行身份验证,然后用ProviderNotFoundException身份验证失败,是一种特殊的AuthenticationException表明ProviderManager没有配置为支持传递给它的Authentication类型。
spring-security的基本概念和原理_第8张图片
实际上,每个AuthenticationProvider都知道如何执行特定类型的身份验证。例如,一个AuthenticationProvider可能能够验证用户名/密码,而另一个AuthenticationProvider可能能够验证SAML断言。这允许每个AuthenticationProvider执行特定类型的身份验证,同时支持多种类型的身份验证,并且只公开一个AuthenticationManager bean。

ProviderManager还允许配置一个可选的父AuthenticationManager,当AuthenticationProvider不能执行身份验证时,会咨询该父AuthenticationManager。父类可以是任何类型的AuthenticationManager,但它通常是ProviderManager的一个实例。

spring-security的基本概念和原理_第9张图片
事实上,多个ProviderManager实例可能共享相同的父AuthenticationManager。这在多个SecurityFilterChain实例具有某些共同身份验证(共享的父类AuthenticationManager)和不同身份验证机制(不同的ProviderManager实例)的场景中有些常见。
spring-security的基本概念和原理_第10张图片

AuthenticationProvider

多个AuthenticationProvider可以被注入到ProviderManager中。每个AuthenticationProvider执行特定类型的身份验证。例如,DaoAuthenticationProvider支持基于用户名/密码的身份验证,而JwtAuthenticationProvider支持验证JWT令牌。

使用AuthenticationEntryPoint请求凭据

AuthenticationEntryPoint用于发送HTTP响应,该响应从客户端请求凭据。

有时,客户端会主动包含用户名/密码等凭据来请求资源。在这些情况下,Spring Security不需要提供从客户机请求凭据的HTTP响应,因为它们已经包含在内。

在其他情况下,客户端将向未被授权访问的资源发出未经身份验证的请求。在本例中,AuthenticationEntryPoint的实现用于从客户端请求凭据。AuthenticationEntryPoint实现可能执行重定向到一个登录页面,响应一个WWW-Authenticate头,等等。

AbstractAuthenticationProcessingFilter

AbstractAuthenticationProcessingFilter被用作验证用户凭据的基本Filter。在验证凭据之前,Spring Security通常使用AuthenticationEntryPoint请求凭据。

接下来,AbstractAuthenticationProcessingFilter可以验证提交给它的任何身份验证请求。
spring-security的基本概念和原理_第11张图片

  • 当用户提交他们的凭据时,AbstractAuthenticationProcessingFilterHttpServletRequest创建一个Authentication来进行身份验证。创建的身份验证类型依赖于AbstractAuthenticationProcessingFilter的子类。例如,UsernamePasswordAuthenticationFilter从HttpServletRequest中提交的用户名和密码创建UsernamePasswordAuthenticationToken
  • 接下来,将Authentication传递给AuthenticationManager进行身份验证。
  • 如果身份验证失败,则Failure
    清除SecurityContextHolder。
    RememberMeServices.loginFail被调用。如果记住我没配置,这是空操作。
    AuthenticationFailureHandler被调用。
  • 如果身份验证成功,则Success
    SessionAuthenticationStrategy会在新登录时得到通知。
    Authentication在SecurityContextHolder上设置。稍后,SecurityContextPersistenceFilterSecurityContext保存到HttpSession
    RememberMeServices.loginSuccess被调用。如果记住我没配置,这是空操作。
    ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent
    AuthenticationSuccessHandler被调用。

用户名/密码验证

验证用户身份的最常见方法之一是验证用户名和密码。因此,Spring Security提供了对使用用户名和密码进行身份验证的全面支持。

读取用户名和密码

Spring Security提供了以下内置机制来从HttpServletRequest读取用户名和密码:

  • 登录表单
  • 基本认证
  • 摘要式身份验证

存储机制

每种受支持的读取用户名和密码的机制都可以利用任何受支持的存储机制:

  • 简单的存储与内存认证
  • 使用JDBC身份验证的关系数据库
  • 使用UserDetailsService自定义数据存储
  • LDAP存储与LDAP认证

登录表单

Spring Security支持通过html表单提供用户名和密码。本节详细介绍基于表单的身份验证如何在Spring Security中工作。

让我们看看基于表单的登录是如何在Spring Security中工作的。首先,我们将看到如何将用户重定向到登录表单。

spring-security的基本概念和原理_第12张图片
该图构建了我们的SecurityFilterChain图。

  1. 首先,用户向未授权的资源/private发出未经身份验证的请求。
  2. Spring Security的FilterSecurityInterceptor通过抛出AccessDeniedException来拒绝未经身份验证的请求。
  3. 由于用户没有经过身份验证,ExceptionTranslationFilter将启动Start Authentication并发送一个重定向到配置了AuthenticationEntryPoint的登录页面。在大多数情况下,AuthenticationEntryPointLoginUrlAuthenticationEntryPoint的一个实例。
  4. 然后,浏览器将请求被重定向到的登录页面。
  5. 应用程序内的某些东西必须呈现登录页面。

提交用户名和密码后,UsernamePasswordAuthenticationFilter将对用户名和密码进行验证。UsernamePasswordAuthenticationFilter扩展了AbstractAuthenticationProcessingFilter,所以这个图看起来应该很相似。
spring-security的基本概念和原理_第13张图片
该图构建了我们的SecurityFilterChain图。

  1. 当用户提交他们的用户名和密码时,UsernamePasswordAuthenticationFilter通过从HttpServletRequest中提取用户名和密码创建UsernamePasswordAuthenticationToken,这是一种Authentication类型。
  2. 接下来,将UsernamePasswordAuthenticationToken传递到AuthenticationManager中进行身份验证。AuthenticationManager的详细信息取决于用户信息的存储方式。
  3. 如果身份验证失败,则Failure
    清除SecurityContextHolder
    RememberMeServices.loginFail被调用。如果记住我没配置,这是空操作。
    AuthenticationFailureHandler被调用。
  4. 如果身份验证成功,则Success
    SessionAuthenticationStrategy会在新登录时得到通知。
    Authentication在SecurityContextHolder上设置。稍后,SecurityContextPersistenceFilterSecurityContext保存到HttpSession
    RememberMeServices.loginSuccess被调用。如果记住我没配置,这是空操作。
    ApplicationEventPublisher发布一个InteractiveAuthenticationSuccessEvent
    AuthenticationSuccessHandler被调用。通常这是一个SimpleUrlAuthenticationSuccessHandler,当我们重定向到登录页面时,它将重定向到ExceptionTranslationFilter保存的请求。

Spring Security表单登录在默认情况下是启用的。但是,只要提供了任何基于servlet的配置,就必须显式地提供基于表单的登录。一个最小的、显式的Java配置可以在下面找到:

protected void configure(HttpSecurity http) {
    http
        // ...
        .formLogin(withDefaults());
}

在这个配置中,Spring Security将呈现一个默认的登录页面。大多数生产应用程序都需要一个自定义的登录表单。

下面的配置演示了如何在表单中提供自定义日志。

protected void configure(HttpSecurity http) throws Exception {
    http
        // ...
        .formLogin(form -> form
            .loginPage("/login")
            .permitAll()
        );
}

当在Spring Security配置中指定登录页面时,您将负责呈现该页面。下面是一个Thymeleaf模板,它生成一个HTML登录表单,符合/login登录页面:

src/main/resources/templates/login.html



    
        Please Log In
    
    
        

Please Log In

Invalid username and password.
You have been logged out.

关于默认HTML表单有几个关键点:

  • 表单应该执行post/login
  • 该表单将需要包括一个CSRF令牌,由Thymeleaf自动包含。
  • 表单应该在名为username的参数中指定用户名
  • 表单应该在名为password的参数中指定密码
  • 如果发现HTTP参数error,则表示用户未能提供有效的用户名/密码
  • 如果查询到HTTP参数logout,则表示用户注销成功

许多用户只需要定制登录页面。但是,如果需要的话,上面的一切都可以通过额外的配置进行定制。

如果您正在使用Spring MVC,您将需要一个控制器,将GET /login映射到我们创建的登录模板。LoginController的最小示例如下:

@Controller
class LoginController {
    @GetMapping("/login")
    String login() {
        return "login";
    }
}

基本身份验证

本节详细介绍Spring Security如何为基于servlet的应用程序提供对基本HTTP身份验证的支持。

让我们看看HTTP基本身份验证是如何在Spring Security中工作的。首先,我们看到WWW-Authenticate头被发回给一个未经过身份验证的客户端。
spring-security的基本概念和原理_第14张图片
该图构建了我们的SecurityFilterChain图。

  1. 首先,用户向未授权的资源/private发出未经身份验证的请求。
  2. Spring Security的FilterSecurityInterceptor通过抛出AccessDeniedException来拒绝未经身份验证的请求。
  3. 由于用户没有经过身份验证,ExceptionTranslationFilter将启动开始身份验证。配置的AuthenticationEntryPoint是一个BasicAuthenticationEntryPoint的实例,它发送一个WWW-Authenticate报头。RequestCache通常是一个不保存请求的NullRequestCache,因为客户机能够重放它最初请求的请求。

当客户端接收到WWW-Authenticate报头时,它知道应该用用户名和密码重试。下面是正在处理的用户名和密码的流程。
spring-security的基本概念和原理_第15张图片
该图构建了我们的SecurityFilterChain图。

  1. 当用户提交他们的用户名和密码时,BasicAuthenticationFilter通过从HttpServletRequest中提取用户名和密码来创建UsernamePasswordAuthenticationToken,这是一种Authentication类型。
  2. 接下来,将UsernamePasswordAuthenticationToken传递到AuthenticationManager中进行身份验证。AuthenticationManager的详细信息取决于用户信息的存储方式。
  3. 如果身份验证失败,则Failure
    清除SecurityContextHolder
    RememberMeServices.loginFail被调用。如果记住我没配置,这是空操作。
    AuthenticationEntryPoint被调用来触发WWW-Authenticate再次发送。
  4. 如果身份验证成功,则Success
    SessionAuthenticationStrategy会在新登录时得到通知。
    RememberMeServices.loginSuccess被调用。如果记住我没配置,这是空操作。
    BasicAuthenticationFilter调用FilterChain.doFilter(request,response)来继续应用程序逻辑的其余部分。

默认情况下,Spring Security的HTTP基本身份验证支持是启用的。但是,只要提供了任何基于servlet的配置,就必须显式地提供HTTP Basic。

一个最小的,显式的配置可以找到如下:

protected void configure(HttpSecurity http) {
    http
        // ...
        .httpBasic(withDefaults());
}

摘要式身份验证

本节详细介绍Spring Security如何提供摘要身份验证支持,摘要身份验证是由DigestAuthenticationFilter提供的。

您不应该在现代应用程序中使用摘要身份验证,因为它被认为不安全。最明显的问题是必须以明文、加密或MD5格式存储密码。所有这些存储格式都是不安全的。相反,您应该使用单向自适应密码散列(即bCrypt, PBKDF2, SCrypt等)存储凭证,这是摘要认证不支持的。

摘要身份验证试图解决基本身份验证的许多弱点,特别是通过确保凭证永远不会通过网络以明文发送。许多浏览器支持摘要身份验证。

管理HTTP摘要身份验证的标准由RFC 2617定义,它更新了RFC 2069规定的摘要身份验证标准的早期版本。大多数用户代理实现RFC 2617。Spring Security Digest Authentication支持与RFC 2617规定的认证质量保护(qop)兼容,它还提供了与RFC 2069的向后兼容性。摘要身份验证被认为是一个更有吸引力的选择,如果你需要使用未加密的HTTP(即没有TLS/HTTPS),并希望最大限度地安全性的身份验证过程。然而,每个人都应该使用HTTPS。

摘要身份验证的中心是一个“nonce”。这是服务器生成的值。Spring Security的nonce采用以下格式:

base64(expirationTime + ":" + md5Hex(expirationTime + ":" + key))
expirationTime:   The date and time when the nonce expires, expressed in milliseconds
key:              A private key to prevent modification of the nonce token

你需要确保你配置了不安全的明文密码存储使用NoOpPasswordEncoder。以下是使用Java配置配置摘要认证的示例:

@Autowired
UserDetailsService userDetailsService;

DigestAuthenticationEntryPoint entryPoint() {
    DigestAuthenticationEntryPoint result = new DigestAuthenticationEntryPoint();
    result.setRealmName("My App Relam");
    result.setKey("3028472b-da34-4501-bfd8-a355c42bdf92");
}

DigestAuthenticationFilter digestAuthenticationFilter() {
    DigestAuthenticationFilter result = new DigestAuthenticationFilter();
    result.setUserDetailsService(userDetailsService);
    result.setAuthenticationEntryPoint(entryPoint());
}

protected void configure(HttpSecurity http) throws Exception {
    http
        // ...
        .exceptionHandling(e -> e.authenticationEntryPoint(authenticationEntryPoint()))
        .addFilterBefore(digestFilter());
}

内存中的身份验证

Spring Security的InMemoryUserDetailsManager实现了UserDetailsService,以支持在内存中检索的基于用户名/密码的身份验证。InMemoryUserDetailsManager通过实现UserDetailsManager接口来提供对UserDetails的管理。当Spring Security配置为接受用户名/密码进行身份验证时,将使用基于UserDetails的身份验证。

在这个示例中,我们使用Spring Boot CLI对password的密码进行编码,并获得编码后的密码{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW

@Bean
public UserDetailsService users() {
    UserDetails user = User.builder()
        .username("user")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER")
        .build();
    UserDetails admin = User.builder()
        .username("admin")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER", "ADMIN")
        .build();
    return new InMemoryUserDetailsManager(user, admin);
}

上面的示例以安全的格式存储密码,但在入门经验方面还有很多不足之处。

在下面的示例中,我们利用了User.withDefaultPasswordEncoder来确保存储在内存中的密码是受保护的。但是,它不能通过反编译源代码来防止获得密码。出于这个原因,User.withDefaultPasswordEncoder应该只用于“开始”,而不是用于生产。

@Bean
public UserDetailsService users() {
    // The builder will ensure the passwords are encoded before saving in memory
    UserBuilder users = User.withDefaultPasswordEncoder();
    UserDetails user = users
        .username("user")
        .password("password")
        .roles("USER")
        .build();
    UserDetails admin = users
        .username("admin")
        .password("password")
        .roles("USER", "ADMIN")
        .build();
    return new InMemoryUserDetailsManager(user, admin);
}

没有简单的方法来使用User.withDefaultPasswordEncoder基于XML的配置。对于演示或刚刚开始,您可以选择在密码前加上{noop},以表示不应该使用编码。


    
    

JDBC的身份验证

Spring Security的JdbcDaoImpl实现了UserDetailsService来提供对使用JDBC检索的基于用户名/密码的身份验证的支持。JdbcUserDetailsManager扩展了JdbcDaoImpl,通过UserDetailsManager接口提供对UserDetails的管理。当Spring Security配置为接受用户名/密码进行身份验证时,将使用基于UserDetails的身份验证。

默认架构

Spring Security为基于JDBC的身份验证提供默认查询。本节提供与默认查询相对应的默认模式。您将需要调整模式,以匹配与您正在使用的查询和数据库方言相匹配的定制。

用户模式

JdbcDaoImpl需要表来加载用户的密码、帐户状态(启用或禁用)和权限(角色)列表。需要的默认模式可以在下面找到。

create table users(
    username varchar_ignorecase(50) not null primary key,
    password varchar_ignorecase(500) not null,
    enabled boolean not null
);

create table authorities (
    username varchar_ignorecase(50) not null,
    authority varchar_ignorecase(50) not null,
    constraint fk_authorities_users foreign key(username) references users(username)
);
create unique index ix_auth_username on authorities (username,authority);
组模式

如果您的应用程序利用组,您将需要提供组模式。组的默认模式可以在下面找到。

create table groups (
    id bigint generated by default as identity(start with 0) primary key,
    group_name varchar_ignorecase(50) not null
);

create table group_authorities (
    group_id bigint not null,
    authority varchar(50) not null,
    constraint fk_group_authorities_group foreign key(group_id) references groups(id)
);

create table group_members (
    id bigint generated by default as identity(start with 0) primary key,
    username varchar(50) not null,
    group_id bigint not null,
    constraint fk_group_members_group foreign key(group_id) references groups(id)
);
设置数据源

在配置JdbcUserDetailsManager之前,必须创建一个DataSource。在我们的示例中,我们将设置一个使用默认用户模式初始化的嵌入式数据源。

@Bean
DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(H2)
        .addScript("classpath:org/springframework/security/core/userdetails/jdbc/users.ddl")
        .build();
}
JdbcUserDetailsManager Bean

在这个示例中,我们使用Spring Boot CLI对password的密码进行编码,并获得编码后的密码{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW。有关如何存储密码的更多细节,请参阅PasswordEncoder一节。

@Bean
UserDetailsManager users(DataSource dataSource) {
    UserDetails user = User.builder()
        .username("user")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER")
        .build();
    UserDetails admin = User.builder()
        .username("admin")
        .password("{bcrypt}$2a$10$GRLdNijSQMUvl/au9ofL.eDwmoohzzS7.rmNSJZ.0FxO/BTk76klW")
        .roles("USER", "ADMIN")
        .build();
    JdbcUserDetailsManager users = new JdbcUserDetailsManager(dataSource);
    users.createUser(user);
    users.createUser(admin);
}

UserDetails

UserDetailsUserDetailsService返回。DaoAuthenticationProvider验证UserDetails,然后返回一个Authentication,该Authentication有一个主体,该主体是由已配置的UserDetailsService返回的UserDetails

UserDetailsService

DaoAuthenticationProvider使用UserDetailsService检索用户名、密码和其他属性,以验证用户名和密码。Spring Security提供了UserDetailsService的内存和JDBC实现。

您可以通过将自定义UserDetailsService公开为bean来定义自定义身份验证。例如,以下将自定义身份验证,假设CustomUserDetailsService实现了UserDetailsService:

只有当AuthenticationManagerBuilder没有被填充并且AuthenticationProviderBean没有被定义时才会使用。

@Bean
CustomUserDetailsService customUserDetailsService() {
    return new CustomUserDetailsService();
}

PasswordEncoder

Spring Security的servlet通过与PasswordEncoder集成来支持安全存储密码。定制Spring Security使用的PasswordEncoder实现可以通过公开PasswordEncoder Bean来完成。

DaoAuthenticationProvider

DaoAuthenticationProvider是一个AuthenticationProvider实现,它利用UserDetailsService和PasswordEncoder来验证用户名和密码。

让我们看看DaoAuthenticationProvider是如何在Spring Security中工作的。图中解释了读取用户名和密码中的AuthenticationManager如何工作的细节。
spring-security的基本概念和原理_第16张图片

  1. 读取用户名和密码的身份验证FilterUsernamePasswordAuthenticationToken传递给AuthenticationManager,这是由ProviderManager实现的。
  2. ProviderManager被配置为使用DaoAuthenticationProvider类型的AuthenticationProvider
  3. DaoAuthenticationProviderUserDetailsService中查找UserDetails
  4. 然后,DaoAuthenticationProvider使用PasswordEncoder验证上一步返回的UserDetails上的密码。
  5. 当身份验证成功时,返回的身份验证类型为UsernamePasswordAuthenticationToken,并且具有一个主体,该主体是由已配置的UserDetailsService返回的UserDetails。最终,返回的UsernamePasswordAuthenticationToken将由身份验证FilterSecurityContextHolder上设置。

LDAP认证

LDAP经常被组织用作用户信息的中心存储库和身份验证服务。它还可以用于存储应用程序用户的角色信息。

当Spring Security被配置为接受用户名/密码进行身份验证时,Spring Security将使用基于LDAP的身份验证。但是,尽管利用用户名/密码进行身份验证,但它并没有使用UserDetailsService集成,因为在绑定身份验证中,LDAP服务器没有返回密码,因此应用程序不能执行密码验证。

对于如何配置LDAP服务器,有许多不同的场景,因此Spring Security的LDAP提供者是完全可配置的。它使用单独的策略接口进行身份验证和角色检索,并提供可以配置为处理各种情况的缺省实现。

前提条件

在尝试将LDAP与Spring Security一起使用之前,您应该熟悉LDAP。下面的链接很好地介绍了相关的概念,并提供了使用免费LDAP服务器OpenLDAP设置目录的指南:https://www.zytrax.com/books/ldap/。熟悉一些用于从Java访问LDAP的JNDI api可能也很有用。我们在LDAP提供程序中没有使用任何第三方LDAP库(Mozilla、JLDAP等),但是Spring LDAP得到了广泛的使用,所以如果您计划添加自己的自定义,对该项目有所了解可能会有所帮助。

在使用LDAP身份验证时,一定要确保正确配置LDAP连接池。如果您不熟悉如何做到这一点,可以参考Java LDAP文档。

设置嵌入式LDAP服务器

您需要做的第一件事是确保有一个LDAP Server来指向您的配置。为简单起见,最好从嵌入式LDAP Server开始。Spring Security支持使用以下任意一种:

  • 嵌入式UnboundID服务器
  • 嵌入式ApacheDS服务器

在下面的示例中,我们将下面的内容公开为users.ldif作为类路径资源,以初始化嵌入的LDAP服务器,其中用户useradmin的密码都是password

dn: ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: groups

dn: ou=people,dc=springframework,dc=org
objectclass: top
objectclass: organizationalUnit
ou: people

dn: uid=admin,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Rod Johnson
sn: Johnson
uid: admin
userPassword: password

dn: uid=user,ou=people,dc=springframework,dc=org
objectclass: top
objectclass: person
objectclass: organizationalPerson
objectclass: inetOrgPerson
cn: Dianne Emu
sn: Emu
uid: user
userPassword: password

dn: cn=user,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: user
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org
uniqueMember: uid=user,ou=people,dc=springframework,dc=org

dn: cn=admin,ou=groups,dc=springframework,dc=org
objectclass: top
objectclass: groupOfNames
cn: admin
uniqueMember: uid=admin,ou=people,dc=springframework,dc=org

待补充

会话管理

HTTP会话相关的功能是通过SessionManagementFilterSessionAuthenticationStrategy接口的组合来处理的,该接口由过滤器委托给它。典型的应用包括会话固定保护、攻击预防、会话超时检测和限制通过身份验证的用户可以同时打开的会话数量。

检测超时

您可以配置Spring Security来检测无效会话ID的提交,并将用户重定向到适当的URL。这是通过session-management元素实现的:


...


注意,如果您使用这种机制来检测会话超时,如果用户登出,然后在没有关闭浏览器的情况下重新登录,它可能会错误地报告错误。这是因为当您使会话失效时,会话cookie不会被清除,即使用户已经注销,它也会被重新提交。你可以在登出时显式地删除JSESSIONID cookie,例如在登出处理程序中使用以下语法:




不幸的是,这不能保证对每个servlet容器都适用,所以您需要在您的环境中测试它

如果您在代理服务器后运行应用程序,您还可以通过配置代理服务器来删除会话cookie。例如,使用Apache HTTPD的mod_headers,下面的指令会在登出请求的响应中使JSESSIONID过期,从而删除JSESSIONID cookie(假设应用程序部署在path /tutorial下):


Header always set Set-Cookie "JSESSIONID=;Path=/tutorial;Expires=Thu, 01 Jan 1970 00:00:00 GMT"

并发会话控制

如果您希望对单个用户登录到您的应用程序的能力进行限制,Spring Security通过以下简单的附加功能提供了开箱即用的支持。首先,你需要将以下侦听器添加到你的web.xml文件中,以保持Spring Security关于会话生命周期事件的更新:



    org.springframework.security.web.session.HttpSessionEventPublisher


然后将以下几行添加到你的应用程序上下文中:


...

    


这将防止用户多次登录—第二次登录将导致第一次登录无效。通常您希望防止第二次登录,在这种情况下您可以使用


...

    


第二次登录将被拒绝。通过“拒绝”,我们的意思是,如果使用基于表单的登录,用户将被发送到authentication-fail -url。如果第二次身份验证是通过另一种非交互机制进行的,比如“remember-me”,一个“未经授权的”(401)错误将被发送给客户机。如果希望使用错误页面,可以将属性session-authentication-error-url添加到session-management元素。

如果您正在为基于表单的登录使用自定义身份验证过滤器,那么您必须显式地配置并发会话控制支持。更多的细节可以在会话管理一章中找到。

会话固定攻击保护

会话固定攻击是一个潜在的风险,在这种情况下,恶意攻击者可能通过访问一个站点来创建一个会话,然后说服另一个用户使用相同的会话登录(例如,通过向他们发送一个包含会话标识符作为参数的链接)。Spring Security通过在用户登录时创建新会话或更改会话ID来自动防止这种情况发生。如果您不需要这种保护,或者它与其他一些需求冲突,您可以使用上的session-fixation-protection属性来控制行为,该属性有四个选项

  • none-不做任何事。原会议将保留。
  • newSession-创建一个新的“干净”会话,而不复制现有的会话数据(与Spring security相关的属性仍将被复制)。
  • migrateSession-创建一个新会话,并将所有现有会话属性复制到新会话。这是Servlet 3.0或旧容器的默认设置。
  • changeSessionId-不要创建新的会话。相反,使用Servlet容器提供的会话固定保护(HttpServletRequest#changeSessionId())。这个选项只在Servlet 3.1 (Java EE 7)和更新的容器中可用。在旧容器中指定它将导致异常。这是Servlet 3.1和新容器中的默认值。

当会话固定保护发生时,它会导致在应用程序上下文中发布SessionFixationProtectionEvent。如果您使用changeSessionId,此保护也将导致任何javax.servlet.http. httpessionidlistener被通知,所以如果您的代码侦听这两个事件,请谨慎使用。有关更多信息,请参见会话管理一章。

SessionManagementFilter

SessionManagementFilter根据SecurityContextHolder的当前内容检查SecurityContextRepository的内容,以确定用户在当前请求期间是否已经通过身份验证,通常由非交互式验证机制,如pre-authentication或记得我[3]。如果存储库包含安全上下文,则筛选器不执行任何操作。如果没有,并且线程本地的SecurityContext包含一个(非匿名的)Authentication对象,那么过滤器假定它们已经通过堆栈中先前的过滤器进行了身份验证。然后它将调用已配置的SessionAuthenticationStrategy

如果用户当前没有经过身份验证,该过滤器将检查是否请求了一个无效的会话ID(例如,由于超时),并将调用配置的InvalidSessionStrategy(如果设置了一个)。最常见的行为就是重定向到一个固定的URL,这被封装在标准实现SimpleRedirectInvalidSessionStrategy中。当通过名称空间配置无效的会话URL时也会使用后者,如前所述。

SessionAuthenticationStrategy

SessionAuthenticationStrategySessionManagementFilterAbstractAuthenticationProcessingFilter使用,所以如果你使用一个定制的表单登录类,例如,你将需要将它注入到这两个类中。在这种情况下,结合命名空间和自定义bean的典型配置可能是这样的:







    
    ...



请注意,如果您将bean存储在实现HttpSessionBindingListener的会话中(包括Spring会话范围内的bean),使用缺省的SessionFixationProtectionStrategy可能会导致问题。有关这个类的更多信息,请参阅Javadoc。

并发控制

Spring Security能够防止主体对同一应用程序的并发身份验证次数超过指定的次数。许多isv利用这一点来实施许可,而网络管理员喜欢这个特性,因为它有助于防止人们共享登录名。例如,您可以阻止用户“Batman”从两个不同的会话登录到web应用程序。您可以过期他们之前的登录,也可以在他们试图再次登录时报告错误,以防止第二次登录。注意,如果您使用第二种方法,没有显式登出的用户(例如,刚刚关闭浏览器的用户)将不能再次登录,直到原始会话过期。

命名空间支持并发控制,因此请检查前面的命名空间章节以获得最简单的配置。但有时你需要定制一些东西。

该实现使用了SessionAuthenticationStrategy的一个特殊版本,称为ConcurrentSessionControlAuthenticationStrategy

以前,并发身份验证检查是由ProviderManager进行的,它可以被注入一个ConcurrentSessionController。后者将检查用户是否试图超过允许的会话数。但是,这种方法要求预先创建HTTP会话,这是不可取的。在Spring Security 3中,用户首先通过AuthenticationManager进行身份验证,一旦验证成功,将创建一个会话,并检查是否允许打开另一个会话。

要使用并发会话支持,你需要在web.xml中添加以下内容:


    
    org.springframework.security.web.session.HttpSessionEventPublisher
    

此外,你需要添加ConcurrentSessionFilter到你的FilterChainProxyConcurrentSessionFilter需要两个构造函数参数:sessionRegistrysessionInformationExpiredStrategy,前者通常指向SessionRegistryImpl的一个实例,后者定义了在会话过期时应用的策略。使用命名空间来创建FilterChainProxy和其他默认bean的配置可能是这样的:
























    
    
        
        
        
    
    
    
    
        
    
    




web.xml中添加侦听器会导致在每次HttpSession开始或结束时将ApplicationEvent发布到Spring ApplicationContext中。这非常重要,因为它允许在会话结束时通知SessionRegistryImpl。如果没有它,用户将永远无法在超过会话允许范围后再次登录,即使他们退出另一个会话或会话超时。

查询当前认证用户及其会话的SessionRegistry

设置并发控制,通过名称空间或使用简单的bean的有用的副作用为你提供SessionRegistry的引用,您可以直接使用在您的应用程序,所以即使你不希望限制用户的会话数量,它可能值得建立基础设施。您可以将maximumSession属性设置为-1,以允许无限制的会话。如果使用名称空间,可以使用session-registry-alias属性为内部创建的SessionRegistry设置别名,提供一个可以注入到自己的bean中的引用。

getAllPrincipals()方法为您提供了当前经过身份验证的用户列表。您可以通过调用getAllSessions(Object principal,boolean includeExpiredSessions)方法来列出用户的会话,该方法返回一个SessionInformation对象列表。您还可以通过在SessionInformation实例上调用expireNow()来终止用户的会话。当用户返回到应用程序时,他们将被阻止继续。例如,您可能会发现这些方法在管理应用程序中很有用。查看Javadoc以获得更多信息。

记住我的身份验证

概述

记住我或持久登录身份验证指的是网站能够在会话之间记住主体的身份。这通常是通过向浏览器发送cookie来完成的,在未来的会话中检测到cookie并导致自动登录发生。Spring Security为执行这些操作提供了必要的钩子,并有两个具体的remember-me实现。一个使用散列来保护基于cookie的令牌的安全性,另一个使用数据库或其他持久存储机制来存储生成的令牌。

注意,两个实现都需要一个UserDetailsService。如果您使用的身份验证提供程序不使用UserDetailsService(例如,LDAP提供程序),那么它将无法工作,除非您的应用程序上下文中也有UserDetailsService bean。

简单的基于哈希的令牌方法

这种方法使用哈希来实现一个有用的记忆策略。本质上,一个cookie在成功的交互身份验证后被发送到浏览器,cookie组成如下:

base64(username + ":" + expirationTime + ":" +
md5Hex(username + ":" + expirationTime + ":" password + ":" + key))

username:          As identifiable to the UserDetailsService
password:          That matches the one in the retrieved UserDetails
expirationTime:    The date and time when the remember-me token expires, expressed in milliseconds
key:               A private key to prevent modification of the remember-me token

因此,remember-me令牌仅在指定的时间段内有效,并且前提是用户名、密码和密钥不发生更改。值得注意的是,这有一个潜在的安全问题,因为捕获的remember-me令牌在令牌过期之前对任何用户代理都是可用的。这与摘要身份验证是相同的问题。如果主体意识到一个令牌已经被捕获,那么他们可以很容易地更改自己的密码,并立即使所有remember-me令牌失效。如果需要更重要的安全性,您应该使用下一节中描述的方法。或者记住我的服务根本不应该被使用。

如果您熟悉关于名称空间配置那一章讨论的主题,您可以通过添加元素来启用remember-me身份验证:


...


UserDetailsService通常会被自动选择。如果在您的应用程序上下文中有多个,您需要指定应该将哪个与user-service-ref属性一起使用,其中值是您的UserDetailsService bean的名称。

持久化令牌方法

这种方法基于文章http://jaspan.com/improved_persistent_login_cookie_best_practice,并对[4]做了一些小修改。要在命名空间配置中使用这种方法,你需要提供一个数据源引用:


...


数据库应该包含一个persistent_logins表,使用以下SQL(或等效SQL)创建:

create table persistent_logins (username varchar(64) not null,
                                series varchar(64) primary key,
                                token varchar(64) not null,
                                last_used timestamp not null)

记住我的接口和实现

Remember-me与UsernamePasswordAuthenticationFilter一起使用,并通过AbstractAuthenticationProcessingFilter超类中的钩子实现。它也在BasicAuthenticationFilter中使用。这些钩子将在适当的时候调用一个具体的RememberMeServices。界面看起来是这样的:

Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);

void loginFail(HttpServletRequest request, HttpServletResponse response);

void loginSuccess(HttpServletRequest request, HttpServletResponse response,
    Authentication successfulAuthentication);

请参考Javadoc以获得关于这些方法做什么的更全面的讨论,尽管在这个阶段注意AbstractAuthenticationProcessingFilter只调用loginFail()loginSuccess()方法。当SecurityContextHolder不包含Authentication时,RememberMeAuthenticationFilter会调用autoLogin()方法。因此,该接口为底层的remember-me实现提供了与身份验证相关的事件的充分通知,并在候选web请求可能包含cookie并希望被记住时委托给实现。这种设计允许使用任意数量的记忆实现策略。我们在上面已经看到Spring Security提供了两种实现。我们将依次讨论这些问题。

TokenBasedRememberMeServices

此实现支持简单哈希令牌方法中描述的更简单的方法。TokenBasedRememberMeServices生成一个RememberMeAuthenticationToken,该token由RememberMeAuthenticationProvider处理。密钥在这个身份验证提供者和TokenBasedRememberMeServices之间共享。此外,TokenBasedRememberMeServices需要一个UserDetailsService,它可以从中检索用户名和密码,用于签名比较,并生成RememberMeAuthenticationToken来包含正确的GrantedAuthoritys。应用程序应该提供某种注销命令,当用户请求时使cookie失效。TokenBasedRememberMeServices也实现了Spring Security的LogoutHandler接口,因此可以与LogoutFilter一起使用来自动清除cookie。

应用程序上下文中启用remember-me服务所需的bean如下所示:














别忘了把你的RememberMeServices实现添加到UsernamePasswordAuthenticationFilter.setRememberMeServices()属性中,在AuthenticationManager.setProviders()列表中包括RememberMeAuthenticationProvider,并添加RememberMeAuthenticationFilter到你的FilterChainProxy(通常在你的UsernamePasswordAuthenticationFilter之后)。

PersistentTokenBasedRememberMeServices

这个类可以以与TokenBasedRememberMeServices相同的方式使用,但是它还需要配置一个PersistentTokenRepository来存储令牌。有两种标准实现。

  • InMemoryTokenRepositoryImpl,它仅用于测试。
  • JdbcTokenRepositoryImpl,它将令牌存储在数据库中。

数据库模式在上述持久令牌方法中进行了描述。

OpenID支持

OpenID 1.0和2.0协议已经被弃用,我们鼓励用户迁移到OpenID Connect, spring-security-oauth2支持OpenID Connect。

命名空间支持OpenID登录,而不是普通的基于表单的登录。





然后你应该注册一个OpenID提供商(如myopenid.com),并将用户信息添加到内存:


您应该能够使用myopenid.com站点登录进行身份验证。通过在openid-login元素上设置user-service-ref属性,也可以选择一个特定的UserDetailsService bean来使用OpenID。注意,我们在上面的用户配置中省略了password属性,因为这组用户数据只用于加载用户的权限。将在内部生成一个随机密码,防止您在配置的其他地方意外地使用此用户数据作为身份验证源。

属性交换

支持OpenID属性交换。例如,下面的配置将尝试从OpenID提供者检索电子邮件和全名,供应用程序使用:



    
    


每个OpenID属性的“类型”是一个URI,由特定的模式决定,在本例中为https://axschema.org/。如果必须检索一个属性才能成功进行身份验证,则可以设置required属性。所支持的确切模式和属性将取决于您的OpenID提供者。属性值作为身份验证过程的一部分返回,可以使用以下代码访问:

OpenIDAuthenticationToken token =
    (OpenIDAuthenticationToken)SecurityContextHolder.getContext().getAuthentication();
List attributes = token.getAttributes();

我们可以从SecurityContextHolder中获得OpenIDAuthenticationTokenOpenIDAttribute包含属性类型和检索到的值(或多值属性的值)。您可以提供多个attribute-exchange元素,对每个元素使用identifier-matcher属性。它包含一个正则表达式,将与用户提供的OpenID标识符进行匹配。请参阅代码库中的OpenID示例应用程序以获得示例配置,它为谷歌、Yahoo和MyOpenID提供者提供了不同的属性列表。

匿名认证

概述

通常认为采用“默认拒绝”是良好的安全实践,即显式指定允许什么和不允许什么。定义未经身份验证的用户可以访问的内容也是类似的情况,特别是对于web应用程序。许多站点要求用户必须对除了一些url(例如主页和登录页面)以外的任何内容进行身份验证。在这种情况下,为这些特定url定义访问配置属性比为每个安全资源定义访问配置属性更容易。换句话说,有时候说ROLE_SOMETHING默认情况下是必需的,并且只允许这个规则的某些例外,比如登录、注销和应用程序的主页。您也可以从过滤器链中完全忽略这些页面,从而绕过访问控制检查,但由于其他原因,这可能是不需要的,特别是如果页面对经过身份验证的用户的行为不同。

这就是我们所说的匿名身份验证。注意,在“匿名身份验证”的用户和未身份验证的用户之间没有真正的概念上的区别。Spring Security的匿名身份验证为配置访问控制属性提供了一种更方便的方式。对servlet API调用(例如getCallerPrincipal)的调用仍然返回null,即使SecurityContextHolder中实际上有一个匿名身份验证对象。

在其他情况下,匿名身份验证也很有用,比如审计拦截器查询SecurityContextHolder以确定哪个主体负责给定的操作。如果知道SecurityContextHolder总是包含一个Authentication对象,而且永远不会为null,则可以更加健壮地创建类。

配置

使用HTTP配置Spring Security 3.0时自动提供了匿名身份验证支持,并且可以使用元素进行定制(或禁用)。您不需要配置这里描述的bean,除非您正在使用传统bean配置。

三个类一起提供匿名身份验证特性。AnonymousAuthenticationTokenAuthentication的实现,并存储应用于匿名主体的GrantedAuthority。有一个对应的AnonymousAuthenticationProvider,它被链接到ProviderManager中,以便AnonymousAuthenticationToken被接受。最后,还有一个AnonymousAuthenticationFilter,它被链接到普通的身份验证机制之后,如果那里没有存在的身份验证,它会自动将一个AnonymousAuthenticationToken添加到SecurityContextHolder中。过滤器和身份验证提供程序的定义如下所示









key在筛选器和身份验证提供者之间共享,因此前者创建的令牌被后者接受。userAttribute表示为usernameInTheAuthenticationToken,grantedAuthority[,grantedAuthority]。这与InMemoryDaoImpluserMap属性的等号后面使用的语法相同。

如前所述,匿名身份验证的好处是所有URI模式都可以应用安全性。例如:





    
    
    
    
    
    
    " +


AuthenticationTrustResolver

匿名身份验证讨论的最后一部分是AuthenticationTrustResolver接口及其相应的AuthenticationTrustResolverImpl实现。该接口提供了一个isAnonymous(Authentication)方法,该方法允许感兴趣的类考虑这种特殊类型的身份验证状态。ExceptionTranslationFilter使用这个接口在处理AccessDeniedException。如果抛出AccessDeniedException,匿名类型的身份验证,而不是扔一个403(禁止)响应,过滤器将开始AuthenticationEntryPoint所以主体可以正常进行身份验证。这是一个必要的区别,否则主体总是被认为是“经过身份验证的”,从来没有机会通过表单、基本的、摘要或其他一些正常的身份验证机制登录。

在上面的拦截器配置中,您经常会看到ROLE_ANONYMOUS属性被IS_AUTHENTICATED_ANONYMOUSLY所取代,这在定义访问控制时实际上是相同的。这是一个使用AuthenticatedVoter的例子,我们将在授权一章中看到。它使用AuthenticationTrustResolver来处理这个特定的配置属性,并将访问权限授予匿名用户。AuthenticatedVoter方法更加强大,因为它允许您区分匿名、记住我和完全验证的用户。如果您不需要这个功能,那么您可以坚持使用ROLE_ANONYMOUS,它将由Spring Security的标准RoleVoter处理。

Pre-Authentication场景

有些情况下,您希望使用Spring Security进行授权,但是用户在访问应用程序之前已经通过某些外部系统的可靠身份验证。我们将这些情况称为“预先身份验证”场景。示例包括X.509、Siteminder和应用程序在其中运行的Java EE容器的身份验证。当使用预认证时,Spring Security必须这样做

  • 识别发出请求的用户。
  • 请获取用户的权限。

具体细节将取决于外部身份验证机制。对于X.509,用户可以通过证书信息来标识,对于Siteminder,则可以通过HTTP请求头来标识。如果依赖容器身份验证,将通过调用传入HTTP请求的getUserPrincipal()方法来标识用户。在某些情况下,外部机制可能为用户提供角色/权限信息,但在其他情况下,权限必须从单独的来源获得,如UserDetailsService

Pre-Authentication框架类

由于大多数预身份验证机制遵循相同的模式,Spring Security有一组类,它们为实现预身份验证提供者提供了内部框架。这消除了重复,并允许以结构化的方式添加新的实现,而不必从头编写所有内容。如果您想使用X.509身份验证之类的东西,则不需要了解这些类,因为它已经有一个名称空间配置选项,使用起来更简单。如果您需要使用显式bean配置,或者正在计划编写自己的实现,那么了解所提供的实现是如何工作的将非常有用。你可以在org.springframework.security.web.authentication.preauth下找到类。我们只是在这里提供一个大纲,因此您应该在适当的时候参考Javadoc和源代码。

AbstractPreAuthenticatedProcessingFilter

这个类将检查安全上下文的当前内容,如果为空,它将尝试从HTTP请求中提取用户信息并将其提交给AuthenticationManager。子类覆盖以下方法以获得该信息:

protected abstract Object getPreAuthenticatedPrincipal(HttpServletRequest request);

protected abstract Object getPreAuthenticatedCredentials(HttpServletRequest request);

调用这些之后,过滤器将创建一个包含返回数据的PreAuthenticatedAuthenticationToken,并提交它进行身份验证。这里所说的“身份验证”,实际上只是指可能加载用户权限的进一步处理,但是遵循标准Spring Security身份验证体系结构。

像其他Spring安全身份验证过滤器,pre-authentication过滤器有一个authenticationDetailsSource属性,默认情况下将在Authentication对象的details属性创建一个WebAuthenticationDetails对象来存储更多的信息,比如会话标识符和原始IP地址。如果用户角色信息可以从预认证机制获得,数据也存储在这个属性中,详细信息实现了GrantedAuthortiesContainer接口。这使身份验证提供者能够读取外部分配给用户的权限。下面我们来看一个具体的例子。

J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource

如果过滤器配置了该类的一个实例authenticationDetailsSource,则通过为每一个预先确定的“可映射角色”集合调用isUserInRole(String role)方法来获得权威信息。这个类从一个配置好的MappableAttributesRetriever检索器获得这些。可能的实现包括在应用程序上下文中硬编码一个列表,并从web.xml文件中的信息中读取角色信息。身份验证前的示例应用程序使用后一种方法。

还有一个附加阶段,使用配置的Attributes2GrantedAuthortiesMapper将角色(或属性)映射到Spring Security GrantedAuthority对象。默认情况下只会在名称中添加通常的ROLE_前缀,但它让您完全控制行为。

PreAuthenticatedAuthenticationProvider

预先身份验证的提供者除了为用户加载UserDetails对象外,没有什么其他工作要做。它通过委托给AuthenticationUserDetailsService来做到这一点。后者类似于标准的UserDetailsService,但接受一个Authentication对象,而不仅仅是用户名:

public interface AuthenticationUserDetailsService {
    UserDetails loadUserDetails(Authentication token) throws UsernameNotFoundException;
}

这个接口可能还有其他用途,但是使用预身份验证,它允许访问封装在Authentication对象中的权限,正如我们在前一节中看到的那样。PreAuthenticatedGrantedAuthortiesUserDetailsService类可以做到这一点。或者,它可以通过UserDetailsByNameServiceWrapper实现委托给一个标准的UserDetailsService

Http403ForbiddenEntryPoint

AuthenticationEntryPoint负责为未经身份验证的用户(当他们试图访问受保护的资源时)启动身份验证过程,但在预先身份验证的情况下,这并不适用。如果不使用预身份验证和其他身份验证机制结合使用,那么只能使用该类的一个实例来配置ExceptionTranslationFilter。如果用户被AbstractPreAuthenticatedProcessingFilter拒绝,就会调用它,从而导致验证为空。如果被调用,它总是返回一个403禁止的响应码。

具体实现

X.509身份验证将在其单独的一章中介绍。这里我们将介绍一些类,它们为其他预身份验证场景提供支持。

请求头身份验证(Siteminder)

外部身份验证系统可以通过在HTTP请求上设置特定的头向应用程序提供信息。一个著名的例子是Siteminder,它在名为SM_USER的头文件中传递用户名。该类RequestHeaderAuthenticationFilter支持这种机制,它只是从头中提取用户名。它默认使用名称SM_USER作为头名称。有关更多细节,请参阅Javadoc。

这里请注意,在使用这样的系统时,框架根本不执行身份验证检查,正确配置外部系统并保护对应用程序的所有访问是极其重要的。如果攻击者能够在他们的原始请求中伪造报头而不被检测到,那么他们可以选择任何他们想要的用户名。

Siteminder示例配置

使用该过滤器的典型配置如下所示:













    
    
    






这里我们假设安全名称空间用于配置。它还假设您已经添加了一个UserDetailsService(称为“userDetailsService”)到您的配置中,以加载用户的角色。

Java EE容器身份验证

J2eePreAuthenticatedProcessingFilter将从HttpServletRequestuserPrincipal属性中提取用户名。该过滤器的使用通常会与Java EE角色的使用相结合,如上所述的J2eeBasedPreAuthenticatedWebAuthenticationDetailsSource。

在代码库中有一个使用这种方法的示例应用程序,所以从github获得代码,如果你感兴趣,可以看看应用程序上下文文件。代码位于samples/xml/preauth目录中。

Java身份验证和授权服务(JAAS)提供者

概述

Spring Security提供了一个能够将身份验证请求委托给Java身份验证和授权服务(JAAS)的包。这个包将在下面详细讨论。

AbstractJaasAuthenticationProvider

AbstractJaasAuthenticationProvider是所提供的JAAS AuthenticationProvider实现的基础。子类必须实现一个创建LoginContext的方法。AbstractJaasAuthenticationProvider有许多可以注入其中的依赖项,下面将讨论这些依赖项。

JAAS CallbackHandler

TBD

JAAS AuthorityGranter

TBD

DefaultJaasAuthenticationProvider

TBD

InMemoryConfiguration

TBD

DefaultJaasAuthenticationProvider示例配置

TBD

JaasAuthenticationProvider

TBD

Running as a Subject

TBD

中心认证服务认证

概述

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

您可以在https://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应用程序被称为“服务”。有三种类型的服务。验证服务票据的、可以获得代理票据的和验证代理票据的。对代理票据的身份验证不同,因为必须验证代理列表,而且通常可以重用代理票据。

Spring安全性和CAS交互序列

web浏览器、CAS服务器和Spring Security-secure服务之间的基本交互如下:

  • 网络用户正在浏览服务的公共页面。不涉及CAS或Spring Security。
  • 用户最终请求的页面要么是安全的,要么是使用的某个bean是安全的。Spring Security的ExceptionTranslationFilter将检测AccessDeniedExceptionAuthenticationException
  • 因为用户的Authentication对象(或缺少它)导致了AuthenticationException, ExceptionTranslationFilter将调用配置好的AuthenticationEntryPoint。如果使用CAS,这将是CasAuthenticationEntryPoint类。
  • CasAuthenticationEntryPoint将用户的浏览器重定向到CAS服务器。它还将指示一个service参数,该参数是Spring Security服务(您的应用程序)的回调URL。例如,浏览器被重定向到的URL可能是https://my.company.com/cas/login?service=https%3A%2F%2Fserver3.company.com%2Fwebapp%2Flogin/cas
  • 在用户的浏览器重定向到CAS之后,将提示用户输入用户名和密码。如果用户显示了一个会话cookie,表明他们以前已经登录过,那么将不会提示他们再次登录(这个过程有一个例外,我们将在后面讨论)。CAS将使用上面讨论的PasswordHandler(如果使用CAS 3.0,则使用AuthenticationHandler)来决定用户名和密码是否有效。
  • 成功登录后,CAS将用户的浏览器重定向回原来的服务。它还将包含一个ticket参数,这是一个表示“服务票证”的不透明字符串。继续我们前面的示例,浏览器重定向到的URL可能是https://server3.company.com/webapp/login/cas?ticket=ST-0-ER94xMJmn6pha35CQRoZ
  • 回到服务web应用程序中,CasAuthenticationFilter总是侦听对/login/cas的请求(这是可配置的,但在本介绍中我们将使用默认值)。处理过滤器将构造一个UsernamePasswordAuthenticationToken,表示服务票据。主体将等于CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER,而凭据将是服务票据不透明值。然后将此身份验证请求传递给已配置的AuthenticationManager
  • AuthenticationManager实现将是ProviderManager,而ProviderManager又是用CasAuthenticationProvider配置的。CasAuthenticationProvider只响应UsernamePasswordAuthenticationToken,其中包含特定于cas的主体(例如CasAuthenticationFilter.CAS_STATEFUL_IDENTIFIER)和CasAuthenticationToken(稍后讨论)。
  • CasAuthenticationProvider将使用TicketValidator实现验证服务票据。这通常是一个Cas20ServiceTicketValidator,它是CAS客户端库中包含的类之一。如果应用程序需要验证代理票据,则使用Cas20ProxyTicketValidatorTicketValidator向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
  • 在CAS服务器上,验证请求将被接收。如果提供的服务票证与票证发出到的服务URL匹配,CAS将用XML提供一个肯定的响应,表示用户名。如果身份验证中涉及任何代理(将在下面讨论),XML响应中还包括代理列表。
  • [可选]如果对CAS验证服务的请求包含代理回调URL(在pgtUrl参数中),CAS将在XML响应中包含一个pgtIou字符串。这个pgtIou表示代理授权票据IOU。然后CAS服务器将创建自己的HTTPS连接回pgtUrl。这是为了相互验证CAS服务器和声明的服务URL。HTTPS连接将用于向原始web应用程序发送代理授权票据。例如,https://server3.company.com/webapp/login/cas/proxyreceptor?pgtIou=PGTIOU-0-R0zlgrl4pdAQwBvJWO3vnNpevwqStbSGcq3vKB2SqSFFRnjPHt&pgtId=PGT-1-si9YkkHLrtACBo64rmsi3v2nf7cpCResXg5MpESZFArbaZiOKH
  • Cas20TicketValidator将解析从CAS服务器接收到的XML。它将向CasAuthenticationProvider返回一个TicketResponse,其中包括用户名(强制)、代理列表(如果涉及)和代理授权票据IOU(如果请求了代理回调)。
  • 接下来CasAuthenticationProvider将调用配置好的CasProxyDeciderCasProxyDecider指示TicketResponse中的代理列表是否为服务所接受。Spring Security提供了几个实现:RejectProxyTickets, AcceptAnyCasProxyNamedCasProxyDecider。除了NamedCasProxyDecider允许提供受信任的代理List表外,这些名称基本上是不言自明的。
  • CasAuthenticationProvider接下来将请求AuthenticationUserDetailsService加载应用于Assertion中包含的用户的GrantedAuthority对象。
  • 如果没有问题,CasAuthenticationProvider将构造一个CasAuthenticationToken,其中包含TicketResponseGrantedAuthority中包含的详细信息。
  • 然后控制返回到CasAuthenticationFilter,它将创建的CasAuthenticationToken放入安全上下文中。
  • 用户的浏览器被重定向到导致AuthenticationException的原始页面(或根据配置自定义的目的地)。

你还在这里真是太好了!现在让我们看看如何配置它

配置CAS客户端

由于Spring安全性,CAS的web应用端变得很容易。假定您已经知道使用Spring Security的基本知识,因此下面不再讨论这些内容。我们将假设正在使用基于名称空间的配置,并根据需要添加CAS bean。每一节都以前一节为基础。完整的CAS示例应用程序可以在Spring Security Samples中找到。

服务票证的身份验证

本节描述如何设置Spring Security来验证服务票据。通常这就是web应用程序所需要的。您需要将ServiceProperties bean添加到您的应用程序上下文中。这代表你的CAS服务:





service必须等于CasAuthenticationFilter将监视的URL。sendRenew默认值为false,但如果应用程序特别敏感,则应该设置为true。该参数的作用是告诉CAS登录服务登录时的单次登录是不可接受的。相反,用户需要重新输入用户名和密码才能访问该服务。

应该配置以下bean以开始CAS身份验证过程(假设您正在使用名称空间配置):


...











为了让CAS操作,ExceptionTranslationFilter必须将其authenticationEntryPoint属性设置为CasAuthenticationEntryPoint bean。这可以很容易地使用entry-point-ref来完成,就像上面的例子一样。CasAuthenticationEntryPoint必须引用ServiceProperties bean(上面讨论过),ServiceProperties bean为企业的CAS登录服务器提供URL。这是用户的浏览器将被重定向的地方。

CasAuthenticationFilter具有与UsernamePasswordAuthenticationFilter(用于基于表单的登录)非常相似的属性。您可以使用这些属性来定制身份验证成功和失败的行为等内容。

接下来,你需要添加CasAuthenticationProvider和它的合作者:







    
    
    



    
    
    







...

CasAuthenticationProvider使用UserDetailsService实例为用户加载授权,一旦用户通过CAS进行身份验证。我们在这里展示了一个简单的内存设置。请注意,CasAuthenticationProvider实际上并不使用密码进行身份验证,但它使用了权限。

如果您回看CAS的工作原理一节,那么这些bean都是很容易解释的。

这就完成了CAS的最基本配置。如果您没有犯任何错误,那么您的web应用程序应该能够在CAS单点登录框架内愉快地工作。Spring Security的其他部分不需要考虑CAS处理身份验证这一事实。在下面的部分中,我们将讨论一些(可选的)更高级的配置。

单点注销

CAS协议支持单次登出,可以很容易地添加到Spring Security配置中。下面是对处理Single Logout的Spring Security配置的更新


...












    



logout元素使用户退出本地应用程序,但不结束与CAS服务器或已经登录到的任何其他应用程序的会话。requestSingleLogoutFilter过滤器将允许请求/spring_security_cas_logout的URL,将应用程序重定向到配置的CAS服务器注销URL。然后CAS服务器将向所有已签名的服务发送一个Single Logout请求。singleLogoutFilter通过在静态Map中查找HttpSession,然后使其失效来处理Single Logout请求。

为什么同时需要logout元素和singleLogoutFilter,这可能会让人感到困惑。最好的做法是先本地注销,因为SingleSignOutFilter只是将HttpSession存储在静态Map中,以便在其上调用invalidate。使用上面的配置,注销流程将是:

  • 用户请求/logout将使用户退出本地应用程序,并将用户发送到注销成功页面。
  • 注销成功页面/cas-logout.jsp应该指示用户单击指向/logout/cas的链接以注销所有应用程序。
  • 当用户单击链接时,用户被重定向到CAS单注销URL (https://localhost:9443/cas/logout)。
  • 在CAS Server端,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将无状态服务设置为Authenticate。

配置CAS获取代理授权票据

为了对无状态服务进行身份验证,应用程序需要获得代理授权票证(PGT)。本节介绍如何配置Spring Security,以获得在thencas-st[服务票证验证]配置上的PGT构建。

第一步是在Spring Security配置中包含一个ProxyGrantingTicketStorage。它用于存储由CasAuthenticationFilter获得的PGT,以便它们可以用于获取代理票据。下面显示了一个配置示例



下一步是更新CasAuthenticationProvider,使其能够获得代理票据。为此,将Cas20ServiceTicketValidator替换为Cas20ProxyTicketValidatorproxyCallbackUrl应该设置为应用程序将接收PGT所在的URL。最后,配置还应该引用ProxyGrantingTicketStorage,这样它就可以使用PGT来获得代理票据。您可以在下面找到一个配置更改的示例。


...

    
    
        
    
    


最后一步是更新CasAuthenticationFilter以接受PGT,并将它们存储在ProxyGrantingTicketStorage中。proxyReceptorUrl匹配Cas20ProxyTicketValidatorproxyCallbackUrl是很重要的。下面显示了一个配置示例。


    ...
    
    

使用代理票据调用无状态服务

现在Spring Security获得了PGT,您可以使用它们来创建代理票据,代理票据可以用于对无状态服务进行身份验证。CAS示例应用程序在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");
...
}
代理身份验证票

CasAuthenticationProvider区分有状态和无状态客户端。有状态客户端被认为是任何提交到CasAuthenticationFilterfilterProcessUrl的客户端。无状态客户端是指在URL(而不是filterProcessUrl)上向CasAuthenticationFilter提出身份验证请求的任何客户端。

因为远程协议没有办法在HttpSession的上下文中显示自己,所以不可能依赖于在请求之间的会话中存储安全上下文的默认实践。此外,由于CAS服务器将在TicketValidator验证后使票据失效,因此在后续请求中呈现相同的代理票据将无法工作。

一个明显的选择是对远程协议客户机根本不使用CAS。然而,这将消除CAS的许多理想特性。作为一个中间地带,CasAuthenticationProvider使用一个StatelessTicketCache。这仅用于使用等同于CasAuthenticationFilter.CAS_STATELESS_IDENTIFIER主体的无状态客户端。CasAuthenticationProvider将结果CasAuthenticationToken存储在StatelessTicketCache中,并在代理票据上键入键。因此,远程协议客户端可以提供相同的代理票据,CasAuthenticationProvider将不需要联系CAS服务器进行验证(除了第一个请求)。一旦经过身份验证,代理票证就可以用于原始目标服务之外的其他url。

本节构建在前面的基础上,以适应代理票证身份验证。第一步是指定对所有工件进行身份验证,如下所示。


...


下一步是为CasAuthenticationFilter指定servicePropertiesauthenticationDetailsSourceserviceProperties属性指示CasAuthenticationFilter尝试验证所有工件,而不是仅验证filterProcessUrl上的工件。ServiceAuthenticationDetailsSource创建一个ServiceAuthenticationDetails,以确保在验证票据时使用基于HttpServletRequest的当前URL作为服务URL。生成服务URL的方法可以通过注入一个返回自定义ServiceAuthenticationDetails的自定义AuthenticationDetailsSource来定制。


...


    
    
    


您还需要更新CasAuthenticationProvider来处理代理票据。为此,将Cas20ServiceTicketValidator替换为Cas20ProxyTicketValidator。您需要配置statelessTicketCache以及您想要接受的代理。您可以在下面找到一个接受所有代理所需的更新示例。


...

    
    
    
    


    
    
        
        
        
        
        
        
        
        
    
    


X.509认证

概述

X.509证书身份验证最常用的用法是在使用SSL时验证服务器的身份,最常用的用法是在浏览器中使用HTTPS时。浏览器将自动检查服务器提供的证书是否已由它所维护的可信证书颁发机构之一颁发(即数字签名)。

你也可以使用SSL进行“双向认证”;然后,服务器将从客户机请求一个有效的证书,作为SSL握手的一部分。服务器将通过检查客户端证书是否由可接受的权威机构签名来验证客户端。如果提供了有效的证书,可以通过应用程序中的servlet API获得它。Spring Security X.509模块使用过滤器提取证书。它将证书映射到应用程序用户,并加载该用户所授予的权限集,以便与标准Spring Security基础设施一起使用。

在尝试将它与Spring Security一起使用之前,您应该熟悉如何使用证书并为您的servlet容器设置客户机身份验证。大部分工作都是创建和安装合适的证书和密钥。例如,如果您正在使用Tomcat,请阅读这里的说明https://tomcat.apache.org/tomcat-9.0-doc/ssl-howto.html。在使用Spring Security试用它之前,让它工作是很重要的

向Web应用程序添加X.509身份验证

启用X.509客户机身份验证非常简单。只需将元素添加到http安全名称空间配置中。


...
    ;

元素有两个可选属性:

  • subject-principal-regex。用于从证书的主题名称中提取用户名的正则表达式。默认值如上所示。这是将被传递给UserDetailsService以加载用户权限的用户名。
  • user-service-ref。这是将与X.509一起使用的UserDetailsService的bean Id。如果您的应用程序上下文中只定义了一个,则不需要它。

subject-principal-regex应该包含单个组。例如,默认表达式“CN=(.?)”匹配公共名称字段。因此,如果证书中的主题名是“CN=Jimi Hendrix, OU=”,则会得到一个用户名“Jimi Hendrix”。匹配不区分大小写。所以“emailAddress=(.?),”将匹配“emailAddress= [email protected],CN=”,给出一个用户名“[email protected]”。如果客户端提供了一个证书,并且成功提取了有效的用户名,那么在安全上下文中应该有一个有效的Authentication对象。如果没有找到证书,或者没有找到对应的用户,则安全上下文将保持为空。这意味着您可以轻松地将X.509身份验证与其他选项(如基于表单的登录)一起使用。

在Spring Security项目的samples/certificate目录中有一些预生成的证书。如果不希望生成自己的SSL,可以使用它们来启用SSL进行测试。文件server.jks包含服务器证书、私钥和颁发证书的权威证书。还有一些用于示例应用程序的用户的客户机证书文件。您可以在浏览器中安装这些组件,以启用SSL客户机身份验证。

要运行具有SSL支持的tomcat,请导入server.jks文件到tomcat conf目录中,并将以下连接器添加到server.xml文件中


如果您仍然希望SSL连接成功,即使客户端不提供证书,也可以将clientAuth设置为want。不提供证书的客户端将不能访问任何受Spring Security保护的对象,除非您使用非x.509认证机制,如表单认证。

Run-As验证替换

概述

AbstractSecurityInterceptor能够在安全对象回调阶段临时替换SecurityContextSecurityContextHolder中的Authentication对象。只有AuthenticationManagerAccessDecisionManager成功处理了原始的Authentication对象时才会发生这种情况。RunAsManager将指示应该在SecurityInterceptorCallback期间使用的替换Authentication对象(如果有的话)。

通过在安全对象回调阶段临时替换Authentication对象,安全调用将能够调用其他需要不同身份验证和授权凭证的对象。它还能够对特定的GrantedAuthority对象执行任何内部安全检查。因为Spring Security提供了许多帮助器类,它们基于SecurityContextHolder的内容自动配置远程协议,所以这些run-as替换在调用远程web服务时特别有用

配置

Spring Security提供了一个RunAsManager接口:

Authentication buildRunAs(Authentication authentication, Object object,
    List config);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

第一个方法返回Authentication对象,该对象在方法调用期间应该替换现有的Authentication对象。如果该方法返回null,则表示不应进行替换。AbstractSecurityInterceptor使用第二种方法作为配置属性的启动验证的一部分。安全拦截器实现调用supports(Class)方法,以确保配置的RunAsManager支持安全拦截器将提供的安全对象类型。

Spring Security提供了RunAsManager的一个具体实现。如果任何ConfigAttributeRUN_AS_开头,RunAsManagerImpl类返回一个替换的RunAsUserToken。如果找到任何这样的ConfigAttribute,则替换的RunAsUserToken将包含与原始Authentication对象相同的主体、凭据和已授予的权限,以及每个RUN_AS_ ConfigAttribute的新的SimpleGrantedAuthority。每个新的SimpleGrantedAuthority都将加上ROLE_前缀,然后是RUN_AS ConfigAttribute。例如,RUN_AS_SERVER将导致替换RunAsUserToken,其中包含授予的ROLE_RUN_AS_SERVER权限。

替换的RunAsUserToken就像任何其他Authentication对象一样。它需要由AuthenticationManager进行身份验证,可能通过委托给合适的AuthenticationProviderRunAsImplAuthenticationProvider执行这样的身份验证。它只是接受任何提供的RunAsUserToken为有效。

为了确保恶意代码不会创建RunAsUserToken并将其提供给RunAsImplAuthenticationProvider以保证接受,密钥的哈希值存储在所有生成的令牌中。RunAsManagerImplRunAsImplAuthenticationProvider是在bean上下文中使用相同的键创建的:








通过使用相同的密钥,每个RunAsUserToken都可以验证它是由经过批准的RunAsManagerImpl创建的。出于安全原因,RunAsUserToken在创建之后是不可变的

处理注销

注销Java/Kotlin配置

当使用WebSecurityConfigurerAdapter时,会自动应用注销功能。默认情况下,访问URL/logout将通过以下方式注销用户:

  • 使HTTP会话失效
  • 清除已配置的任何RememberMe身份验证
  • 清理SecurityContextHolder
  • 重定向到/login?logout

然而,与配置登录功能类似,您也有各种选项来进一步定制您的注销需求:

protected void configure(HttpSecurity http) throws Exception {
    http
        .logout(logout -> logout                                                
            .logoutUrl("/my/logout")                                            
            .logoutSuccessUrl("/my/index")                                      
            .logoutSuccessHandler(logoutSuccessHandler)                         
            .invalidateHttpSession(true)                                        
            .addLogoutHandler(logoutHandler)                                    
            .deleteCookies(cookieNamesToClear)                                  
        )
        ...
}
  1. 提供注销的支持。
    这在使用WebSecurityConfigurerAdapter时自动应用。
  2. 触发注销发生的URL(默认是/logout)。
    如果启用了CSRF保护(默认),那么请求也必须是POST。
    要了解更多信息,请咨询JavaDoc。
  3. 注销后要重定向到的URL。
    默认为/login?logout
    要了解更多信息,请咨询JavaDoc。
  4. 让我们指定一个自定义的LogoutSuccesssHandler
    如果指定了这个参数,logoutSuccessUrl()将被忽略。
    要了解更多信息,请咨询JavaDoc。
  5. 指定在注销时是否使HttpSession失效。
    默认情况下是true
    在下面配置SecurityContextLogoutHandler
    要了解更多信息,请咨询JavaDoc。
  6. 添加一个LogoutHandler
    默认情况下,SecurityContextLogoutHandler被添加为最后一个LogoutHandler
  7. 允许指定注销成功时要删除的cookie名称。
    这是显式添加CookieClearingLogoutHandler的快捷方式。

当然,注销也可以使用XML Namespace表示法进行配置。请参阅Spring Security XML Namespace部分中登出元素的文档以获得更多详细信息。

通常,为了定制注销功能,您可以添加LogoutHandler和/或LogoutSuccessHandler实现。对于许多常见的场景,这些处理程序是在使用流畅API时在底层应用的。

注销XML配置

logout元素添加了通过导航到特定URL来登出的支持。默认的注销URL是/logout,但是您可以使用logout-url属性将其设置为其他内容。关于其他可用属性的更多信息可以在名称空间附录中找到。

LogoutHandler

通常,LogoutHandler实现指出能够参与注销处理的类。他们将被要求进行必要的清理工作。因此,它们不应该抛出异常。提供了各种实现:

  • PersistentTokenBasedRememberMeServices
  • TokenBasedRememberMeServices
  • CookieClearingLogoutHandler
  • CsrfLogoutHandler
  • SecurityContextLogoutHandler
  • HeaderWriterLogoutHandler

请参阅Remember-Me接口和实现的详细信息。

流畅的API并没有直接提供LogoutHandler实现,而是提供了在幕后提供各自LogoutHandler实现的快捷方式。例如deleteCookies()允许指定登出成功时要删除的一个或多个cookie的名称。与添加一个CookieClearingLogoutHandler相比,这是一个快捷方式。

LogoutSuccessHandler

LogoutFilter成功注销后调用LogoutSuccessHandler,以处理例如重定向或转发到适当的目的地。注意,该接口几乎与LogoutHandler相同,但可能引发异常。

提供了以下实现:

  • SimpleUrlLogoutSuccessHandler
  • HttpStatusReturningLogoutSuccessHandler

如上所述,您不需要直接指定SimpleUrlLogoutSuccessHandler。相反,流畅API通过设置logoutSuccessUrl()提供了一种快捷方式。这将在幕后设置SimpleUrlLogoutSuccessHandler。所提供的URL将在登出后重定向到。默认为/login?logout

HttpStatusReturningLogoutSuccessHandler在REST API类型的场景中可能很有趣。此LogoutSuccessHandler允许您提供一个要返回的普通HTTP状态代码,而不是在成功登出时重定向到一个URL。如果没有配置,则默认返回状态码200。

进一步Logout-Related引用

  • 注销处理
  • 测试注销
  • HttpServletRequest.logout()
  • 记住我的接口和实现
  • 在CSRF部分登出警告
  • Spring Security XML Namespace部分中登出元素的文档

认证事件

对于每一个成功或失败的身份验证,将分别触发一个AuthenticationSuccessEventAuthenticationFailureEvent

要侦听这些事件,您必须首先发布一个AuthenticationEventPublisher。Spring Security的DefaultAuthenticationEventPublisher可能会做得很好:

@Bean
public AuthenticationEventPublisher authenticationEventPublisher
        (ApplicationEventPublisher applicationEventPublisher) {
    return new DefaultAuthenticationEventPublisher(applicationEventPublisher);
}

然后,你可以使用Spring的@EventListener支持:

@Component
public class AuthenticationEvents {
    @EventListener
    public void onSuccess(AuthenticationSuccessEvent success) {
        // ...
    }

    @EventListener
    public void onFailure(AuthenticationFailureEvent failures) {
        // ...
    }
}

虽然类似于AuthenticationSuccessHandlerAuthenticationFailureHandler,但它们很好,因为它们可以独立于servlet API使用。

添加异常映射

默认情况下,DefaultAuthenticationEventPublisher将为以下事件发布一个AuthenticationFailureEvent:

异常 事件
BadCredentialsException AuthenticationFailureBadCredentialsEvent
UsernameNotFoundException AuthenticationFailureBadCredentialsEvent
AccountExpiredException AuthenticationFailureExpiredEvent
ProviderNotFoundException AuthenticationFailureProviderNotFoundEvent
DisabledException AuthenticationFailureDisabledEvent
LockedException AuthenticationFailureLockedEvent
AuthenticationServiceException AuthenticationFailureServiceExceptionEvent
CredentialsExpiredException AuthenticationFailureCredentialsExpiredEvent
InvalidBearerTokenException AuthenticationFailureBadCredentialsEvent

发布者执行一个精确的Exception匹配,这意味着这些异常的子类也不会产生事件。

为此,你可能想通过setAdditionalExceptionMappings方法为发布者提供额外的映射:

@Bean
public AuthenticationEventPublisher authenticationEventPublisher
        (ApplicationEventPublisher applicationEventPublisher) {
    Map,
        Class> mapping =
            Collections.singletonMap(FooException.class, FooEvent.class);
    AuthenticationEventPublisher authenticationEventPublisher =
        new DefaultAuthenticationEventPublisher(applicationEventPublisher);
    authenticationEventPublisher.setAdditionalExceptionMappings(mapping);
    return authenticationEventPublisher;
}

默认事件

并且,在任何AuthenticationException的情况下,你可以提供一个捕获所有事件来触发:

@Bean
public AuthenticationEventPublisher authenticationEventPublisher
        (ApplicationEventPublisher applicationEventPublisher) {
    AuthenticationEventPublisher authenticationEventPublisher =
        new DefaultAuthenticationEventPublisher(applicationEventPublisher);
    authenticationEventPublisher.setDefaultAuthenticationFailureEvent
        (GenericAuthenticationFailureEvent.class);
    return authenticationEventPublisher;
}

授权

Spring Security中的高级授权功能是其流行的最有力的原因之一。无论选择如何进行身份验证——是使用Spring Security提供的机制和提供者,还是与容器或其他非Spring Security身份验证机构集成——您都会发现,可以以一致而简单的方式在应用程序中使用授权服务。

在本部分中,我们将研究在第1部分中介绍的不同的AbstractSecurityInterceptor实现。然后,我们将研究如何通过使用域访问控制列表来优化授权。

授权体系结构

权限

Authentication,讨论了所有Authentication实现如何存储GrantedAuthority对象列表。这些代表了授予主体的权力。GrantedAuthority对象被AuthenticationManager插入到Authentication对象中,稍后AccessDecisionManager在作出授权决策时读取这些对象。

GrantAuthority是一个只有一个方法的接口:

String getAuthority();

此方法允许AccessDecisionManager获得GrantedAuthority的精确String表示。通过返回字符串表示,大多数AccessDecisionManager可以轻松地“读取”一个GrantedAuthority。如果一个GrantedAuthority不能精确地表示为String,则被认为是“复杂的”,getAuthority()必须返回null

“复杂的”GrantedAuthority的一个例子是存储应用于不同客户帐号的操作和权限阈值列表的实现。将这个复杂的GrantedAuthority表示为字符串非常困难,因此getAuthority()方法应该返回null。这将向任何AccessDecisionManager表明,它需要专门支持GrantedAuthority实现,以便理解其内容。

Spring Security包括一个具体的GrantedAuthority实现,SimpleGrantedAuthority。这允许将任何用户指定的String转换为GrantedAuthority。安全体系结构中包含的所有AuthenticationProvider都使用SimpleGrantedAuthority来填充Authentication对象。

前置-调用处理

Spring Security提供了拦截器来控制对安全对象(如方法调用或web请求)的访问。关于是否允许继续调用的前置调用决策是由AccessDecisionManager作出的。

The AccessDecisionManager

AccessDecisionManagerAbstractSecurityInterceptor调用,负责做出最终的访问控制决策。AccessDecisionManager接口包含三个方法:

void decide(Authentication authentication, Object secureObject,
    Collection attrs) throws AccessDeniedException;

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

AccessDecisionManagerdecide方法被传递了它需要的所有相关信息,以便做出授权决策。特别是,传递安全Object可以检查实际安全对象调用中包含的那些参数。例如,让我们假设安全对象是MethodInvocation。可以很容易地查询MethodInvocation中的任何Customer参数,然后在AccessDecisionManager中实现某种安全逻辑,以确保允许主体对该客户进行操作。如果访问被拒绝,实现将抛出AccessDeniedException异常。

AbstractSecurityInterceptor在启动时调用supports(ConfigAttribute)方法,以确定AccessDecisionManager是否可以处理传递的ConfigAttribute。安全拦截器实现调用supports(Class)方法,以确保配置的AccessDecisionManager支持安全拦截器将提供的安全对象类型。

基于投票的AccessDecisionManager实现

虽然用户可以实现自己的AccessDecisionManager来控制授权的所有方面,但是Spring Security包含了几个基于投票的AccessDecisionManager实现。Voting Decision Manager演示了相关的类。
spring-security的基本概念和原理_第17张图片
使用这种方法,将根据授权决策轮询一系列AccessDecisionVoter实现。然后AccessDecisionManager根据其对投票的评估决定是否抛出AccessDeniedException

AccessDecisionVoter接口有三个方法:

int vote(Authentication authentication, Object object, Collection attrs);

boolean supports(ConfigAttribute attribute);

boolean supports(Class clazz);

具体的实现返回一个int,可能的值反映在AccessDecisionVoter静态字段ACCESS_ABSTAINACCESS_DENIEDACCESS_GRANTED中。如果投票实现对授权决策没有意见,它将返回ACCESS_ABSTAIN。如果它有一个意见,它必须返回ACCESS_DENIEDACCESS_GRANTED

Spring Security提供了三个具体的AccessDecisionManager来记录投票。ConsensusBased的实现将基于非弃权票的共识批准或拒绝访问。提供属性是为了在投票相等或所有投票弃权的情况下控制行为。如果收到一个或多个ACCESS_GRANTED投票,AffirmativeBased实现将授予访问权限(即,如果至少有一个grant投票,则拒绝投票将被忽略)。与ConsensusBased实现一样,如果所有投票者都弃权,也有一个参数来控制行为。UnanimousBased提供程序期望一致的ACCESS_GRANTED投票来授予访问权,忽略弃权。如果存在任何ACCESS_DENIED投票,它将拒绝访问。与其他实现一样,如果所有投票者都弃权,则有一个参数来控制行为。

可以实现一个自定义的AccessDecisionManager以不同的方式统计投票。例如,来自特定AccessDecisionVoter的投票可能获得额外的权重,而来自特定投票者的否决投票可能具有否决效果。

RoleVoter

Spring Security提供的最常用的AccessDecisionVoter是简单的RoleVoter,它将配置属性视为简单的角色名,如果用户已被分配了该角色,则投票授予访问权。

如果任何以ROLE_前缀开头的ConfigAttribute,它将进行投票。如果有一个GrantedAuthority返回的String表示(通过getAuthority()方法)与一个或多个以ROLE_开头的ConfigAttributes完全相等,它将投票授予访问权限。如果没有任何以ROLE_开头的ConfigAttribute的精确匹配,RoleVoter将投票拒绝访问。如果没有以ROLE_开头的ConfigAttribute,投票者将弃权。

AuthenticatedVoter

我们已经隐式看到的另一个投票者是AuthenticatedVoter,它可以用来区分匿名、完全验证和remember-me验证的用户。许多网站允许在记忆我身份验证下进行某些有限的访问,但要求用户通过登录来确认身份以获得完全的访问权限。

当我们使用属性IS_AUTHENTICATED_ANONYMOUSLY来授予匿名访问时,AuthenticatedVoter正在处理这个属性。有关这个类的更多信息,请参阅Javadoc。

自定义Voters

显然,您还可以实现一个自定义AccessDecisionVoter,并且可以在其中放入任何您想要的访问控制逻辑。它可能特定于您的应用程序(与业务逻辑相关),也可能实现一些安全管理逻辑。例如,你会在Spring网站上找到一篇博客文章,它描述了如何使用投票人实时拒绝帐户被暂停的用户的访问。

后置调用处理

虽然AccessDecisionManager在继续安全对象调用之前由AbstractSecurityInterceptor调用,但一些应用程序需要一种方法来修改安全对象调用实际返回的对象。虽然您可以轻松地实现自己的AOP关注点来实现这一点,但Spring Security提供了一个方便的钩子,它有几个具体的实现,与它的ACL功能集成在一起。

AfterInvocation Implementation演示了Spring Security的AfterInvocationManager及其具体实现。

spring-security的基本概念和原理_第18张图片
像Spring安全性的许多其他部分一样,AfterInvocationManager有一个单独的具体实现,AfterInvocationProviderManager,它对AfterInvocationProvider的列表进行轮询。每个AfterInvocationProvider允许修改返回对象或抛出AccessDeniedException。实际上,多个提供程序可以修改对象,因为前一个提供程序的结果被传递给列表中的下一个提供程序。

请注意,如果您正在使用AfterInvocationManager,您仍然需要配置属性,允许MethodSecurityInterceptorAccessDecisionManager允许操作。

如果您正在使用包含AccessDecisionManager的典型Spring Security实现,那么没有为特定的安全方法调用定义配置属性将导致每个AccessDecisionVoter放弃投票。反过来,如果AccessDecisionManager属性“allowIfAllAbstainDecisions”为false,则会抛出AccessDeniedException异常。您可以通过(i)将“allowIfAllAbstainDecisions”设置为true(尽管通常不建议这样做)或(ii)简单地确保至少有一个配置属性AccessDecisionVoter将投票授予访问权限。后一种(推荐的)方法通常通过ROLE_USERROLE_AUTHENTICATED配置属性来实现。

层次化角色

应用程序中的特定角色应该自动“include”其他角色,这是一个常见的需求。例如,在具有“admin”和“user”角色概念的应用程序中,您可能希望管理员能够做普通用户可以做的所有事情。要实现这一点,您可以确保所有管理员用户也都被分配了“user”角色。或者,您可以修改每个需要“user”角色也包括“admin”角色的访问约束。如果您的应用程序中有很多不同的角色,这可能会变得非常复杂。

角色层次结构的使用允许您配置哪些角色(或权限)应该包括其他角色。Spring Security的RoleVoter的一个扩展版本RoleHierarchyVoter配置了一个RoleHierarchy,从这个RoleHierarchy中它可以获得分配给用户的所有“可到达的权限”。一个典型的配置可能是这样的:


    


    
        
            ROLE_ADMIN > ROLE_STAFF
            ROLE_STAFF > ROLE_USER
            ROLE_USER > ROLE_GUEST
        
    

在这个层次中,我们有四个角色:ROLE_ADMIN>ROLE_STAFF>ROLE_USER>ROLE_GUEST。使用ROLE_ADMIN进行身份验证的用户,在根据使用上述RoleHierarchyVoter配置的AccessDecisionManager评估安全约束时,将表现得好像他们拥有所有四个角色一样。>符号可以被认为是“包括”的意思。

角色层次结构为简化应用程序的访问控制配置数据和/或减少需要分配给用户的权限数量提供了一种方便的方法。对于更复杂的需求,您可能希望在应用程序所需的特定访问权限和分配给用户的角色之间定义一个逻辑映射,并在加载用户信息时在这两者之间进行转换。

使用FilterSecurityInterceptor授权HttpServletRequest

本节以Servlet体系结构和实现为基础,深入研究授权如何在基于Servlet的应用程序中工作。

FilterSecurityInterceptor为HttpServletRequests提供了授权。它作为安全过滤器之一被插入到FilterChainProxy中。

spring-security的基本概念和原理_第19张图片

  1. 首先,FilterSecurityInterceptorSecurityContextHolder获取一个Authentication
  2. 第二,FilterSecurityInterceptor从传递到FilterSecurityInterceptor中的HttpServletRequestHttpServletResponseFilterChain创建一个FilterInvocation
  3. 接下来,它将FilterInvocation传递给SecurityMetadataSource以获得ConfigAttributes
  4. 最后,它将AuthenticationFilterInvocationConfigAttributes传递给AccessDecisionManager
  5. 如果拒绝授权,则抛出AccessDeniedException。在本例中,ExceptionTranslationFilter处理AccessDeniedException
  6. 如果访问被授予,FilterSecurityInterceptor继续使用FilterChain,这允许应用程序正常处理。

默认情况下,Spring Security的授权将要求对所有请求进行身份验证。显式配置如下:

protected void configure(HttpSecurity http) throws Exception {
    http
        // ...
        .authorizeRequests(authorize -> authorize
            .anyRequest().authenticated()
        );
}

我们可以通过按优先级添加更多规则来配置Spring Security,使其具有不同的规则。

protected void configure(HttpSecurity http) throws Exception {
    http
        // ...
        .authorizeRequests(authorize -> authorize                                  
            .mvcMatchers("/resources/**", "/signup", "/about").permitAll()         
            .mvcMatchers("/admin/**").hasRole("ADMIN")                             
            .mvcMatchers("/db/**").access("hasRole('ADMIN') and hasRole('DBA')")   
            .anyRequest().denyAll()                                                
        );
}
  1. 指定了多个授权规则。
    每个规则都是按照它们被声明的顺序考虑的。
  2. 我们指定了任何用户都可以访问的多个URL模式。
    具体来说,如果URL以"/resources/“开头,等于”/sign “或等于”/about",则任何用户都可以访问请求。
  3. 任何以“/admin/”开头的URL将被限制为具有“ROLE_ADMIN”角色的用户。
    您将注意到,因为我们正在调用hasRole方法,所以不需要指定“ROLE_”前缀。
  4. 任何以"/db/“开头的URL都要求用户同时拥有"ROLE_ADMIN"和"ROLE_DBA”。
    您会注意到,因为我们使用的是hasRole表达式,所以不需要指定“ROLE_”前缀。
  5. 任何尚未匹配的URL都将被拒绝访问。
    如果您不想意外地忘记更新授权规则,那么这是一个很好的策略。

表达式的访问控制

Spring Security 3.0引入了使用Spring EL表达式作为授权机制的能力,除了之前看到的简单使用配置属性和访问决策投票者之外。基于表达式的访问控制构建在相同的架构上,但是允许将复杂的布尔逻辑封装在一个表达式中。

概述

Spring Security使用Spring EL来支持表达式,如果您有兴趣更深入地理解这个主题,那么应该看看它是如何工作的。表达式是用“根对象”作为计算上下文的一部分来计算的。Spring Security使用特定的web和方法安全类作为根对象,以提供内置表达式和对值(如当前主体)的访问。

表达式根对象的基类是SecurityExpressionRoot。这提供了一些在web和方法安全中都可用的通用表达式。

表达式 描述
hasRole(String role) 如果当前主体具有指定的角色,则返回true。例如,hasRole(admin) 默认情况下,如果提供的角色不是以’ROLE_'开头,它将被添加。这可以通过修改DefaultWebSecurityExpressionHandler上的defaultRolePrefix来定制。

网络安全表达式

要使用表达式保护各个url,首先需要将元素中的use-expressions属性设置为true。然后,Spring Security将期望元素的access属性包含Spring EL表达式。表达式的计算值应该是一个布尔值,定义是否允许访问。例如:


    
    ...

这里我们定义了应用程序的“管理”区域(由URL模式定义)应该只对具有授予的权限“admin”且IP地址与本地子网匹配的用户可用。我们已经在前一节中看到了内置的hasRole表达式。hasIpAddress表达式是一个额外的内置表达式,它特定于web安全。它是由WebSecurityExpressionRoot类定义的,它的一个实例在计算web访问表达式时被用作表达式根对象。该对象还直接在名称request下公开HttpServletRequest对象,因此您可以在表达式中直接调用该请求。如果正在使用表达式,那么一个WebExpressionVoter将被添加到命名空间使用的AccessDecisionManager中。因此,如果您不使用名称空间而想要使用表达式,则必须将其中一个添加到您的配置中。

在Web安全表达式中引用bean

如果您希望扩展可用的表达式,您可以很容易地引用您公开的任何Spring Bean。例如,假设你有一个名为webSecurity的Bean,它包含以下方法签名:

public class WebSecurity {
        public boolean check(Authentication authentication, HttpServletRequest request) {
                ...
        }
}

你可以使用以下方法引用该方法:

http
    .authorizeRequests(authorize -> authorize
        .antMatchers("/user/**").access("@webSecurity.check(authentication,request)")
        ...
    )
Web安全表达式中的路径变量

有时,能够在URL中引用路径变量是件好事。例如,考虑一个基于rest的应用程序,它通过/user/{userId}格式的URL路径查找用户。

通过将路径变量放置在模式中,您可以轻松地引用它。例如,如果你有一个名为webSecurity的Bean,它包含以下方法签名:

public class WebSecurity {
        public boolean checkUserId(Authentication authentication, int id) {
                ...
        }
}

你可以使用以下方法引用该方法:

http
    .authorizeRequests(authorize -> authorize
        .antMatchers("/user/{userId}/**").access("@webSecurity.checkUserId(authentication,#userId)")
        ...
    );

在这个配置中,匹配的url将把路径变量传递(并将其转换为checkUserId方法)。例如,如果URL是/user/123/resource,那么传入的id将是123

方法安全性表达式

方法安全性比简单的允许或拒绝规则要复杂一些。Spring Security 3.0引入了一些新的注释,以便全面支持表达式的使用。

@Pre和@Post注释

有四种注释支持表达式属性,以允许调用前和调用后的授权检查,并支持对提交的集合参数或返回值进行过滤。它们是@PreAuthorize@PreFilter@PostAuthorize@PostFilter。它们的使用是通过global-method-security命名空间元素启用的:


使用@PreAuthorize和@PostAuthorize访问控制

最明显有用的注释是@PreAuthorize,它决定是否可以实际调用方法。例如(来自“Contacts”示例应用程序)

@PreAuthorize("hasRole('USER')")
public void create(Contact contact);

这意味着只允许角色为“ROLE_USER”的用户访问。显然,使用传统配置和所需角色的简单配置属性可以很容易地实现相同的功能。但:

@PreAuthorize("hasPermission(#contact, 'admin')")
public void deletePermission(Contact contact, Sid recipient, Permission permission);

这里我们实际上使用了一个方法参数作为表达式的一部分来决定当前用户是否拥有给定联系人的“admin”权限。内置的hasPermission()表达式通过应用程序上下文链接到Spring Security ACL模块,如下所示。您可以通过名称访问任何方法参数作为表达式变量。

Spring Security可以通过多种方式解析方法参数。Spring Security使用DefaultSecurityParameterNameDiscoverer发现参数名称。默认情况下,将对一个方法整体尝试以下选项。

  • 如果Spring Security的@P注释出现在该方法的单个参数上,则将使用该值。这对于使用JDK 8之前的JDK编译的接口很有用,因为JDK不包含任何关于参数名的信息。例如:
import org.springframework.security.access.method.P;

...

@PreAuthorize("#c.name == authentication.name")
public void doSomething(@P("c") Contact contact);

在幕后,这是使用AnnotationParameterNameDiscoverer实现的,它可以自定义以支持任何指定注释的value属性。

  • 如果Spring Data的@Param注释出现在该方法的至少一个参数上,则将使用该值。这对于使用JDK 8之前的JDK编译的接口很有用,因为JDK不包含任何关于参数名的信息。例如:
import org.springframework.data.repository.query.Param;

...

@PreAuthorize("#n == authentication.name")
Contact findContactByName(@Param("n") String name);

在幕后,这是使用AnnotationParameterNameDiscoverer实现的,它可以自定义以支持任何指定注释的value属性。

  • 如果使用JDK 8编译带有-parameters参数的源代码,并且使用Spring 4+,那么将使用标准JDK反射API来发现参数名。这适用于类和接口。
  • 最后,如果代码是用调试符号编译的,则将使用调试符号发现参数名。这对接口不起作用,因为它们没有关于参数名的调试信息。对于接口,必须使用注释或JDK 8。

表达式中可以使用任何Spring-EL功能,因此您也可以访问参数上的属性。例如,如果您希望一个特定的方法只允许访问用户名与联系人的用户名匹配的用户,那么您可以这样写

@PreAuthorize("#contact.name == authentication.name")
public void doSomething(Contact contact);

这里我们访问另一个内置表达式authentication,它是存储在安全上下文中的authentication。您还可以使用表达式principal直接访问它的“principal”属性。值通常是一个UserDetails实例,所以您可以使用像principal.usernameprincipal.enabled这样的表达式。

不太常见的情况是,您可能希望在调用该方法后执行访问控制检查。这可以使用@PostAuthorize注释来实现。要访问方法的返回值,请在表达式中使用内置名称returnObject

使用@PreFilter和@PostFilter进行过滤

Spring Security支持使用表达式过滤集合、数组、映射和流。这通常在方法的返回值上执行。例如:

@PreAuthorize("hasRole('USER')")
@PostFilter("hasPermission(filterObject, 'read') or hasPermission(filterObject, 'admin')")
public List getAll();

当使用@PostFilter注释时,Spring Security遍历返回的集合或映射,并删除所提供表达式为false的任何元素。对于数组,将返回包含筛选过的元素的新数组实例。名称filterObject引用集合中的当前对象。当map被使用时,它将引用当前的Map.Entry对象,这允许在表达式中使用filterObject.keyfilterObject.value。您还可以使用@PreFilter在方法调用之前进行筛选,尽管这是一个不太常见的需求。语法是相同的,但是如果有多个参数是集合类型,那么您必须使用该注释的filterTarget属性按名称选择一个。

注意,筛选显然不能替代数据检索查询的调优。如果您正在过滤大型集合并删除许多条目,那么这可能是低效的。

内置的表达式

有一些特定于方法安全性的内置表达式,我们在上面已经看到了它们的使用。filterTargetreturnValue值非常简单,但是hasPermission()表达式的使用值得进一步研究。

PermissionEvaluator接口

hasPermission()表达式被委托给PermissionEvaluator的实例。它的目的是在表达式系统和Spring Security的ACL系统之间架起桥梁,允许您基于抽象权限对域对象指定授权约束。它对ACL模块没有显式依赖关系,因此如果需要,可以将其替换为其他实现。该接口有两个方法:

boolean hasPermission(Authentication authentication, Object targetDomainObject,
                            Object permission);

boolean hasPermission(Authentication authentication, Serializable targetId,
                            String targetType, Object permission);

它直接映射到表达式的可用版本,但不提供第一个参数(Authentication对象)。第一个是在已经加载了域对象(对其访问进行控制)的情况下使用的。如果当前用户拥有该对象的权限,则expression将返回true。第二个版本用于未加载对象,但其标识符已知的情况。还需要域对象的抽象“类型”说明符,以允许加载正确的ACL权限。传统上这是对象的Java类,但只要它与权限加载方式一致,就不必这样做。

要使用hasPermission()表达式,必须在应用程序上下文中显式配置PermissionEvaluator。这看起来像这样:






    

其中myPermissionEvaluator是实现PermissionEvaluator的bean。通常这将是来自ACL模块的实现,该模块被称为AclPermissionEvaluator。有关更多细节,请参见“Contacts”示例应用程序配置。

方法安全性元注释

您可以使用元注释来保证方法安全性,从而使代码更具可读性。如果您发现在整个代码库中重复相同的复杂表达式,这尤其方便。例如,考虑以下情况:

@PreAuthorize("#contact.name == authentication.name")

我们可以创建一个元注释来代替它,而不是到处重复此操作。

@Retention(RetentionPolicy.RUNTIME)
@PreAuthorize("#contact.name == authentication.name")
public @interface ContactPermission {}

元注释可以用于任何Spring Security方法安全注释。为了与规范保持一致,JSR-250注释不支持元注释。

安全对象的实现

AOP联盟(方法调用)安全拦截器

在Spring Security 2.0之前,保护MethodInvocation需要大量的常规配置。现在推荐的方法是使用名称空间配置。通过这种方式,方法安全基础结构bean将自动为您配置,因此您不需要真正了解实现类。我们将简要介绍这里涉及到的类。

方法安全性通过MethodSecurityInterceptor强制执行,它保护MethodInvocation。根据配置方法的不同,拦截器可能特定于单个bean,也可能在多个bean之间共享。拦截器使用MethodSecurityMetadataSource实例获取应用于特定方法调用的配置属性。MapBasedMethodSecurityMetadataSource用于存储以方法名(可以通配符)为键的配置属性,当使用元素在应用程序上下文中定义这些属性时将在内部使用。其他实现将用于处理基于注释的配置。

明确MethodSecurityInterceptor配置

当然,你可以直接在你的应用上下文中配置MethodSecurityInterceptor,以便与Spring AOP的代理机制一起使用:






    
    
    
    


AspectJ (JoinPoint)安全拦截器

AspectJ安全拦截器与前一节讨论的AOP Alliance安全拦截器非常相似。实际上,我们将在本节中只讨论它们之间的区别。

AspectJ拦截器被命名为AspectJSecurityInterceptor。AOP Alliance安全拦截器依赖于Spring应用程序上下文通过代理将其编织到安全拦截器中,与之不同的是,AspectJSecurityInterceptor是通过AspectJ编译器进行编织的。在同一个应用程序中使用这两种类型的安全拦截器并不少见,AspectJSecurityInterceptor用于域对象实例安全,AOP Alliance MethodSecurityInterceptor用于服务层安全。

让我们首先考虑AspectJSecurityInterceptor是如何在Spring应用程序上下文中配置的:






    
    
    
    


正如您所看到的,除了类名之外,AspectJSecurityInterceptor与AOP Alliance安全拦截器完全相同。实际上,这两个拦截器可以共享相同的securityMetadataSource,因为SecurityMetadataSource使用java.lang.reflect.method,而不是特定于AOP库的类。当然,您的访问决策可以访问相关的特定于AOP库的调用(如MethodInvocationJoinPoint),因此在做出访问决策时可以考虑一系列附加条件(如方法参数)。

接下来需要定义AspectJ方面。例如:

package org.springframework.security.samples.aspectj;

import org.springframework.security.access.intercept.aspectj.AspectJSecurityInterceptor;
import org.springframework.security.access.intercept.aspectj.AspectJCallback;
import org.springframework.beans.factory.InitializingBean;

public aspect DomainObjectInstanceSecurityAspect implements InitializingBean {

    private AspectJSecurityInterceptor securityInterceptor;

    pointcut domainObjectInstanceExecution(): target(PersistableEntity)
        && execution(public * *(..)) && !within(DomainObjectInstanceSecurityAspect);

    Object around(): domainObjectInstanceExecution() {
        if (this.securityInterceptor == null) {
            return proceed();
        }

        AspectJCallback callback = new AspectJCallback() {
            public Object proceedWithObject() {
                return proceed();
            }
        };

        return this.securityInterceptor.invoke(thisJoinPoint, callback);
    }

    public AspectJSecurityInterceptor getSecurityInterceptor() {
        return securityInterceptor;
    }

    public void setSecurityInterceptor(AspectJSecurityInterceptor securityInterceptor) {
        this.securityInterceptor = securityInterceptor;
    }

    public void afterPropertiesSet() throws Exception {
        if (this.securityInterceptor == null)
            throw new IllegalArgumentException("securityInterceptor required");
        }
    }
}

在上面的示例中,安全拦截器将应用于PersistableEntity的每个实例,它是一个没有显示的抽象类(您可以使用任何其他类或切入点表达式)。对于那些好奇的人,需要AspectJCallback,因为proceed();语句只有在around()语句体中才有特殊含义。当AspectJSecurityInterceptor希望目标对象继续时,它会调用这个匿名的AspectJCallback类。

您需要配置Spring来加载方面,并将它与AspectJSecurityInterceptor连接起来。实现这一点的bean声明如下所示:




就是这样!现在您可以在应用程序的任何地方创建bean,使用您认为合适的任何方法(例如new Person();),它们将应用安全拦截器。

方法安全

从版本2.0开始,Spring Security已经大大改进了对为服务层方法添加安全性的支持。它提供了对JSR-250注释安全性的支持,以及框架的原始@Secured注释。从3.0开始,您还可以使用新的基于表达式的注释。您可以使用intercept-methods元素来修饰bean声明,将安全性应用于单个bean,或者可以使用AspectJ样式切入点来保护整个服务层中的多个bean。

我们可以在任何@Configuration实例上使用@EnableGlobalMethodSecurity注释来启用基于注释的安全性。例如,下面将启用Spring Security的@Secured注释。

向方法(在类或接口上)添加注释将相应地限制对该方法的访问。Spring Security的原生注释支持为该方法定义了一组属性。这些将被传递给AccessDecisionManager以便它做出实际的决策:

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

支持JSR-250注释可以使用

@EnableGlobalMethodSecurity(jsr250Enabled = true)
public class MethodSecurityConfig {
// ...
}

它们是基于标准的,允许应用简单的基于角色的约束,但是不具有Spring Security的原生注释的强大功能。要使用新的基于表达式的语法,您将使用

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig {
// ...
}

而等价的Java代码将是

public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

GlobalMethodSecurityConfiguration

有时,您可能需要执行比@EnableGlobalMethodSecurity注释allow所能执行的更复杂的操作。对于这些实例,您可以扩展GlobalMethodSecurityConfiguration,确保@EnableGlobalMethodSecurity注释出现在您的子类上。例如,如果你想提供一个自定义的MethodSecurityExpressionHandler,你可以使用以下配置:

@EnableGlobalMethodSecurity(prePostEnabled = true)
public class MethodSecurityConfig extends GlobalMethodSecurityConfiguration {
    @Override
    protected MethodSecurityExpressionHandler createExpressionHandler() {
        // ... create and return custom MethodSecurityExpressionHandler ...
        return expressionHandler;
    }
}

有关可被覆盖方法的其他信息,请参阅GlobalMethodSecurityConfiguration Javadoc。

元素

此元素用于在应用程序中启用基于注释的安全性(通过在元素上设置适当的属性),还用于将安全性切入点声明组合在一起,这些声明将应用于整个应用程序上下文。你应该只声明一个元素。下面的声明将启用Spring Security的@Secured支持:


向方法(在类或接口上)添加注释将相应地限制对该方法的访问。Spring Security的原生注释支持为该方法定义了一组属性。这些将被传递给AccessDecisionManager以便它做出实际的决策:

public interface BankService {

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account readAccount(Long id);

@Secured("IS_AUTHENTICATED_ANONYMOUSLY")
public Account[] findAccounts();

@Secured("ROLE_TELLER")
public Account post(Account account, double amount);
}

支持JSR-250注释可以使用


它们是基于标准的,允许应用简单的基于角色的约束,但是不具有Spring Security的原生注释的强大功能。要使用新的基于表达式的语法,您将使用


而等价的Java代码将是

public interface BankService {

@PreAuthorize("isAnonymous()")
public Account readAccount(Long id);

@PreAuthorize("isAnonymous()")
public Account[] findAccounts();

@PreAuthorize("hasAuthority('ROLE_TELLER')")
public Account post(Account account, double amount);
}

如果您需要定义简单的规则,而不仅仅是根据用户权限列表检查角色名,那么基于表达式的注释是一个很好的选择。

带注释的方法只对定义为Spring bean的实例(在启用方法安全性的同一应用程序上下文中)安全。如果您希望保护不是由Spring创建的实例(例如,使用new操作符),那么您需要使用AspectJ。

您可以在同一个应用程序中启用多个注释类型,但任何接口或类只能使用一种类型,否则将无法定义行为。如果找到两个适用于特定方法的注释,则只应用其中一个。

使用protect-pointcut添加安全切入点

protect-pointcut的使用尤其强大,因为它允许您仅用一个简单的声明就可以对许多bean应用安全性。考虑以下例子:




这将保护在应用程序上下文中声明的bean上的所有方法,这些bean的类位于com.mycompany包中,类名以“Service”结尾。只有具有ROLE_USER角色的用户才能调用这些方法。与URL匹配一样,最特定的匹配必须放在切入点列表的前面,因为将使用第一个匹配表达式。安全注释优先于切入点。

域对象安全(acl)

概述

复杂的应用程序通常需要定义访问权限,而不是简单地在web请求或方法调用级别上定义访问权限。相反,安全决策需要包含who (Authentication)、where (MethodInvocation)和what (SomeDomainObject)。换句话说,授权决策还需要考虑方法调用的实际域对象实例主题。

假设您正在为一家宠物诊所设计一个应用程序。基于spring的应用程序主要有两组用户:宠物诊所的工作人员,以及宠物诊所的客户。工作人员将有权访问所有数据,而您的客户将只能看到他们自己的客户记录。为了让它更有趣一点,您的客户可以允许其他用户查看他们的客户记录,比如他们的“幼犬学前班”导师或当地“Pony Club”的主席。以Spring Security为基础,您可以使用以下几种方法:

  1. 编写业务方法来加强安全性。您可以查询Customer域对象实例中的集合,以确定哪些用户具有访问权限。通过使用SecurityContextHolder.getContext().getAuthentication(),您将能够访问Authentication对象。
  2. 写一个AccessDecisionVoter来从存储在Authentication对象中的GrantedAuthority[]强制安全性。这意味着AuthenticationManager需要使用自定义的GrantedAuthority[]来填充Authentication,这些自定义的GrantAuthority[]表示主体可以访问的每个Customer域对象实例。
  3. 编写AccessDecisionVoter来强制安全性并直接打开目标Customer域对象。这意味着投票者需要访问一个允许它检索Customer对象的DAO。然后,它将访问Customer对象的已批准用户集合,并做出适当的决策。

每一种方法都是完全合法的。但是,第一种方法将授权检查与业务代码结合起来。这方面的主要问题包括单元测试的难度增加,以及在其他地方重用Customer授权逻辑将变得更加困难。从Authentication对象获取GrantAuthority[]也可以,但是不能扩展到大量的Customer。如果一个用户可能能够访问5000个Customer(在本例中不可能,但是想象一下,如果它是一个大型Pony Club的流行兽医!),那么构造Authentication对象所消耗的内存量和所需的时间将是不受欢迎的。最后一种方法,直接从外部代码打开Customer,可能是这三种方法中最好的。它实现了关注点的分离,并且不会误用内存或CPU周期,但是它仍然是低效的,因为AccessDecisionVoter和最终的业务方法本身都将执行对负责检索Customer对象的DAO的调用。每个方法调用两次访问显然是不可取的。此外,对于列出的每种方法,您都需要从头编写自己的访问控制列表(ACL)持久性和业务逻辑。

幸运的是,还有另一种选择,我们将在下面讨论。

关键概念

Spring Security的ACL服务在spring-security-acl-xxx.jar中提供。您需要将这个JAR添加到您的类路径中,以使用Spring Security的域对象实例安全功能。

Spring Security的域对象实例安全功能以访问控制列表(ACL)的概念为中心。系统中的每个域对象实例都有自己的ACL, ACL记录了谁可以和不能使用该域对象的详细信息。考虑到这一点,Spring Security为您的应用程序提供了三个与acl相关的主要功能:

  • 一种有效检索所有域对象的ACL条目(并修改这些ACL)的方法
  • 一种确保在调用方法之前允许给定主体使用对象的方法
  • 一种确保给定主体在调用方法后可以使用对象(或它们返回的东西)的方法

正如第一个要点所指出的,Spring Security ACL模块的主要功能之一是提供检索ACL的高性能方法。这个ACL存储库功能非常重要,因为系统中的每个域对象实例可能有多个访问控制项,并且每个ACL可能以树状结构从其他ACL继承(Spring Security可以开箱即用,并且非常常用)。Spring Security的ACL功能经过精心设计,提供了高性能的ACL检索,以及可插入的缓存、最小化死锁的数据库更新、独立于ORM框架(我们直接使用JDBC)、适当的封装和透明的数据库更新。

已知数据库是ACL模块操作的中心,让我们研究一下实现中默认使用的四个主要表。在一个典型的Spring Security ACL部署中,表的大小顺序如下所示,其中行数最多的表列在最后:

  • ACL_SID允许我们唯一地标识系统中的任何主体或权限(“SID”代表“安全标识”)。唯一的列是ID、SID的文本表示,以及指示文本表示是引用主体名称还是引用GrantedAuthority的标志。因此,每个惟一主体或GrantedAuthority对应一行。当在接收权限的上下文中使用时,SID通常被称为“接收者”。
  • ACL_CLASS允许我们唯一地标识系统中的任何域对象类。唯一的列是ID和Java类名。因此,我们希望为每个惟一的Class存储ACL权限,每一行对应一行。
  • ACL_OBJECT_IDENTITY存储系统中每个惟一域对象实例的信息。列包含ID, ACL_CLASS表的外键,所以我们知道唯一标识符ACL_CLASS实例我们提供信息,父,ACL_SID表的外键表示域对象实例的所有者,以及我们是否允许ACL条目继承任何父ACL。对于存储ACL权限的每个域对象实例,我们有一行。
  • 最后,ACL_ENTRY存储分配给每个收件人的单独权限。列包括ACL_OBJECT_IDENTITY的外键(即ACL_SID的外键),无论是否进行审计,以及表示授予或拒绝的实际权限的整数位掩码。我们为每个接收到使用域对象权限的接收者设置了一行。

正如上一段提到的,ACL系统使用整数位屏蔽。不要担心,您不需要了解使用ACL系统时位转移的细节,但可以这样说,我们有32位可以打开或关闭。每一个位代表权限,默认情况下,权限阅读(0),写(1),创建(2),删除(第3位)和管理(4)。很容易实现自己的如果你想使用其他的权限,允许实例和剩余的ACL框架将没有知识的扩展。

重要的是要理解,系统中域对象的数量与我们选择使用整数位屏蔽的事实绝对没有关系。虽然您有32位可用于权限,但您可能有数十亿域对象实例(这意味着ACL_OBJECT_IDENTITY中有数十亿行,很可能还有ACL_ENTRY)。我们之所以提出这一点,是因为我们发现有时人们会错误地认为他们需要为每个潜在的域对象分配一点,但事实并非如此。

现在,我们已经基本概述了ACL系统的功能,以及它在表结构上的样子,接下来让我们研究一下关键的接口。关键接口有:

  • Acl:每个域对象都有且只有一个Acl对象,该对象内部保存AccessControlEntry,并知道Acl的所有者。Acl不直接引用域对象,而是引用ObjectIdentityAcl存储在ACL_OBJECT_IDENTITY表中。
  • AccessControlEntry:一个Acl包含多个AccessControlEntry,在框架中通常缩写为ACEs。每个ACE都指向一个特定的PermissionSidAcl元组。ACE也可以是授予的或非授予的,并且包含审计设置。ACE存储在ACL_ENTRY表中。
  • Permission:权限表示特定的不可变位掩码,提供了方便的位掩码和信息输出函数。上面显示的基本权限(位0到4)包含在BasePermission类中。
  • Sid: ACL模块需要引用主体和GrantedAuthority[]。一个间接级别是由Sid接口提供的,它是“安全标识”的缩写。常见的类包括PrincipalSid(表示Authentication对象中的主体)和GrantedAuthoritySid。安全标识信息存储在ACL_SID表中。
  • ObjectIdentity:每个域对象在ACL模块内部由一个ObjectIdentity表示。默认实现称为ObjectIdentityImpl
  • AclService:检索适用于给定ObjectIdentity的Acl。在包含的实现(JdbcAclService)中,检索操作被委托给一个LookupStrategyLookupStrategy为检索ACL信息提供了高度优化的策略,使用批处理检索(BasicLookupStrategy)并支持利用物化视图、分层查询和类似的以性能为中心的非ansi SQL功能的自定义实现。
  • MutableAclService:允许显示修改后的Acl以进行持久化。如果您不希望使用此接口,则无需使用该接口。

请注意,我们的开箱即用的AclService和相关的数据库类都使用ANSI SQL。因此,这应该适用于所有主要数据库。在撰写本文时,该系统已经成功地使用Hypersonic SQL、PostgreSQL、Microsoft SQL Server和Oracle进行了测试。

Spring Security附带的两个示例演示了ACL模块。第一个是联系人样本,另一个是文档管理系统(DMS)样本。我们建议看一看这些例子。

开始

要开始使用Spring Security的ACL功能,您需要将ACL信息存储在某个地方。这就需要使用Spring实例化DataSource。然后DataSource被注入到JdbcMutableAclServiceBasicLookupStrategy实例中。后者提供高性能ACL检索功能,前者提供变异功能。关于示例配置,请参考随Spring Security一起发布的一个示例。您还需要使用上一节中列出的四个特定于ACL的表填充数据库(请参阅ACL示例以获取适当的SQL语句)。

一旦您创建了所需的模式并实例化了JdbcMutableAclService,接下来您需要确保您的域模型支持与Spring Security ACL包的互操作性。希望ObjectIdentityImpl将被证明是足够的,因为它提供了大量可以使用它的方式。大多数人将拥有包含public Serializable getId()方法的域对象。如果返回类型是long,或者与long兼容(例如int),你会发现你不需要进一步考虑ObjectIdentity问题。ACL模块的许多部分依赖于long标识符。如果你不使用long(或int, byte等),很有可能你需要重新实现一些类。我们不打算在Spring Security的ACL模块中支持非long标识符,因为long标识符已经与所有数据库序列兼容,这是最常见的标识符数据类型,并且有足够的长度来适应所有常见的使用场景。

下面的代码片段展示了如何创建一个Acl,或修改一个现有的Acl:

// Prepare the information we'd like in our access control entry (ACE)
ObjectIdentity oi = new ObjectIdentityImpl(Foo.class, new Long(44));
Sid sid = new PrincipalSid("Samantha");
Permission p = BasePermission.ADMINISTRATION;

// Create or update the relevant ACL
MutableAcl acl = null;
try {
acl = (MutableAcl) aclService.readAclById(oi);
} catch (NotFoundException nfe) {
acl = aclService.createAcl(oi);
}

// Now grant some permissions via an access control entry (ACE)
acl.insertAce(acl.getEntries().length, p, sid, true);
aclService.updateAcl(acl);

在上面的示例中,我们检索与标识符为44的“Foo”域对象关联的ACL。然后添加一个ACE,以便名为“Samantha”的主体可以“管理”该对象。除了insertAce方法之外,代码片段相对来说是不言自明的。insertAce方法的第一个参数确定新条目将插入Acl中的哪个位置。在上面的示例中,我们只是将新的ACE放在现有ACE的末尾。最后一个参数是一个布尔值,指示ACE是授予还是拒绝。大多数情况下,它将授予(true),但如果拒绝(false),权限将被有效地阻止。

Spring Security没有提供任何特殊集成来自动创建、更新或删除作为DAO或存储库操作的一部分的acl。相反,您需要为您的单个域对象编写如上所示的代码。值得考虑在服务层上使用AOP来自动集成ACL信息和服务层操作。在过去,我们发现这是一种相当有效的方法。

使用上述技术在数据库中存储一些ACL信息后,下一步是实际使用ACL信息作为授权决策逻辑的一部分。你有很多选择。您可以编写自己的AccessDecisionVoterAfterInvocationProvider,它们分别在方法调用之前或之后触发。这些类将使用AclService检索相关ACL,然后调用ACL.isGranted(Permission[] Permission, Sid[] Sid, boolean administrativeMode)决定是否授予或拒绝权限。或者,你可以使用我们的AclEntryVoterAclEntryAfterInvocationProviderAclEntryAfterInvocationCollectionFilteringProvider类。所有这些类都提供了一种基于声明的方法来在运行时计算ACL信息,使您无需编写任何代码。请参考示例应用程序了解如何使用这些类。

你可能感兴趣的:(服务器,spring,security)