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

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

身份验证和访问控制

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

认证方式

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

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;

}

一个AuthenticationManager可以做三件事之一authenticate()方法:

  1. 返回一个Authentication (通常与authenticated=true )是否可以验证输入内容代表有效的委托人。

  2. 扔一个AuthenticationException如果它认为输入代表无效的主体。

  3. 返回null如果无法决定。

AuthenticationException是运行时异常。它通常由应用程序以通用方式处理,具体取决于应用程序的样式或目的。换句话说,通常不希望用户代码捕获并处理它。例如,网络用户界面将呈现一个页面,该页面显示身份验证失败,后端HTTP服务将发送401响应,无论是否包含WWW-Authenticate标头取决于上下文。

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

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);

}

Class的论点supports()方法是真的Class (只会询问它是否支持将传递到authenticate()方法)。一种ProviderManager通过委派给一个链,可以在同一应用程序中支持多种不同的身份验证机制AuthenticationProviders 。如果一个ProviderManager无法识别特定的Authentication实例类型将被跳过。

一种ProviderManager有一个可选的父级,如果所有提供程序都返回,则可以咨询该父级null 。如果父母不在家,那么nullAuthentication导致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应用程序安全性的详细信息,请参见下文)。请注意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 。缺省值本身具有足够的安全性,除非您积极需要自定义全局变量,否则不必担心太多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_ADMIN要么ROLE_AUDIT ),并且它们通常具有特殊的格式(例如ROLE_前缀)或表示需要评估的表达式。

大多数人只使用默认值AccessDecisionManager这是AffirmativeBased (如果任何选民肯定地返回,那么将授予访问权限)。任何定制都倾向于在选民中发生,要么增加新选民,要么修改现有选民的工作方式。

使用非常普遍ConfigAttributes例如,Spring Expression Language(SpEL)表达式isFullyAuthenticated() && hasRole('FOO') 。这得到了AccessDecisionVoter可以处理表达式并为其创建上下文。要扩展可处理的表达式范围,需要自定义实现SecurityExpressionRoot有时也SecurityExpressionHandler

网络安全

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

委托给Servlet的过滤器链

客户端向应用程序发送请求,然后容器根据请求URI的路径确定对它应用哪些过滤器和哪个servlet。一个servlet最多只能处理一个请求,但是过滤器形成一个链,因此它们是有序的,实际上,如果过滤器要处理请求本身,则可以否决链的其余部分。过滤器还可以修改下游过滤器和Servlet中使用的请求和/或响应。过滤器链的顺序非常重要,Spring Boot通过两种机制对其进行管理:一种是@Beans类型的Filter可以有一个@Order或实施Ordered ,另一个是它们可以成为FilterRegistrationBean该API本身具有一个订单。一些现成的过滤器定义了它们自己的常数,以帮助表示它们希望相对于彼此的顺序(例如, SessionRepositoryFilter从 Spring 会议有一个DEFAULT_ORDERInteger.MIN_VALUE + 50 ,这告诉我们它喜欢在链中处于早期,但不排除其他过滤器排在它之前。

Spring Security作为单个安装Filter在链中,其秘密类型为FilterChainProxy ,其原因很快就会显现出来。在Spring Boot应用中,安全过滤器是@Bean在里面ApplicationContext ,并且默认情况下已安装,因此它适用于每个请求。它安装在由以下位置定义的位置SecurityProperties.DEFAULT_FILTER_ORDER ,而后者又由FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER (Spring Boot应用程序希望过滤器包装请求并修改其行为的最大顺序)。但是,还有更多的功能:从容器的角度来看,Spring Security是单个过滤器,但是在内部有其他过滤器,每个过滤器都扮演着特殊的角色。这是一张图片:

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

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

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

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

没有自定义安全配置的普通Spring Boot应用程序具有多个(称为n)过滤器链,其中通常n = 6。前(n-1)个链只是用来忽略静态资源模式,例如/css/**/images/** ,以及错误视图/error (路径可以由用户使用security.ignored来自SecurityProperties配置bean)。最后一条链匹配全部捕获路径/**并且更活跃,包含用于身份验证,授权,异常处理,会话处理,标头写入等的逻辑。默认情况下,此链中共有11个过滤器,但通常用户无需考虑使用哪些过滤器使用和何时。

注意
容器未知Spring Security内部的所有过滤器这一事实很重要,尤其是在Spring Boot应用程序中, @Beans类型的Filter默认情况下会自动向容器注册。因此,如果您想向安全链添加自定义过滤器,则无需将其设置为@Bean或包裹在一个FilterRegistrationBean明确禁用容器注册。

创建和定制过滤器链

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("/foo/**")
     ...;
  }
}

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

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

请求匹配以进行调度和授权

安全过滤器链(或等效的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("/foo/**")
      .authorizeRequests()
        .antMatchers("/foo/bar").hasRole("BAR")
        .antMatchers("/foo/spam").hasRole("SPAM")
        .anyRequest().isAuthenticated();
  }
}

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

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

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

如果您希望将应用程序安全规则应用于执行器端点,则可以添加一个比执行器顺序更早订购的过滤器链,并带有一个包括所有执行器端点的请求匹配器。如果您喜欢执行器端点的默认安全性设置,那么最简单的方法是在执行器端点之后但在回退之前(例如,)添加自己的过滤器。 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依次简单地操纵TheadLocal ,例如

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

用户应用程序代码执行此操作并不常见,但是例如在您需要编写自定义身份验证过滤器时可能会很有用(尽管即使如此,Spring Security中也可以使用基类来避免需要使用SecurityContextHolder )。

如果您需要访问Web端点中当前经过身份验证的用户,则可以在@RequestMapping 。例如

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

该注释拉动当前Authentication出于SecurityContext并致电getPrincipal() method产生方法参数。的类型Principal在一个Authentication取决于AuthenticationManager用于验证身份验证,因此这对于获得对用户数据的类型安全引用是一个有用的小技巧。

如果正在使用Spring Security Principal来自HttpServletRequest将是类型Authentication ,因此您也可以直接使用它:

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

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

异步处理安全方法

自从SecurityContext如果您想进行任何调用安全方法的后台处理,例如@Async ,您需要确保传播上下文。这归结为包装SecurityContext完成任务( RunnableCallable等)在后台执行。Spring Security提供了一些帮助程序来简化此工作,例如包装RunnableCallable 。传播SecurityContext@Async您需要提供的方法AsyncConfigurer并确保Executor是正确的类型:

@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {

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

}