多维柔性作业调用
假设我们已经实现了一个Spring支持的应用程序,并使用Spring Security的方法安全性表达式对其进行了保护 。
我们的下一个任务是使用安全方法实施计划作业。 更具体地说,我们必须实现一个计划的作业,该作业从我们的服务类中获取一条消息,并将接收到的消息写入日志。
让我们开始吧。
本博客文章中描述的计划作业使用在特定于配置文件的配置文件中配置的cron表达式。 如果您不知道如何执行此操作,建议您阅读我的博客文章,其中描述了如何使用带有@Scheduled批注的特定于环境的cron表达式 。
我们的第一次尝试
让我们创建一个计划的作业,该作业调用受保护的方法并找出执行作业时发生的情况。 让我们先来看一下示例应用程序的服务层。
服务层
安全服务类的方法在MessageService接口中声明。 它声明了一个称为getMessage()的方法,并指定只有具有角色ROLE_USER的用户才能调用它。
MessageService接口的源代码如下所示:
import org.springframework.security.access.prepost.PreAuthorize;public interface MessageService {@PreAuthorize("hasRole('ROLE_USER')")public String getMessage();
}
我们对MessageService接口的实现非常简单。 其源代码如下:
import org.springframework.stereotype.Service;@Service
public class HelloMessageService implements MessageService {@Overridepublic String getMessage() {return "Hello World!";}
}
让我们继续并创建调用getMessage()方法的计划作业。
创建计划的作业
我们可以按照以下步骤创建计划的作业:
- 创建一个ScheduledJob类,并使用@Component注释对其进行注释。 这样可以确保在类路径扫描期间找到我们的计划作业(只要将其放入要扫描的程序包中)。
- 将私有的Logger字段添加到创建的类中,并通过调用LoggerFactory类的静态getLogger()方法来创建Logger对象。 我们将使用Logger对象将从HelloMessageService对象收到的消息写入日志。
- 将私有MessageService字段添加到创建的类。
- 将一个构造函数添加到创建的类中,并使用@Autowired注释对其进行注释。 这确保了我们可以使用构造函数注入将MessageService bean注入MessageService字段。
- 向创建的类添加一个公共run()方法,并使用@Scheduled批注对其进行批注。 将其cron属性的值设置为'$ {scheduling.job.cron}' 。 这意味着cron表达式是从属性文件中读取的,其值是schedule.job.cron属性的值( 有关此内容的更多信息,请参阅此博客文章 )。
- 通过调用MessageService接口的getMessage()方法来实现run()方法。 将收到的消息写入日志。
我们计划的作业的源代码如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;@Component
public class ScheduledJob {private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledJob.class);private final MessageService messageService;@Autowiredpublic ScheduledJob(MessageService messageService) {this.messageService = messageService;}@Scheduled(cron = "${scheduling.job.cron}")public void run() {String message = messageService.getMessage();LOGGER.debug("Received message: {}", message);}
}
让我们看看调用ScheduledJob类的run()方法时会发生什么。
它不起作用
当执行我们的计划作业时,将抛出AuthenticationCredentialsNotFoundException ,并且我们看到以下堆栈跟踪:
2013-12-10 19:45:19,001 ERROR - kUtils$LoggingErrorHandler - Unexpected error occurred in scheduled task.
org.springframework.security.authentication.AuthenticationCredentialsNotFoundException: An Authentication object was not found in the SecurityContextat org.springframework.security.access.intercept.AbstractSecurityInterceptor.credentialsNotFound(AbstractSecurityInterceptor.java:339)at org.springframework.security.access.intercept.AbstractSecurityInterceptor.beforeInvocation(AbstractSecurityInterceptor.java:198)at org.springframework.security.access.intercept.aopalliance.MethodSecurityInterceptor.invoke(MethodSecurityInterceptor.java:60)at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172)at org.springframework.aop.framework.JdkDynamicAopProxy.invoke(JdkDynamicAopProxy.java:204)at com.sun.proxy.$Proxy31.getMessage(Unknown Source)at net.petrikainulainen.spring.trenches.scheduling.job.ScheduledJobTwo.run(ScheduledJobTwo.java:26)at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57)at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)at java.lang.reflect.Method.invoke(Method.java:601)at org.springframework.scheduling.support.ScheduledMethodRunnable.run(ScheduledMethodRunnable.java:64)at org.springframework.scheduling.support.DelegatingErrorHandlingRunnable.run(DelegatingErrorHandlingRunnable.java:53)at org.springframework.scheduling.concurrent.ReschedulingRunnable.run(ReschedulingRunnable.java:81)at java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:471)at java.util.concurrent.FutureTask$Sync.innerRun(FutureTask.java:334)at java.util.concurrent.FutureTask.run(FutureTask.java:166)at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.access$201(ScheduledThreadPoolExecutor.java:178)at java.util.concurrent.ScheduledThreadPoolExecutor$ScheduledFutureTask.run(ScheduledThreadPoolExecutor.java:292)at java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1145)at java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:615)at java.lang.Thread.run(Thread.java:722)
该stacktrace实际上非常有用。 它告诉我们安全方法无法调用,因为从SecurityContext中找不到Authentication对象。
我看到的两个最常见的解决方案是:
- 创建一个与受保护的方法具有相同功能的单独方法,然后修改计划的作业以使用此方法。 此方法通常具有Javadoc注释,该注释指出只有计划的作业才能调用此方法。 这个解决方案有两个问题:1)它会使代码库混乱,并且2)最终无论如何都将调用该方法(除非真正需要,否则没人真正阅读Javadocs)。
- 从计划作业调用的方法中删除方法安全注释。 由于明显的原因,这是一个非常糟糕的解决方案。 提示:该方法的安全是有充分理由的!
幸运的是,还有第三种方法可以解决此问题。 让我们开始查找计划作业使用的安全上下文的存储位置。
安全上下文从何而来?
我们的问题的解决方案很明确:我们必须创建一个Authentication对象,然后在调用安全方法之前将其添加到SecurityContext中 。
但是,在对示例应用程序进行必要的修改之前,我们必须了解SecurityContext对象的存储位置。
如果未进行其他配置,则将安全上下文存储到ThreadLocal 。 换句话说,每个线程都有其自己的安全上下文。 这意味着在同一线程中执行的所有计划作业均共享相同的安全上下文。
假设我们有三个预定的作业。 这些作业称为A , B和C。 另外,我们假设这些作业是按字母顺序执行的。
如果我们使用只有一个线程的默认线程池,则所有作业共享相同的安全上下文。 如果作业B将身份验证对象设置为安全上下文,则执行计划的作业时会发生以下情况:
- 作业A无法调用安全方法,因为它在作业B之前执行。 这意味着从安全上下文中找不到身份验证对象。
- 作业B可以调用安全方法,因为作业B在尝试调用安全方法之前将Authentication对象设置为安全上下文。
- 作业C可以调用安全方法,因为它是在将身份验证对象设置为安全上下文的作业B之后执行的。
如果我们使用具有多个线程的线程池,则每个线程都有其自己的安全上下文。 如果作业A将Authentication对象设置为安全上下文,则在同一线程中执行的所有作业都将使用相同的特权执行,只要它们在作业A之后执行即可。
让我们一步一步地完成每一项工作:
- 作业A可以调用安全方法,因为作业A在尝试调用安全方法之前将Authentication对象设置为安全上下文。
- 如果作业B 与作业A在同一线程中执行,则作业B可以调用安全方法。 如果未在同一线程中执行作业,则无法调用安全方法,因为无法从安全上下文中找到Authentication对象。
- 如果作业C 与作业A在同一线程中执行,则作业C可以调用安全方法。 如果未在同一线程中执行作业,则无法调用安全方法,因为无法从安全上下文中找到Authentication对象。
显然,解决此问题的最佳方法是确保使用所需的特权执行每个计划的作业。 此解决方案有两个好处:
- 我们可以按任何顺序执行工作。
- 我们不必确保作业在“正确的”线程中执行。
让我们找出当我们的应用程序使用Spring Security 3.1时如何解决这个问题。
Spring Security 3.1:需要手动工作
如果我们的应用程序使用Spring Security 3.1,则解决问题的最简单方法是
- 在我们的工作尝试调用安全方法之前,创建一个Authentication对象并将其设置为安全上下文。
- 在作业完成之前,从安全上下文中删除身份验证对象。
让我们从创建提供所需方法的AuthenticationUtil类开始。
创建AuthenticationUtil类
我们可以按照以下步骤创建AuthenticationUtil类:
- 创建AuthenticationUtil类。
- 向AuthenticationUtil类添加一个私有构造函数。 这样可以确保无法实例化该类。
- 将静态clearAuthentication()方法添加到该类,并通过以下步骤实现该方法:
- 通过调用SecurityContextHolder类的静态getContext()方法来获取SecurityContext对象。
- 通过调用SecurityContext接口的setContext()方法删除身份验证信息。 将null作为方法参数传递。
- 将静态configureAuthentication()方法添加到该类。 此方法将用户的角色作为方法参数。 通过执行以下步骤来实现此方法:
- 通过调用AuthorityUtils类的静态createAuthorityList()方法来创建GrantedAuthority对象的集合 。 将用户角色作为方法参数传递。
- 创建一个新的UsernamePasswordAuthenticationToken对象,并将以下对象作为构造函数参数传递:
- 第一个构造函数参数是主体。 将字符串“ user”作为第一个构造函数参数传递。
- 第二个构造函数参数是用户的凭据。 将作为方法参数给出的角色作为第二个构造函数参数传递。
- 第三个构造函数参数包含用户的权限。 将创建的Collection <GrantedAuthority>对象作为第三个构造函数参数传递。
- 通过调用SecurityContextHolder类的静态getContext()方法来获取SecurityContext对象。
- 通过调用SecurityContext接口的setAuthentication()方法,将创建的Authentication对象设置为安全上下文。 将创建的UsernamePasswordAuthenticationToken作为方法参数传递。
AuthenticationUtil类的源代码如下所示:
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContextHolder;import java.util.Collection;public final class AuthenticationUtil {//Ensures that this class cannot be instantiatedprivate AuthenticationUtil() {}public static void clearAuthentication() {SecurityContextHolder.getContext().setAuthentication(null);}public static void configureAuthentication(String role) {Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(role);Authentication authentication = new UsernamePasswordAuthenticationToken("user",role,authorities);SecurityContextHolder.getContext().setAuthentication(authentication);}
}
我们还没有完成。 我们仍然必须对我们的预定工作进行一些修改。 让我们找出如何进行这些修改。
修改计划的作业
我们必须对ScheduledJob类进行两次修改。 我们可以按照以下步骤进行修改:
- 启动作业时,调用AuthenticationUtil类的静态configureAuthentication()方法,并将字符串 'ROLE_USER'作为方法参数传递。 这样可以确保我们的计划作业可以执行与具有ROLE_USER角色的普通用户相同的方法。
- 在作业完成之前,调用AuthenticationUtil类的静态clearAuthentication()方法。 这从安全上下文中删除了身份验证信息。
ScheduledJob类的源代码如下所示:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;@Component
public class ScheduledJob {private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledJob.class);private final MessageService messageService;@Autowiredpublic ScheduledJob(MessageService messageService) {this.messageService = messageService;}@Scheduled(cron = "${scheduling.job.cron}")public void run() {AuthenticationUtil.configureAuthentication("ROLE_USER");String message = messageService.getMessage();LOGGER.debug("Received message: {}", message);AuthenticationUtil.clearAuthentication();}
}
让我们找出运行预定作业时会发生什么。
运行计划的作业
调用作业时,以下消息将写入日志:
2013-12-17 20:41:33,019 DEBUG - ScheduledJob - Received message: Hello World!
当我们的应用程序使用Spring Security 3.1时,一切都将正常运行。 我们的解决方案不是那么优雅,但可以。 该解决方案的明显缺点是,我们必须记住在计划的作业中调用AuthenticationUtil类的configureAuthentication()和clearAuthentication()方法。
Spring Security 3.2解决了这个问题。 让我们继续前进,找出当我们的应用程序使用Spring Security 3.2时如何解决这个问题。
Spring Security 3.2:几乎就像魔术一样!
Spring Security 3.2具有全新的并发支持 ,这使我们可以将安全上下文从一个线程转移到另一个线程。 让我们找出如何配置应用程序上下文以使用Spring Security 3.2提供的功能。
配置应用程序上下文
因为我们要使用Spring Security 3.2的新并发支持,所以我们必须对应用程序上下文配置类进行以下更改( 原始配置在此博客文章中进行了描述 ):
- 实现SchedulingConfigurer接口。 该接口可以由使用@EnableScheduling批注注释的应用程序上下文配置类来实现,并且通常用于配置使用的TaskScheduler bean或以编程方式配置执行的任务。
- 将私有createrSchedulerSecurityContext()方法添加到配置类。 此方法没有方法参数,它返回一个SecurityContext对象。 通过执行以下步骤来实现此方法:
- 通过调用SecurityContextHolder类的静态createEmptyContext()方法来创建新的SecurityContext对象。
- 通过调用AuthorityUtils类的静态createAuthorityList()方法来创建GrantedAuthority对象的集合 。 将字符串 “ ROLE_USER”作为方法参数传递。
- 创建一个新的UsernamePasswordAuthenticationToken对象,并将以下对象作为构造函数参数传递:
- 第一个构造函数参数是主体。 将字符串 “ user”作为第一个构造函数参数传递。
- 第二个构造函数参数是用户的凭据。 将字符串 “ ROLE_USER”作为第二个构造函数参数传递。
- 第三个构造函数参数包含用户的权限。 将创建的Collection <GrantedAuthority>对象作为第三个构造函数参数传递。
- 通过调用SecurityContext接口的setAuthentication()方法,将创建的UsernamePasswordAuthenticationToken对象设置为创建的安全上下文。
- 将公共taskExecutor()方法添加到配置类中,并使用@Bean注释对该方法进行注释。 此方法没有方法参数,并返回Executor对象。 通过执行以下步骤来实现此方法:
- 通过调用Executors类的静态newSingleThreadScheduledExecutor()方法来创建新的ScheduledExecutorService对象。 这将创建一个ScheduledExecutorService对象,该对象通过使用一个线程来运行所有作业。
- 通过调用私有的createSchedulerSecurityContext()方法来获取对SecurityContext对象的引用。
- 创建一个新的DelegatingSecurityContextScheduledExecutorService对象,并将以下对象作为构造函数参数传递:
- 第一个构造函数参数是ScheduledExecutorService对象。 该对象用于调用计划的作业。 将创建的ScheduledExecutorService对象作为第一个构造函数参数传递。
- 第二个构造函数参数是SecurityContext对象。 创建的DelegatingSecurityContextScheduledExecutorService对象确保每个调用的作业都使用此SecurityContext 。 将创建的SecurityContext对象作为第二个构造函数参数传递。
- 返回创建的DelegatingSecurityContextScheduledExecutorService对象。
- 实现SchedulingConfigurer接口的configureTasks()方法 。 此方法将ScheduledTaskRegistrar对象作为方法参数。 通过执行以下步骤来实现此方法:
- 通过调用taskExecutor()方法创建一个新的Executor对象。
- 通过调用ScheduledTaskRegistrar类的setScheduler()方法来设置使用的调度程序 ,并将Executor对象作为方法参数传递。
ExampleApplicationContext类的源代码如下所示(相关部分已突出显示):
import org.springframework.context.annotation.*;
import org.springframework.context.support.PropertySourcesPlaceholderConfigurer;
import org.springframework.core.io.ClassPathResource;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.concurrent.DelegatingSecurityContextScheduledExecutorService;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.security.core.context.SecurityContextHolder;import java.util.Collection;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;@Configuration
@EnableScheduling
@ComponentScan(basePackages = {"net.petrikainulainen.spring.trenches.scheduling"
})
@Import(ExampleSecurityContext.class)
@PropertySource("classpath:application.properties")
public class ExampleApplicationContext implements SchedulingConfigurer {@Overridepublic void configureTasks(ScheduledTaskRegistrar taskRegistrar) {taskRegistrar.setScheduler(taskExecutor());}@Beanpublic Executor taskExecutor() {ScheduledExecutorService delegateExecutor = Executors.newSingleThreadScheduledExecutor();SecurityContext schedulerContext = createSchedulerSecurityContext();return new DelegatingSecurityContextScheduledExecutorService(delegateExecutor, schedulerContext);}private SecurityContext createSchedulerSecurityContext() {SecurityContext context = SecurityContextHolder.createEmptyContext();Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList("ROLE_USER");Authentication authentication = new UsernamePasswordAuthenticationToken("user","ROLE_USER",authorities);context.setAuthentication(authentication);return context;}@Beanpublic PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {PropertySourcesPlaceholderConfigurer properties = new PropertySourcesPlaceholderConfigurer();properties.setLocation(new ClassPathResource( "application.properties" ));properties.setIgnoreResourceNotFound(false);return properties;}
}
这就对了。 此配置确保每个计划的作业都可以访问由createSchedulerSecurityContext()方法创建的SecurityContext对象。 这意味着每个计划的作业都可以调用安全的方法,这些方法可以由角色为“ ROLE_USER”的用户调用。
让我们快速看一下我们的预定工作。
那预定的工作呢?
该解决方案的最好之处在于,我们不必对ScheduledJob类进行任何更改。 其源代码如下:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;@Component
public class ScheduledJob {private static final Logger LOGGER = LoggerFactory.getLogger(ScheduledJob.class);private final MessageService messageService;@Autowiredpublic ScheduledJob(MessageService messageService) {this.messageService = messageService;}@Scheduled(cron = "${scheduling.job.cron}")public void run() {String message = messageService.getMessage();LOGGER.debug("Received message: {}", message);}
}
调用计划作业时,将以下行写入日志:
2013-12-17 21:12:14,012 DEBUG - ScheduledJob - Received message: Hello World!
很酷 对?
摘要
现在,我们已经成功创建了可以调用安全方法的计划作业。 本教程教会了我们三件事:
- 我们了解到,通常将SecurityContext对象存储到ThreadLocal中 ,这意味着在同一线程中执行的所有计划作业均共享相同的安全上下文
- 我们了解到,如果我们的应用程序使用Spring Security 3.1,并且希望从计划的作业中调用安全方法,则最简单的方法是在每个计划的作业中配置使用的Authentication对象。
- 我们学习了如何使用Spring Security 3.2的并发支持,以及如何将SecurityContext对象从一个线程转移到另一个线程。
您可以从Github( Spring Security 3.1和Spring Security 3.2 )获得此博客文章的示例应用程序。
注意: Spring Security 3.2示例的XML配置目前无法正常工作。 如果有时间,我会修复它。
翻译自: https://www.javacodegeeks.com/2014/01/spring-from-the-trenches-invoking-a-secured-method-from-a-scheduled-job.html
多维柔性作业调用