1

As the jobs operate asynchronously they can't access to the user's session. A solution needs to be found so that that the jobs can access the user's session (if the user is still logged in at that moment).

User session

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

public class UserPrincipal implements UserDetails {

private User user;

private Collection<SimpleGrantedAuthority> grantedAuthorities;

public UserPrincipal(User user) {
    Assert.notNull(user);
    this.user = user;

    Set<SimpleGrantedAuthority> authorities = new LinkedHashSet<>();
    for (Role role : user.getRoles()) {
        authorities.add(new SimpleGrantedAuthority(role.getName().toUpperCase(Locale.ENGLISH)));
    }
    grantedAuthorities = Collections.unmodifiableCollection(authorities);
}

}

Abstract Job class

public abstract class Job implements Runnable {

protected Logger logger = LoggerFactory.getLogger(getClass());

protected Job() {
}

@Override
public final void run() {
    logger.debug("starting work");
    /* code goes here */
    logger.debug("work is done");
}
}

Job class

@Component
@Scope(value = "prototype")
public class ProcessLoggingJob extends Job {

@Override
protected void work(Map<String, Object> context) throws Exception {
    // need to access user session here
}
Ritesh
  • 7,472
  • 2
  • 39
  • 43
Kushan
  • 10,657
  • 4
  • 37
  • 41

1 Answers1

2

Async jobs are executed by another thread (as expected). The session is managed by the application server and offered by the request. Spring security additional manages a context in a thread local to access it without the request (as @Michael shows in his Answer).

As the security context (got from the session) is held in one thread local (typical the HTTP-thread of the application server), the async job runs in another thread with no chance to access the thread local of the request thread.

The only chance I see is to use a queue mechanism, creating new job data from the request thread including the user data, passing them to a queue, processing data from the queue in the job.

In the request handling thread this could look like (missing null handling):

private BlockingQueue<UserDetails> usersToProceedAsync;

public void doSomethingInRequestThread() throws InterruptedException {
  UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()
  ...
  usersToProceedAsync.put(principal);
}

The job implementation may be:

private BlockingQueue<UserDetails> usersToProceedAsync;

protected void work(Map<String, Object> context) throws Exception {
  UserDetails principal = usersToProceedAsync.poll();
  if (principal != null) {
    ...
  }
}

You just have to connect the two classes by injecting the same queue to both (a LinkedBlockingQueue e.g.). If the job instance is created per job run, you will need a factory to inject the queue.

Be careful when creating one job data per request! How to ensure the async job is fast enough to process all the work? You have many options to tune the behavior using other methods to add to or remove data from the BlockingQueue and/or by using another implementation like the bounded ArrayBlockingQueue.

An other idea may be to use the executor framework instead of scheduled job execution. Just create a executor in your request handler and let it run the jobs for you:

private final Executor asyncJobs = Executors.newCachedThreadPool()

public void doSomethingInRequestThread() throws InterruptedException {
  UserDetails principal = (UserDetails) SecurityContextHolder.getContext().getAuthentication().getPrincipal()
  ...
  asyncJobs.execute(new AsyncJob(principal));
}

private class AsyncJob implements Runnable {
  private final UserDetails principal;

  public AsyncJob(UserDetails principal) {
    this.principal = principal;
  }

  public void run() {
    ...
  }
}
Arne Burmeister
  • 20,046
  • 8
  • 53
  • 94