摆脱困境:从计划作业中调用安全方法
假設我們已經實現了一個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()方法的計劃作業。
創建計劃的作業
我們可以按照以下步驟創建計劃的作業:
我們計劃的作業的源代碼如下所示:
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)該堆棧跟蹤實際上非常有用。 它告訴我們安全方法無法調用,因為從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可以調用安全方法,因為它在嘗試調用安全方法之前將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類的源代碼如下所示:
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類進行兩次修改。 我們可以按照以下步驟進行修改:
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的新并發支持,所以我們必須對應用程序上下文配置類進行以下更改( 原始配置在此博客文章中進行了描述 ):
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
總結
以上是生活随笔為你收集整理的摆脱困境:从计划作业中调用安全方法的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: vivo手机应用商店停止运行是怎么回事
- 下一篇: 955的含义 955解释