本指南是Spring Security的入门,它提供了对该框架的设计和基本构建块的见解。我们仅介绍应用程序安全性的最基础知识。但是,这样做可以消除使用Spring Security的开发人员遇到的一些困惑。为此,我们看一下通过使用过滤器,更普遍地通过使用方法注释,将安全性应用到Web应用程序中的方式。当您需要对安全应用程序的工作原理,如何对其进行自定义或需要学习如何考虑应用程序安全性的高级了解时,请使用本指南。

本指南的目的不是用来解决最基本的问题(还有其他来源)的手册或食谱,但对于初学者和专家都可能有用。还经常引用Spring Boot,因为它为安全的应用程序提供了一些默认行为,并且有助于理解它与整个体系结构之间的关系。

笔记
所有这些原则同样适用于不使用Spring Boot的应用程序。

身份验证和访问控制

应用程序安全性可以归结为或多或少的两个独立问题:身份验证(您是谁?)和授权(您被允许做什么?)。有时人们会说“访问控制”而不是“授权”,这可能会造成混乱,但是以这种方式思考可能会有所帮助,因为“授权”在其他地方很繁重。Spring Security的体系结构旨在将身份验证与授权分开,并具有策略和扩展点。

验证

身份验证的主要策略界面是AuthenticationManager,它只有一种方法:

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;
}

AnAuthenticationManager可以在其authenticate()方法中执行以下三件事之一:

  • 如果它可以验证输入是否代表有效的主体,则返回一个Authentication(通常带有authenticated=true)。

  • AuthenticationException如果它认为输入代表无效的主体,则抛出一个。

  • null如果无法决定,则返回。

AuthenticationException是运行时异常。它通常由应用程序以通用方式处理,具体取决于应用程序的样式或目的。换句话说,通常不希望用户代码捕获并处理它。例如,一个Web UI可能会呈现一个页面,该页面指出身份验证失败,而后端HTTP服务可能会发送401响应(带有或不带有WWW-Authenticate标题,具体取决于上下文)。

最常用的实现AuthenticationManagerProviderManager,它委托一系列AuthenticationProvider实例。AnAuthenticationProvider有点像an AuthenticationManager,但是它有一个额外的方法,允许调用者查询它是否支持给定Authentication类型:

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);
}

方法中的Class<?>参数supports()实际上是Class<? extends Authentication>(仅询问它是否支持传递到authenticate()方法中的内容)。ProviderManager通过委派一个链,A可以在同一应用程序中支持多种不同的身份验证机制AuthenticationProviders。如果aProviderManager无法识别特定的Authentication实例类型,则将其跳过。

AProviderManager有一个可选的父级,如果所有提供程序都返回,它可以向其查询null。如果父级不可用,则null Authentication结果为AuthenticationException

有时,应用程序具有逻辑上受保护的资源组(例如,所有与路径模式匹配的Web资源,例如/api/**),并且每个组可以有自己专用的AuthenticationManager。通常,每个都是一个ProviderManager,并且它们共享一个父级。因此,父级是一种“全局”资源,充当所有提供者的后备。

具有公共父级的ProviderManager
图1.AuthenticationManager使用ProviderManager

自定义身份验证管理器

Spring Security提供了一些配置助手,可以快速获取在应用程序中设置的通用身份验证管理器功能。最常用的帮助器是AuthenticationManagerBuilder,它非常适合设置内存,JDBC或LDAP用户详细信息或添加自定义UserDetailsService。以下示例显示了配置全局(父)的应用程序AuthenticationManager

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

   ... // web stuff here

  @Autowired
  public void initialize(AuthenticationManagerBuilder builder, DataSource dataSource) {
    builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }

}

此示例与Web应用程序有关,但是的用法AuthenticationManagerBuilder更为广泛(有关如何实现Web应用程序安全性的详细信息,请参阅Web安全性)。请注意,AuthenticationManagerBuilder@Autowired进入的一个方法@Bean -这是什么使得它建立全局(父)AuthenticationManager。相比之下,请考虑以下示例:

@Configuration
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {

  @Autowired
  DataSource dataSource;

   ... // web stuff here

  @Override
  public void configure(AuthenticationManagerBuilder builder) {
    builder.jdbcAuthentication().dataSource(dataSource).withUser("dave")
      .password("secret").roles("USER");
  }

}

如果我们@Override在配置程序中使用的方法,则该方法AuthenticationManagerBuilder仅用于构建“本地” AuthenticationManager,它将是全局方法的子级。在Spring Boot应用程序中,您可以@Autowired将全局的一个转换为另一个Bean,但是除非您自己显式公开它,否则不能使用本地的Bean来实现。

Spring Boot提供了一个默认的全局AuthenticationManager(只有一个用户),除非您通过提供自己的type Bean来抢占它AuthenticationManager。缺省值本身具有足够的安全性,除非您积极需要自定义global,否则不必担心太多AuthenticationManager。如果您进行任何构建的配置,AuthenticationManager通常可以在本地对要保护的资源进行配置,而不必担心全局默认值。

授权或访问控制

认证成功后,我们可以继续进行授权,这里的核心策略是AccessDecisionManager。框架提供了三个实现,所有三个委托都链接到一个AccessDecisionVoter实例链,有点像的ProviderManager委托AuthenticationProviders

一个AccessDecisionVoter考虑了Authentication(代表委托人)和一个证券的证券Object,该证券已装饰有ConfigAttributes

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

int vote(Authentication authentication, S object,
        Collection<ConfigAttribute> attributes);

Object是的签名完全通用的AccessDecisionManagerAccessDecisionVoter。它代表用户可能想要访问的任何内容(Web资源或Java类中的方法是两种最常见的情况)。该ConfigAttributes也相当一般,较安全的装修Object用一些确定的权限级别元数据来访问它所需。ConfigAttribute是一个接口。它只有一个方法(非常通用并返回String),因此这些字符串以某种方式编码资源所有者的意图,表达有关允许谁访问它的规则。典型ConfigAttribute的名称是用户角色的名称(如ROLE_ADMINROLE_AUDIT),并且它们通常具有特殊的格式(如ROLE_ 前缀)或表示需要求值的表达式。

大多数人都使用默认值AccessDecisionManager,即默认值AffirmativeBased(如果任何投票者肯定返回,则将授予访问权限)。通过添加新的选项或修改现有选项的工作方式,任何定制都倾向于在投票者中发生。

使用ConfigAttributesSpring Expression Language(SpEL)表达式是非常普遍的-例如,isFullyAuthenticated() && hasRole('user')AccessDecisionVoter可以处理这些表达式并为其创建上下文的对此提供了支持。要扩展可以处理的表达式的范围,需要自定义实现,SecurityExpressionRoot有时还需要自定义实现SecurityExpressionHandler

网络安全

Web层(用于UI和HTTP后端)中的Spring Security基于Servlet Filters,因此首先了解一下Filters一般角色是有帮助的。下图显示了单个HTTP请求的处理程序的典型分层。

委托给Servlet的过滤器链

客户端将请求发送到应用程序,然后容器根据请求URI的路径确定对它应用哪些过滤器和哪个servlet。一个servlet最多只能处理一个请求,但是过滤器形成一个链,因此它们是有序的。实际上,如果过滤器要处理请求本身,则可以否决该链的其余部分。过滤器还可以修改下游过滤器和Servlet中使用的请求或响应。过滤器链的顺序非常重要,Spring Boot通过两种机制对其进行管理:@BeanstypeFilter可以具有an@Order或Implement Ordered,它们可以是a的一部分。FilterRegistrationBean该订单本身是其API的一部分。一些现成的过滤器定义了它们自己的常量,以帮助发出信号,表示彼此之间相对喜欢的顺序(例如,SessionRepositoryFilter来自Spring Session的DEFAULT_ORDERof Integer.MIN_VALUE + 50,它告诉我们它喜欢在链中处于早期状态,但是它不排除其他过滤器在其之前)。

Spring Security是作为一个整体安装Filter在链中的,其具体类型为FilterChainProxy,出于我们稍后将介绍的原因。在Spring Boot应用程序中,安全过滤器位于@BeanApplicationContext,并且默认情况下已安装,以便将其应用于每个请求。它安装在由定义的位置SecurityProperties.DEFAULT_FILTER_ORDER,而该位置又由锚定FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER(Spring Boot应用程序期望过滤器在包装请求,修改其行为时具有的最大顺序)。但是,还有更多的功能:从容器的角度来看,Spring Security是单个过滤器,但是在内部,还有其他过滤器,每个过滤器都扮演着特殊的角色。下图显示了这种关系:

弹簧安全过滤器
图2. Spring Security是单个物理的,Filter但是将处理委托给一系列内部过滤器

实际上,安全性过滤器中甚至还有一层间接层:通常DelegatingFilterProxy,它作为a安装在容器中,而不必是Spring @Bean。代理委托给a FilterChainProxy(始终为a)@Bean,通常使用固定名称springSecurityFilterChain。它FilterChainProxy包含所有安全逻辑,这些安全逻辑在内部排列为一个或多个过滤器链。所有过滤器都具有相同的API(它们都实现了FilterServlet规范中的接口),并且它们都有机会否决该链的其余部分。

可以有多个过滤器链,这些过滤器链均由Spring Security在同一顶层管理,FilterChainProxy并且对于容器来说都是未知的。Spring Security过滤器包含一个过滤器链列表,并向与其匹配的第一个链调度一个请求。下图显示了基于匹配请求路径(/foo/**匹配之前/**)发生的调度。这是很常见的,但不是匹配请求的唯一方法。此调度过程的最重要特征是,只有一个链可以处理请求。

安全过滤器派遣
图3. Spring SecurityFilterChainProxy将请求分派到匹配的第一个链。

没有自定义安全配置的普通Spring Boot应用程序具有多个(称为n)过滤器链,其中通常n = 6。前(n-1)个链仅用于忽略静态资源模式(如/css/**/images/**)以及错误视图:/error。(这些路径可通过与用户控制security.ignoredSecurityProperties配置Bean。)最后链的全捕获路径相匹配(/**),并且是更加活跃,包含逻辑,用于认证,授权,异常处理,会话处理,报头写入,等等上。默认情况下,该链中总共有11个过滤器,但通常用户不必担心使用哪种过滤器以及何时使用。

笔记
容器不知道Spring Security内部所有过滤器这一事实很重要,尤其是在Spring Boot应用程序中,默认情况下,所有@Beans类型都会Filter自动向容器注册。因此,如果要向安全链添加自定义过滤器,则无需使其成为a@Bean或将其包装在FilterRegistrationBean显式禁用容器注册的a中。

创建和自定义过滤器链

Spring Boot应用程序(带有/**请求匹配器的应用程序)中的默认后备过滤器链的预定义顺序为SecurityProperties.BASIC_AUTH_ORDER。您可以通过设置将其完全关闭security.basic.enabled=false,也可以将其用作后备并以较低的顺序定义其他规则。为此,添加@Bean类型WebSecurityConfigurerAdapter(或WebSecurityConfigurer)并使用修饰类@Order,如下所示:

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/match1/**")
     ...;
  }
}

这个bean使Spring Security添加一个新的过滤器链并在回退之前对其进行排序。

与另一套资源相比,许多应用程序对一套资源的访问规则完全不同。例如,承载UI和支持API的应用程序可能支持基于cookie的身份验证以及对UI部件的登录页面的重定向,以及基于令牌的身份验证以及对API部件的未经身份验证的请求的401响应。每组资源都有自己WebSecurityConfigurerAdapter的唯一顺序和自己的请求匹配器。如果匹配规则重叠,则最早的有序过滤器链将获胜。

请求匹配以进行派遣和授权

安全过滤器链(或等效地,a WebSecurityConfigurerAdapter)具有一个请求匹配器,该请求匹配器用于决定是否将其应用于HTTP请求。一旦决定应用特定的过滤器链,就不再应用其他过滤器链。但是,在过滤器链中,可以通过在HttpSecurity配置器中设置其他匹配器来对授权进行更细粒度的控制,如下所示:

@Configuration
@Order(SecurityProperties.BASIC_AUTH_ORDER - 10)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/match1/**")
      .authorizeRequests()
        .antMatchers("/match1/user").hasRole("USER")
        .antMatchers("/match1/spam").hasRole("SPAM")
        .anyRequest().isAuthenticated();
  }
}

配置Spring Security时最容易犯的错误之一就是忘记了这些匹配器适用于不同的进程。一个是整个过滤器链的请求匹配器,另一个只是选择要应用的访问规则。

将应用程序安全规则与执行器规则相结合

如果将Spring Boot Actuator用于管理端点,则可能希望它们是安全的,并且默认情况下它们是安全的。实际上,将执行器添加到安全应用程序后,您将获得仅适用于执行器端点的附加过滤器链。它由仅匹配执行器端点的请求匹配器定义,其顺序为ManagementServerProperties.BASIC_AUTH_ORDER,比默认SecurityProperties回退过滤器小5,因此在回退之前进行查询。

如果您希望将应用程序安全规则应用于执行器端点,则可以添加一个过滤器链,该过滤器链的订购要早于执行器一,并且其请求匹配器包括所有执行器端点。如果您喜欢执行器端点的默认安全性设置,最简单的方法是在执行器端点之后但在回退之前(例如ManagementServerProperties.BASIC_AUTH_ORDER + 1)添加您自己的过滤器,如下所示:

@Configuration
@Order(ManagementServerProperties.BASIC_AUTH_ORDER + 1)
public class ApplicationConfigurerAdapter extends WebSecurityConfigurerAdapter {
  @Override
  protected void configure(HttpSecurity http) throws Exception {
    http.antMatcher("/foo/**")
     ...;
  }
}
笔记
Web层中的Spring Security当前与Servlet API绑定,因此仅当在Servlet容器中运行应用程序时,该应用程序才真正适用。但是,它不依赖于Spring MVC或Spring Web堆栈的其余部分,因此可以在任何servlet应用程序中使用,例如,使用JAX-RS的servlet应用程序。

方法安全性

除了保护Web应用程序安全外,Spring Security还提供了将访问规则应用于Java方法执行的支持。对于Spring Security,这只是另一种类型的“受保护资源”。对于用户而言,这意味着使用相同的ConfigAttribute字符串格式(例如,角色或表达式)声明访问规则,但在代码中的不同位置。第一步是启用方法安全性-例如,在我们应用程序的顶级配置中:

@SpringBootApplication
@EnableGlobalMethodSecurity(securedEnabled = true)
public class SampleSecureApplication {
}

然后,我们可以直接装饰方法资源:

@Service
public class MyService {

  @Secured("ROLE_USER")
  public String secure() {
    return "Hello Security";
  }

}

此示例是具有安全方法的服务。如果Spring创建了@Bean此类,它将被代理,并且在实际执行该方法之前,调用者必须经过安全拦截器。如果访问被拒绝,则调用者得到的是AccessDeniedException而不是实际方法的结果。

您还可以在方法上使用其他注释来强制执行安全性约束,特别是@PreAuthorize@PostAuthorize,它们可以使您编写分别包含对方法参数和返回值的引用的表达式。

小费
结合使用Web安全性和方法安全性并不少见。筛选器链提供了用户体验功能,例如身份验证和重定向到登录页面等,并且方法安全性在更精细的级别上提供了保护。

使用线程

Spring Security从根本上讲是线程绑定的,因为它需要使当前经过身份验证的主体可供各种下游使用者使用。基本的构建块是SecurityContext,它可以包含一个Authentication(并且当用户登录时,它Authentication是明确地是authenticated)。您始终可以SecurityContext通过中的静态便捷方法来访问和操作SecurityContextHolder,而这些方法又可以操作ThreadLocal。以下示例显示了这种安排:

SecurityContext context = SecurityContextHolder.getContext();
Authentication authentication = context.getAuthentication();
assert(authentication.isAuthenticated);

这是不是对用户应用程序代码来执行这个共同的,但它可以是有用的,如果你,比如,需要写一个自定义的验证过滤器(虽然,即使如此,也有Spring Security的基类,您可以使用,让你可以避免使用SecurityContextHolder)。

如果您需要访问Web端点中当前经过身份验证的用户,则可以在中使用method参数@RequestMapping,如下所示:

@RequestMapping("/foo")
public String foo(@AuthenticationPrincipal User user) {
  ... // do stuff with user
}

此注释从中拉出当前Authentication值,SecurityContextgetPrincipal()在其上调用方法以产生method参数。该类型的PrincipalAuthentication依赖于AuthenticationManager用于验证的认证,所以这可能是一个有用的小窍门得到一个类型安全的引用您的用户数据。

如果使用的是Spring Security,则Principalfrom中的HttpServletRequestis类型为Authentication,因此您也可以直接使用它:

@RequestMapping("/foo")
public String foo(Principal principal) {
  Authentication authentication = (Authentication) principal;
  User = (User) authentication.getPrincipal();
  ... // do stuff with user
}

如果您需要编写在不使用Spring Security时可以工作的代码,那么这有时会很有用(您需要在装入Authentication类时更具防御性)。

异步处理安全方法

由于sSecurityContext是线程绑定的,因此,如果要执行任何调用安全方法的后台处理(例如使用@Async),则需要确保传播上下文。归结为将在后台执行SecurityContext的任务(RunnableCallable等)包装起来。Spring Security提供了一些帮手,使这更容易,如包装的RunnableCallable。要传播SecurityContextto@Async方法,您需要提供anAsyncConfigurer并确保Executoris的类型正确:

@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {

  @Override
  public Executor getAsyncExecutor() {
    return new DelegatingSecurityContextExecutorService(Executors.newFixedThreadPool(5));
  }

}