Spring Security 架构

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

本指南并非旨在作为解决最基本问题的手册或秘籍(有其他来源可以提供这些信息),但它对初学者和专家都很有用。Spring Boot 也经常被提及,因为它为安全应用程序提供了一些默认行为,了解它如何与整体架构相适应很有用。

注意
所有原则同样适用于不使用 Spring Boot 的应用程序。

身份验证和访问控制

应用程序安全归结为两个或多或少独立的问题:身份验证(你是谁?)和授权(你能做什么?)。有时人们会用“访问控制”代替“授权”,这可能会令人困惑,但以这种方式考虑它可能会有所帮助,因为“授权”在其他地方被过载。Spring Security 具有旨在将身份验证与授权分开的架构,并具有针对两者都适用的策略和扩展点。

身份验证

用于身份验证的主要策略接口是 AuthenticationManager,它只有一种方法

public interface AuthenticationManager {

  Authentication authenticate(Authentication authentication)
    throws AuthenticationException;
}

AuthenticationManager 可以在其 authenticate() 方法中执行以下 3 项操作之一

  • 如果它可以验证输入表示有效的委托人,则返回 Authentication(通常为 authenticated=true)。

  • 如果它认为输入表示无效的委托人,则抛出 AuthenticationException

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

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

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

public interface AuthenticationProvider {

	Authentication authenticate(Authentication authentication)
			throws AuthenticationException;

	boolean supports(Class<?> authentication);
}

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

ProviderManager 有一个可选的父级,如果所有提供程序都返回 null,它可以咨询父级。如果父级不可用,则 null Authentication 将导致 AuthenticationException

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

ProviderManagers with a common parent
图 1. 使用 ProviderManagerAuthenticationManager 层次结构

自定义身份验证管理器

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,它是全局 AuthenticationManager 的子级。在 Spring Boot 应用程序中,您可以将全局 AuthenticationManager @Autowired 到另一个 bean,但除非您自己显式公开它,否则您无法使用本地 AuthenticationManager 执行此操作。

除非您通过提供自己的 AuthenticationManager 类型 bean 来抢先,否则 Spring Boot 会提供一个默认的全局 AuthenticationManager(仅有一个用户)。默认情况下,它本身就足够安全,您不必太担心它,除非您确实需要一个自定义的全局 AuthenticationManager。如果您执行任何构建 AuthenticationManager 的配置,您通常可以在您要保护的资源本地执行此操作,而不用担心全局默认值。

授权或访问控制

一旦身份验证成功,我们就可以继续进行授权,此处的核心策略是 AccessDecisionManager。该框架提供了三个实现,所有三个实现都委派给 AccessDecisionVoter 实例的链,有点像 ProviderManager 委派给 AuthenticationProviders

AccessDecisionVoter 考虑一个 Authentication(代表主体)和一个安全的 Object,该 Object 已用 ConfigAttributes 修饰

boolean supports(ConfigAttribute attribute);

boolean supports(Class<?> clazz);

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

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

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

使用 Spring 表达式语言 (SpEL) 表达式的 ConfigAttributes 非常常见,例如 isFullyAuthenticated() && hasRole('user')。这得到了 AccessDecisionVoter 的支持,它可以处理表达式并为它们创建上下文。要扩展可以处理的表达式的范围,需要自定义实现 SecurityExpressionRoot,有时还需要 SecurityExpressionHandler

Web 安全性

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

Filter chain delegating to a Servlet

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

Spring Security 安装为链中的单个 Filter,其具体类型为 FilterChainProxy,原因我们很快就会介绍。在 Spring Boot 应用程序中,安全过滤器是 ApplicationContext 中的 @Bean,并且默认安装,以便将其应用于每个请求。它安装在由 SecurityProperties.DEFAULT_FILTER_ORDER 定义的位置,而 SecurityProperties.DEFAULT_FILTER_ORDER 又由 FilterRegistrationBean.REQUEST_WRAPPER_FILTER_MAX_ORDER 锚定(如果 Spring Boot 应用程序期望过滤器包装请求并修改其行为,则为过滤器提供的最大顺序)。但它还有更多内容:从容器的角度来看,Spring Security 是一个单一过滤器,但它内部还有其他过滤器,每个过滤器都发挥着特殊作用。下图显示了这种关系

Spring Security Filter
图 2. Spring Security 是一个单一的物理 Filter,但将处理委托给内部过滤器链

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

在同一个顶级 FilterChainProxy 中,Spring Security 可以管理多个过滤器链,并且容器不知道所有这些过滤器链。Spring Security 过滤器包含过滤器链列表,并将请求分派到与之匹配的第一个链。下图显示了基于匹配请求路径(/foo/**/** 之前匹配)的分派过程。这是非常常见的,但不是匹配请求的唯一方法。此分派过程最重要的特性是,只有一个链可以处理请求。

Security Filter Dispatch
图 3. Spring Security FilterChainProxy 将请求分派到与之匹配的第一个链。

没有自定义安全配置的普通 Spring Boot 应用程序有几个(称之为 n)过滤器链,其中通常 n=6。前 (n-1) 个链只是为了忽略静态资源模式,如 /css/**/images/**,以及错误视图:/error。(用户可以使用 SecurityProperties 配置 bean 中的 security.ignored 控制路径。)最后一个链与 catch-all 路径(/**)匹配,并且更加活跃,包含用于身份验证、授权、异常处理、会话处理、标头写入等的逻辑。默认情况下,此链中总共有 11 个过滤器,但通常用户不必关心使用哪些过滤器以及何时使用。

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

创建和自定义过滤器链

Spring Boot 应用程序中的默认后备过滤器链(具有 /** 请求匹配器的过滤器链)具有预定义的 SecurityProperties.BASIC_AUTH_ORDER 顺序。你可以通过设置 security.basic.enabled=false 完全关闭它,或者你可以将其用作后备并定义具有较低顺序的其他规则。要执行后者,请添加一个 WebSecurityConfigurerAdapter(或 WebSecurityConfigurer)类型的 @Bean,并使用 @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,具有唯一的顺序和自己的请求匹配器。如果匹配规则重叠,则最早排序的过滤器链获胜。

用于分派和授权的请求匹配

安全过滤器链(或等效地,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 执行器用于管理端点,你可能希望它们是安全的,并且默认情况下它们是安全的。事实上,一旦你将执行器添加到安全应用程序,你就会获得一个仅适用于执行器端点的附加过滤器链。它使用仅匹配执行器端点的请求匹配器进行定义,并且具有 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 的应用程序。

方法安全性

除了支持保护 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(当用户登录时,它是一个明确 authenticatedAuthentication)。你始终可以通过 SecurityContextHolder 中的静态便捷方法来访问和操作 SecurityContext,而 SecurityContextHolder 又会操作一个 ThreadLocal。以下示例显示了这样的安排

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
}

此注释从 SecurityContext 中提取当前 Authentication,并在其上调用 getPrincipal() 方法以生成方法参数。AuthenticationPrincipal 的类型取决于用于验证身份验证的 AuthenticationManager,因此这可能是一个有用的技巧,可以获取对用户数据的类型安全引用。

如果正在使用 Spring Security,则 HttpServletRequest 中的 Principal 的类型为 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)的任何后台处理,则需要确保传播上下文。这归结为使用在后台执行的任务(RunnableCallable 等)包装 SecurityContext。Spring Security 提供了一些帮助程序来简化此操作,例如 RunnableCallable 的包装器。要将 SecurityContext 传播到 @Async 方法,你需要提供一个 AsyncConfigurer 并确保 Executor 为正确类型

@Configuration
public class ApplicationConfiguration extends AsyncConfigurerSupport {

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

}

获取代码