33

Estoy intentando obtener el usuario del contexto que ejecuta un método asíncrono en un proyecto con Spring y Spring security de la siguiente forma:

@Service
    @Transactional
    public class FooServiceImpl implements FooService {

        @Async("asyncExecutor")
        public void fooMethod(String bar) {
            System.out.println("Foo: " + bar);
            Authentication auth = SecurityContextHolder.getContext().getAuthentication();
        }
    }

El resultado es nulo, pues el contexto no se propaga a los métodos asíncronos que se ejecutan en otro hilo. Lo he solucionado usando un SecurityContextDelegationAsyncTaskExecutor. Con esto se propaga el usuario logueado al método asíncrono. El problema es que si me deslogueo de la aplicación el usuario es nulo. Lo que quiero es el usuario que ejecuta el método y no el actual.

Mi código:

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {

    @Override
    @Bean(name = "asyncExecutor")
    public Executor getAsyncExecutor() {

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        executor.setMaxPoolSize(1);
        executor.setThreadGroupName("MyCustomExecutor");
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setBeanName("asyncExecutor");
        executor.initialize();

        return new DelegatingSecurityContextAsyncTaskExecutor(executor);
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }
}

También he probado a configurar el Contexto de spring con la opción MODE_INHERITABLETHREADLOCAL. Lo que hace es propagar el contexto entre hilos; pero tengo el mismo problema. Al desloguearse, el usuario en el método asíncrono es nulo. El código:

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {

    @Override
    @Bean(name = "asyncExecutor")
    public Executor getAsyncExecutor() {

        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();

        executor.setMaxPoolSize(1);
        executor.setThreadGroupName("MyCustomExecutor");
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setBeanName("asyncExecutor");
        executor.initialize();

        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }

}

@Configuration
@ComponentScan(basePackageClasses = Application.class, includeFilters = @Filter({Controller.class}), useDefaultFilters = true)
public class MvcConfiguration extends WebMvcConfigurationSupport {

    //others beans

    @Bean
    public MethodInvokingFactoryBean methodInvokingFactoryBean() {
        MethodInvokingFactoryBean methodInvokingFactoryBean = new MethodInvokingFactoryBean();
        methodInvokingFactoryBean.setTargetClass(SecurityContextHolder.class);
        methodInvokingFactoryBean.setTargetMethod("setStrategyName");
        methodInvokingFactoryBean.setArguments(new String[]{SecurityContextHolder.MODE_INHERITABLETHREADLOCAL});
        return methodInvokingFactoryBean;
    }
}

Por último, he implementado un ThreadPoolTaskExecutor sobreescribiendo el método execute, para que cree un contexto nuevo y lo copie. El problema es que mi método execute no se ejecuta; en vez de este método se ejecuta el submit del ThreadPoolTaskExecutor. La implementación:

@Configuration
@EnableAsync
public class SpringAsyncConfig implements AsyncConfigurer {

    @Override
    @Bean(name = "asyncExecutor")
    public Executor getAsyncExecutor() {

        CustomThreadPoolTaskExecutor executor = new CustomThreadPoolTaskExecutor();

        executor.setMaxPoolSize(1);
        executor.setThreadGroupName("MyCustomExecutor");
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setBeanName("asyncExecutor");
        executor.initialize();

        return executor;
    }

    @Override
    public AsyncUncaughtExceptionHandler getAsyncUncaughtExceptionHandler() {
        return new CustomAsyncExceptionHandler();
    }

}

public class CustomThreadPoolTaskExecutor extends ThreadPoolTaskExecutor {

    private static final long serialVersionUID = 1L;

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

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

¿Qué hago mal? ¿Alguna forma de almacenar y recuperar el usuario que ejecuta un método asíncrono?

cnbandicoot
  • 2,614
  • 8
  • 24
oscar
  • 431
  • 3
  • 4
  • 9
    ¿No sería más sencillo obtener el nombre del usuario antes de ejecutar el método asíncrono y mandarlo como dato adicional para evitar todo este trabajo? –  May 13 '16 at 17:43
  • Me valdría para algunos casos en la aplicación. Pero por ejemplo, estoy haciendo una auditoría de un objeto con un "@Aspect", con un método "@AfterReturning" que se ejecuta al usar el método save el repositorio de spring data. En este caso quiero guardar el usuario, pero el método save se ejecuta muchas veces en un método asíncrono de servicio que puede durar unos minutos y no tengo forma de pasarle el usuario al método de repositorio. – oscar May 13 '16 at 17:52
  • Los objetos que manejas, si sin auditables todos ellos deberían tener el nombre del usuario como parte de sus datos. –  May 13 '16 at 17:54
  • No me serviría, porque lo que quiero guardar es un historial de los usuarios que realizan cambios. Por ejemplo la entaidad Foo está en el estado 1, un usuario la edita y pasa al estado 2, quiero guardar en una tabla Foo_Historia, un registro, con el nuevo estado, la fecha y el usuario que realiza el cambio. – oscar May 13 '16 at 18:06
  • 4
    Si en tu proyecto utilizas Hibernate, deberías echar un ojo a Spring Data Envers, ya que hace exactamente eso, por cada entidad anotada con @Audited te crea una tabla de histórico "Foo_aud" con el estado, el usuario y la fecha de modificación y creación. – Javier Alvarez Oct 13 '16 at 11:11
  • La solucion es enviar el nombre de usuario o lo que necesites saber del usuario a la tarea asincrona. es Otro hilo! – sioesi Oct 24 '16 at 13:16
  • Nunca intentaste recuperar el usuario y hacer una replica de los datos del usuario en la clase de tu servicio?. Se supone que el usuario inicia el servicio, en el momento que se inicie, haces un clon del usuario, por lo tanto pasa a ser parte de la instancia del servicio en tu hilo asincrono – UselesssCat Mar 16 '17 at 17:59
  • Al final la solución es enviar a mis métodos asíncronos el usuario como parámetro, de esta forma aunque se haga un logout la función asíncrona tiene una copia del usuario que ejecutó la acción – oscar Apr 14 '17 at 11:18
  • 2
    @oscar me alegra que hayas encontrado la respuestas. Por favor responde tu mismo al pregunta y marcala como la aceptada para que desaparezca de las preguntas sin responder. – Einer Jul 11 '17 at 18:10

2 Answers2

1

Debes habilitar SecurityContextHolder.MODE_INHERITABLETHREADLOCAL.

SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);

Fuente: Baeldung

rvillablanca
  • 171
  • 4
1

entiendo un poco lo que quieres hacer, yo te propondría lo siguiente, en spring existe algo llamado @aspect, aquí puedes encontrar documentación al respecto Aspect Oriented Programming with Spring, básicamente se trata de métodos que se ejecutan al momento de que se inicia, finaliza o durante otro método.

En una clase marcada con @Component y @Aspect puedes crear un método de la siguiente manera:

@After("execution(*ruta.de.la.clase.nombreClase.nombreMétodoCambiaDatos(..))")
public synchronized void saveChangeLog() {
  // Aquí puedes obtener el usuario y
  //  hace la lógica para almacenar en foo_historial
}

Y @Autowired los repositorios que necesites puedes almacenar todo en la BD.

Dwan Z
  • 71
  • 6