玖叶教程网

前端编程开发入门

使用 SecurityContext(使用的英文)

本文讨论安全上下文。我们分析它是如何工作的,如何从它访问数据,以及应用程序如何在不同的线程相关场景中管理它。学习完本文后,您将了解如何为各种情况配置安全上下文。通过这种方式,您可以在后面学习配置授权时使用安全上下文存储的认证用户的详细信息。

认证过程结束后,您可能需要有关已认证实体的详细信息。 例如,您可能需要引用用户名或当前已认证用户的权限。 认证过程完成后,仍可以访问此信息吗? AuthenticationManager 成功完成认证过程后,将为其余请求存储 Authentication 实例。 存储 Authentication 对象的实例称为安全上下文。


认证成功后,认证过滤器会将已认证的实体的详细信息存储在安全上下文中。 从那里,实现映射到请求的动作的控制器可以在需要时访问这些详细信息。

Spring Security 的安全上下文由 SecurityContext 接口描述。 以下清单定义了此接口。

清单 6 SecurityContext

public interface SecurityContext extends Serializable {

  Authentication getAuthentication();
  void setAuthentication(Authentication authentication);
}

正如您从约定定义中可以看到的,SecurityContext 的主要职责是存储 Authentication 对象。但是 SecurityContext 本身是如何管理的呢? Spring Security 提供了三种策略以管理器角色中的对象来管理 SecurityContext。它被命名为 SecurityContextHolder:

  • MODE_THREADLOCAL— 允许每个线程在安全上下文中存储自己的详细信息。在每请求一个线程的 web 应用程序中,这是一种常见的方法,因为每个请求都有一个单独的线程。
  • MODE_INHERITABLETHREADLOCAL—类似于 MODE_THREADLOCAL,但也指示 Spring Security 在异步方法的情况下将安全上下文复制到下一个线程。这样,我们可以说运行 @Async 方法的新线程继承了安全上下文。
  • MODE_GLOBAL— 使应用程序的所有线程看到相同的安全上下文实例。

除了 Spring security 提供的管理安全上下文的这三种策略外,在本文中,我们还将讨论在定义 Spring 不知道的自己的线程时会发生什么。您将了解到,对于这些情况,需要显式地将安全上下文中的细节复制到新线程中。Spring Security 不能自动管理不在 Spring 上下文中的对象,但它为此提供了一些很好的实用程序类。

使用安全上下文的持有策略

管理安全上下文的第一个策略是 MODE_THREADLOCAL 策略。 该策略也是管理 Spring Security 使用的安全上下文的默认策略。 通过这种策略,Spring Security 使用 ThreadLocal 来管理上下文。 ThreadLocal 是 JDK 提供的实现。 此实现充当数据的集合,但是确保应用程序的每个线程只能看到存储在集合中的数据。 这样,每个请求都可以访问其安全上下文。 任何线程都无法访问其他线程的 ThreadLocal。 这意味着在 Web 应用程序中,每个请求只能看到其自己的安全上下文。 我们可以说,这也是后端 Web 应用程序通常需要的内容。

图 7 概述了此功能。 每个请求(A,B和C)都有自己分配的线程(T1,T2和T3)。 这样,每个请求只能看到存储在其安全上下文中的详细信息。 但这也意味着,如果创建了新线程(例如,当调用异步方法时),则新线程也将具有其自己的安全性上下文。 父线程(请求的原始线程)的详细信息不会复制到新线程的安全上下文中。

在这里,我们讨论一个传统的 servlet 应用程序,其中每个请求都与一个线程相关联。 这种架构仅适用于传统的 servlet 应用程序,其中每个请求都分配了自己的线程。 它不适用于响应式应用。 我们将在后面文章中详细讨论响应式方法的安全性。


每个请求都有自己的线程,用箭头表示。 每个线程只能访问其自己的安全上下文详细信息。 创建新线程时(例如,通过@Async方法),不会复制父线程的详细信息。

作为管理安全上下文的默认策略,不需要明确配置此过程。 在认证过程结束后,只要需要,就可以使用静态 getContext() 方法向持有人询问安全上下文。 在清单 7 中,您找到了一个在应用程序的端点之一中获取安全性上下文的示例。 从安全上下文中,您可以进一步获取 Authentication 对象,该对象存储有关已认证实体的详细信息。

清单 7 从SecurityContextHolder获取SecurityContext

@GetMapping("/hello")
public String hello() {
  SecurityContext context = SecurityContextHolder.getContext();
  Authentication a = context.getAuthentication();

  return "Hello, " + a.getName() + "!";
}

从上下文获取认证在端点级别上更加方便,因为 Spring 知道直接将其注入方法参数中。您不需要每次都显式引用 SecurityContextHolder 类。如下面的清单所示,这种方法更好。

清单 8 Spring 在方法的参数中注入 Authentication 值

@GetMapping("/hello")
public String hello(Authentication a) {
  return "Hello, " + a.getName() + "!";
}

当使用正确的用户调用端点时,响应体包含用户名。例如,

curl -u user:99ff79e3-8ca0-401c-a396-0a8625ab3bad http://localhost:8080/hello
Hello, user!

使用异步调用的持有策略

坚持使用默认策略来管理安全上下文是很容易的。在很多情况下,它是你唯一需要的东西。MODE_THREADLOCAL 为您提供了为每个线程隔离安全上下文的能力,它使安全上下文更易于理解和管理。但在某些情况下,这并不适用。

如果我们必须为每个请求处理多个线程,情况就会变得更加复杂。看看如果将端点设置为异步会发生什么。执行该方法的线程不再是服务于请求的同一线程。思考下一个清单中所示的端点。

清单 9 一个由不同线程提供的 @Async 方法

@GetMapping("/bye")
@Async
public void goodbye() {
  SecurityContext context = SecurityContextHolder.getContext();
  String username = context.getAuthentication().getName();

  // do something with the username
}

为了启用 @Async 注解的功能,我还创建了一个配置类,并使用 @EnableAsync 对其进行了注解,如下所示:

@Configuration
@EnableAsync
public class ProjectConfig {

}

有时,在文章或论坛中,您会发现配置注解位于主类( main class ) 之上。 例如,您可能会发现某些示例直接在主类上使用@EnableAsync 注解。 这种方法在技术上是正确的,因为我们使用@SpringBootApplication 注解(包括@Configuration特性)注释了 Spring Boot 应用程序的主类。 但是在现实世界的应用程序中,我们倾向于将职责分开,并且我们永远不会将主类用作配置类。 为了使本系列中的示例尽可能清楚,我喜欢将这些注解保留在 @Configuration 类上,类似于您在实际情况下会发现它们的方式。

如果您现在尝试使用此代码,它将在从认证获取名称的行上引发 NullPointerException,即

String username = context.getAuthentication().getName()

这是因为该方法现在在不继承安全上下文的另一个线程上执行。 出于这个原因,Authorization 对象为 null,并且在所提供的代码的上下文中,导致 NullPointerException。 在这种情况下,您可以通过使用 MODE_INHERITABLETHREADLOCAL 策略来解决此问题。 可以通过调用 SecurityContextHolder.setStrategyName() 方法或使用系统属性 spring.security.strategy 进行设置。 通过设置此策略,框架知道将请求的原始线程的详细信息复制到异步方法的新创建的线程中(图 8)。


使用 MODE_INHERITABLETHREADLOCAL 时,框架会将安全上下文详细信息从请求的原始线程复制到新线程的安全上下文。

下一个清单提供了一种通过调用 setStrategyName() 方法设置安全上下文管理策略的方法。

清单 10 使用 InitializingBean 设置 SecurityContextHolder 模式

@Configuration
@EnableAsync
public class ProjectConfig {

  @Bean
  public InitializingBean initializingBean() {
    return () -> SecurityContextHolder.setStrategyName(
      SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
  }
}

调用端点后,您将看到 Spring 将安全上下文正确地传播到下一个线程。此外,认证不再为空。

注意: 然而,只有在框架本身创建线程时(例如,在@Async方法的情况下),这种方法才能工作。如果你的代码创建了线程,即使使用MODE_INHERITABLETHREADLOCAL策略,你也会遇到同样的问题。这是因为,在本例中,框架并不知道您的代码创建的线程。我们将在本文后面讨论如何解决这些案例的问题。

对独立应用程序使用持有策略

如果您需要的是由应用程序的所有线程共享的安全上下文,则将策略更改为 MODE_GLOBAL(图 9)。 您不会将这种策略用于 web 服务器,因为它不适合应用程序的总体情况。 后端Web 应用程序独立地管理接收到的请求,因此,将每个请求的安全上下文(而不是所有请求的一个上下文)分隔开确实更有意义。 但这对于独立应用程序可能是一个很好的用法。


MODE_GLOBAL 用作安全上下文管理策略,所有线程都访问相同的安全上下文。 这意味着所有这些人都可以访问相同的数据,并且可以更改该信息。 因此,可能会发生争用情况,因此您必须注意同步。

如下面的代码片段所示,您可以像使用 MODE_INHERITABLETHREADLOCAL 一样更改策略。 您可以使用方法 SecurityContextHolder.setStrategyName() 或系统属性 spring.security.strategy

@Bean
public InitializingBean initializingBean() {
  return () -> SecurityContextHolder.setStrategyName(
    SecurityContextHolder.MODE_GLOBAL);
}

另外,请注意 SecurityContext 也不是线程安全的。 因此,使用这种策略,其中应用程序的所有线程都可以访问 SecurityContext 对象,因此您需要注意并发访问。

使用 DelegatingSecurityContextRunnable 转发安全上下文

您已经了解到可以使用 Spring Security 提供的三种模式来管理安全上下文:MODE_THREADLOCAL,MODE_INHERITEDTHREADLOCAL 和 MODE_GLOBAL。 默认情况下,框架仅确保为请求的线程提供安全上下文,并且该安全上下文只能由该线程访问。 但是该框架无法处理新创建的线程(例如,在使用异步方法的情况下)。 并且您了解到,在这种情况下,您必须显式设置其他模式来管理安全上下文。 但是我们仍然有一个特点:当您的代码在框架不了解的情况下启动新线程时会发生什么? 有时我们将这些自管理线程命名为因为管理它们的是我们自己,而不是框架。 在本节中,我们将应用 Spring Security 提供的一些实用工具,这些工具可帮助您将安全性上下文传播到新创建的线程。

SecurityContextHolder没有特定的策略为你提供自管理线程的解决方案。在这种情况下,您需要注意安全上下文的传播。对此的一种解决方案是使用 DelegatingSecurityContextRunnable 来装饰要在单独线程上执行的任务。DelegatingSecurityContextRunnable 扩展了 Runnable。当没有返回值时,可以在任务执行之后使用它。如果您有一个返回值,那么您可以使用 Callable<T> 替代方案,它是 DelegatingSecurityContextCallable<T>。这两个类都表示任务异步执行,就像任何其他可运行或可调用的任务一样。此外,它们确保为执行任务的线程复制当前安全上下文。如图 10 所示,这些对象装饰了原来的任务,并将安全上下文复制到新线程。


DelegatingSecurityContextCallable 被设计为 Callable 对象的装饰器。 构建此类对象时,您提供了应用程序异步执行的可调用任务。 DelegatingSecurityContextCallable 将详细信息从安全上下文复制到新线程,然后执行任务。

清单 11 展示了 DelegatingSecurityContextCallable 的用法。 首先定义一个简单的端点方法,该方法声明一个 Callable 对象。 Callable 任务从当前安全上下文返回用户名。

清单 11 定义一个Callable对象,并将其作为任务在单独的线程上执行

@GetMapping("/ciao")
public String ciao() throws Exception {
  Callable<String> task = () -> {
     SecurityContext context = SecurityContextHolder.getContext();
     return context.getAuthentication().getName();
  };
        
 // ...
}

我们通过将任务提交到 ExecutorService 来继续这个示例。端点检索执行的响应并将其作为响应体返回。

清单 12 定义ExecutorService并提交任务

@GetMapping("/ciao")
public String ciao() throws Exception {
  Callable<String> task = () -> {
      SecurityContext context = SecurityContextHolder.getContext();
      return context.getAuthentication().getName();
  };

  ExecutorService e = Executors.newCachedThreadPool();
  try {
     return "Ciao, " + e.submit(task).get() + "!";
  } finally {
     e.shutdown();
  }
}

如果按原样运行应用程序,则只会得到一个 NullPointerException 。在新创建的运行可调用任务的线程中,身份验证不再存在,安全上下文为空。为了解决这个问题,我们使用 DelegatingSecurityContextCallable 装饰任务,它为新线程提供了当前上下文,如清单所示。

清单 13 运行由 DelegatingSecurityContextCallable 装饰的任务

@GetMapping("/ciao")
public String ciao() throws Exception {
  Callable<String> task = () -> {
    SecurityContext context = SecurityContextHolder.getContext();
    return context.getAuthentication().getName();
  };

  ExecutorService e = Executors.newCachedThreadPool();
  try {
    var contextTask = new DelegatingSecurityContextCallable<>(task);
    return "Ciao, " + e.submit(contextTask).get() + "!";
  } finally {
    e.shutdown();
  }
}

现在调用端点,您可以观察到 Spring 将安全上下文传播到了执行任务的线程中:

curl -u user:2eb3f2e8-debd-420c-9680-48159b2ff905  http://localhost:8080/ciao

这个调用的响应体是

Ciao, user!

使用 DelegatingSecurityContextExecutorService 转发安全上下文

当处理我们的代码在不让框架知道它们的情况下启动的线程时,我们必须管理从安全上下文到下一个线程的细节传播。在第上一节中,您应用了一种技术,通过利用任务本身从安全上下文复制细节。Spring Security 提供了一些很棒的实用程序类,比如 DelegatingSecurityContextCallable 和 DelegatingSecurityContextCallable。这些类装饰了您异步执行的任务,还负责从安全上下文复制细节,以便您的实现可以从新创建的线程访问这些细节。但是我们还有第二个选择来处理到新线程的安全上下文传播,这就是从线程池而不是从任务本身管理传播。在本节中,您将学习如何使用 Spring Security 提供的更棒的实用工具类来应用这种技术。

装饰任务的替代方法是使用特定类型的执行器。 在下一个示例中,您可以观察到该任务仍然是一个简单的 Callable <T>,但是线程仍在管理安全上下文。 之所以会传播安全上下文,是因为称为 DelegatingSecurityContextExecutorService 的实现会装饰 ExecutorService。 DelegatingSecurityContextExecutorService 还负责安全性上下文的传播,如图 11 所示。


DelegatingSecurityContextExecutorService 装饰 ExecutorService 并将安全上下文详细信息传播到下一个线程,然后再提交任务。

清单 14 中的代码显示了如何使用 DelegatingSecurityContextExecutorService 装饰 ExecutorService,以便在您提交任务时注意传播安全上下文的详细信息。

清单 14 传播 SecurityContext

@GetMapping("/hola")
public String hola() throws Exception {
  Callable<String> task = () -> {
    SecurityContext context = SecurityContextHolder.getContext();
    return context.getAuthentication().getName();
  };

  ExecutorService e = Executors.newCachedThreadPool();
  e = new DelegatingSecurityContextExecutorService(e);
  try {
    return "Hola, " + e.submit(task).get() + "!";
  } finally {
    e.shutdown();
  }
}

调用端点来测试 DelegatingSecurityContextExecutorService 是否正确地委托了安全上下文:

curl -u user:5a5124cc-060d-40b1-8aad-753d3da28dca http://localhost:8080/hola

这个调用的响应体是

Hola, user!

注意: 在与安全上下文的并发支持相关的类中,我建议您注意表 1 中列出的类。

Spring 提供了实用程序类的各种实现,您可以在创建自己的线程时在应用程序中使用它们来管理安全上下文。在上一节中,实现了 DelegatingSecurityContextCallable。在本节中,我们使用 DelegatingSecurityContextExecutorService。如果您需要为计划任务实现安全上下文传播,那么您会很高兴听到 Spring security 还为您提供了一个名为 DelegatingSecurityContextScheduledExecutorService 的装饰器。这种机制类似于我们在本节中介绍的 DelegatingSecurityContextExecutorService,不同之处在于它修饰了一个 ScheduledExecutorService,允许您处理调度任务。

另外,为了提高灵活性,Spring Security为 您提供了一个更抽象的装饰器 DelegatingSecurityContextExecutor。 此类直接装饰 Executor,这是此线程池层次结构中最抽象的约定。 如果您希望能够用语言提供的任何选择来替换线程池的实现,则可以为应用程序的设计选择它。

发表评论:

控制面板
您好,欢迎到访网站!
  查看权限
网站分类
最新留言