开发者

Spring Security and @Async (Authenticated Users mixed up)

开发者 https://www.devze.com 2023-02-15 23:01 出处:网络
I asynchronously invoke a method with Spring, using @Async. This method invokes another method annotated with @PreAuthorize, the Spring Security Annotation. To make the authorization work I have to se

I asynchronously invoke a method with Spring, using @Async. This method invokes another method annotated with @PreAuthorize, the Spring Security Annotation. To make the authorization work I have to set SecurityContextHolder mode to MODE_INHERITABLETHREADLOCAL, so that the authentication info is passed to the asynchronous call. Everything works fine so far.

However when I logout and login as a different user, in the asynchronous method the SecurityContextHolder stores the authentication info of the old user that has been logged out. It causes of course unwanted AccessDenied exception. There is no such problem with synchronous calls.

I have defined <task:executor id="executors"开发者_如何学C pool-size="10"/>, so may it be a problem that once thread in executors pool has been initialized it will not override authentication information?


I guess MODE_INHERITABLETHREADLOCAL doesn't work correctly with thread pool.

As a possible solution you can try to subclass ThreadPoolTaskExecutor and override its methods to propagate SecurityContext manually, and then declare that executor instead of <task:executor>, something like this:

public void execute(final Runnable r) {
    final Authentication a = SecurityContextHolder.getContext().getAuthentication();

    super.execute(new Runnable() {
        public void run() {
            try {
                SecurityContext ctx = SecurityContextHolder.createEmptyContext();
                ctx.setAuthentication(a);
                SecurityContextHolder.setContext(ctx);
                r.run();
            } finally {
                SecurityContextHolder.clearContext();
            }
        }
    });
}


I also ran into that problem. It is important to configure the ThreadPoolTaskExecutor correctly using the DelegatingSecurityContextAsyncTaskExecutor. Also it is important to call the initialize() method, otherwise an error is thrown.

@Bean("threadPoolTaskExecutor")
public TaskExecutor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(20);
    executor.setMaxPoolSize(1000);
    executor.setWaitForTasksToCompleteOnShutdown(true);
    executor.setThreadNamePrefix("Async-");
    executor.initialize(); // this is important, otherwise an error is thrown
    return new DelegatingSecurityContextAsyncTaskExecutor(executor);
}

The method in your business logic which is called asynchronously:

@Override
@Async("threadPoolTaskExecutor")
public void methodName() {
    [..]
}


Update - other approach - more information: The solution described above works perfectly fine, if you only want to delegate the security context (i.e. the authentication information).

However, in certain situations you might also want to delegate other thread information, like the MDC context or request context, or you just want to have more control on how things are passed to the executor thread. If this is the case, you can bind the local thread information to the executor thread manually. The idea how this can be done is already described in the answer of @axtavt, but we can now use a TaskDecorator to do it in a more elegant way. The TaskDecorator stores the context of the request thread in variables and binds them in a closure so that the context can be accessed in the executor thread. When the thread execution is finished, the context is cleared from the executor thread, so that the information are gone when the thread is reused.

private static class ContextCopyingDecorator implements TaskDecorator {
    @NonNull
    @Override
    public Runnable decorate(@NonNull Runnable runnable) {
        // store context in variables which will be bound to the executor thread
        RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
        SecurityContext securityContext = SecurityContextHolder.getContext();
        Map<String, String> mdcContextMap = MDC.getCopyOfContextMap();
        return () -> {
            try {
                // code runs inside executor thread and binds context
                RequestContextHolder.setRequestAttributes(requestAttributes);
                SecurityContextHolder.setContext(securityContext);
                MDC.setContextMap(mdcContextMap);
                runnable.run();
            } finally {
                MDC.clear();
                RequestContextHolder.resetRequestAttributes();
                SecurityContextHolder.clearContext();
            }
        };
    }
}

During the TaskExecutor creation, the TaskDecorator is assigned to the TaskExecutor (with the setTaskDecorator method).

@Bean("threadPoolTaskExecutor")
public TaskExecutor getAsyncExecutor() {
    ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
    executor.setCorePoolSize(20);
    executor.setMaxPoolSize(1000);
    executor.setWaitForTasksToCompleteOnShutdown(true);
    executor.setThreadNamePrefix("Async-");
    executor.setTaskDecorator(new ContextCopyingDecorator());
    return executor;
}

You could also combine both approaches (e.g. use TaskDecorator to copy MDC context and still use DelegatingSecurityContextAsyncTaskExecutor for security context), but I would not recommend to do it because it increases complexity. If you use TaskDecorator anyway, you can also set the security context with it and if you only need the security context to be set, just use the DelegatingSecurityContextAsyncTaskExecutor approach.


This is just a hint that needs future investigation (I am too tired, but maybe somebody find this useful for future investigation):

Today I stumbled over org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor see GitHub.

it looks like that his designed to delegate the security context so that it is "passed" through the @Async call.

Also have a look at this post: Spring Security 3.2 M1 Highlights, Servlet 3 API Support is sounds like it is strongly related.


Using the information from Ralph and Oak -

If you want to get @Async working with the standard task executor tag, you would set up your Spring XML config like this

<task:annotation-driven executor="_importPool"/>
<task:executor id="_importPool" pool-size="5"/>

<bean id="importPool"
          class="org.springframework.security.task.DelegatingSecurityContextAsyncTaskExecutor">
     <constructor-arg ref="_importPool"/>
</bean>

Then in your @Async method, you would specify the pool you want to use

@Async("importPool")
public void run(ImportJob import) {
   ...
}

That should work so when whenever you call your @Async method, the threadpool thread will use the same security context as the calling thread


As it was already mentioned, for pooled threads environment DelegatingSecurityContextAsyncTaskExecutor should be used, instead of MODE_INHERITABLETHREADLOCAL (read here).

Leaving simple DelegatingSecurityContextAsyncTaskExecutor configuration for Spring Boot projects which will simply use default Spring Boot pool for async tasks:

@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    private final ThreadPoolTaskExecutor defaultSpringBootAsyncExecutor;

    public AsyncConfig(ThreadPoolTaskExecutor defaultSpringBootAsyncExecutor) {
        this.defaultSpringBootAsyncExecutor = defaultSpringBootAsyncExecutor;
    }

    @Override
    public Executor getAsyncExecutor() {
        return new DelegatingSecurityContextAsyncTaskExecutor(defaultSpringBootAsyncExecutor);
    }
}


Based on @Ralph answer one can achieve Aync event with Spring with threadpooling and delegate the security using http://docs.spring.io/autorepo/docs/spring-security/4.0.0.M1/apidocs/org/springframework/security/task/DelegatingSecurityContextAsyncTaskExecutor.html

Sample code

<bean id="applicationEventMulticaster"
    class="org.springframework.context.event.SimpleApplicationEventMulticaster">
    <property name="taskExecutor">
        <ref bean="delegateSecurityAsyncThreadPool"/>
    </property>
</bean>

<bean id="threadsPool"
    class="org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor">
</bean>


<bean id="delegateSecurityAsyncThreadPool"
    class="org.springframework.security.task.DelegatingSecurityContextTaskExecutor">
    <constructor-arg ref="threadsPool"/>
</bean>


Jus to add to the answer from @axtavt, you would also want to override other method.

@Override
    public <T> Future<T> submit(Callable<T> task) {
        ExecutorService executor = getThreadPoolExecutor();
        final Authentication a = SecurityContextHolder.getContext().getAuthentication();
        try {
            return executor.submit(new Callable<T>() {
                @Override
                public T call() throws Exception {
                    try {
                        SecurityContext ctx = SecurityContextHolder.createEmptyContext();
                        ctx.setAuthentication(a);
                        SecurityContextHolder.setContext(ctx);
                        return task.call();
                    } catch (Exception e) {
                        slf4jLogger.error("error invoking async thread. error details : {}", e);
                        return null;
                    } finally {
                        SecurityContextHolder.clearContext();
                    }
                }
            });
        } catch (RejectedExecutionException ex) {
            throw new TaskRejectedException("Executor [" + executor + "] did not accept task: " + task, ex);
        }
    }


I use an Enterprise Jboss Server with a JNDI managed thread pool; This is what worked for me:

@Configuration
@EnableAsync
public class AsyncAppConfig {

    public static final String THREAD_POOL_ID = "threadPoolId";

    @Bean(THREAD_POOL_ID)
    public DelegatingSecurityContextAsyncTaskExecutor secureThreadPool(
            DefaultManagedTaskExecutor jbossManagedTaskExecutor) {
        return new DelegatingSecurityContextAsyncTaskExecutor(jbossManagedTaskExecutor);
    }

    @Bean
    public DefaultManagedTaskExecutor jbossManagedTaskExecutor() {
        return new DefaultManagedTaskExecutor() {
            @Override
            public void afterPropertiesSet() throws NamingException {
                // gets the ConcurrentExecutor configured by Jboss
                super.afterPropertiesSet();
                // Wraps the jboss configured ConcurrentExecutor in a securityContext aware delegate
                setConcurrentExecutor(new DelegatingSecurityContextExecutor(getConcurrentExecutor(), getContext()));
            }
        };
    }
}


With Spring security 6 I had to disable explicit context save. I did it like below in websecurityConfig:

This is snippet for Kotlin

@Bean
    fun securityFilterChain(http: HttpSecurity): SecurityFilterChain {
        return http
            .securityContext().requireExplicitSave(false).and().....
0

精彩评论

暂无评论...
验证码 换一张
取 消