16

For learning purposes, I have made a custom authentication system where I pass a token from the client to the server through the Authorization header.

In the server side, I'd like to know if it's possible to create in the interceptor, before the request reaches a method in the controller, an User object with the email from the token as a property, and then pass this user object to every request where I require it.

This what I'd like to get, as an example:

@RestController
public class HelloController {

    @RequestMapping("/")
    public String index(final User user) {
        return user.getEmail();
    }

}

public class User {
    private String email;
}

Where user is an object that I created in the pre-interceptor using the request Authorization header and then I can pass, or not, to any method in the RestController.

Is this possible?

xenteros
  • 15,586
  • 12
  • 56
  • 91
Ulises CT
  • 1,361
  • 1
  • 12
  • 21

2 Answers2

13

#Recommended solution I would create a @Bean with @Scope request which would hold the user and then put the appropriate entity into that holder and then take from that holder inside the method.

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class CurrentUser {
    private User currentUser;

    public User getCurrentUser() {
        return currentUser;
    }

    public void setCurrentUser(User currentUser) {
        this.currentUser = currentUser;
    }
}

and then

@Component
public class MyInterceptor implements HandlerInterceptor {

   private CurrentUser currentUser;

   @Autowired
   MyInterceptor(CurrentUser currentUser) {
       this.currentUser = currentUser;
   }

   @Override
   public boolean preHandle(
      HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      this.currentUser.setCurrentUser(new User("whatever"));
      return true;
   }
}

and in the Controller

@RestController
public class HelloController {

    private CurrentUser currentUser;

    @Autowired
    HelloController(CurrentUser currentUser) {
        this.currentUser = currentUser;
    }

    @RequestMapping("/")
    public String index() {
        return currentUser.getCurrentUser().getEmail();
    }

}

#Alternative solution

In case your object that you would like to have, only contains one field, you can just cheat on that and add that field to the HttpServletRequest parameters and just see the magic happen.

@Component
public class MyInterceptor implements HandlerInterceptor {
   @Override
   public boolean preHandle(
      HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
      //TRY ONE AT THE TIME: email OR user
      //BOTH SHOULD WORK BUT SEPARATELY OF COURSE
      request.setAttribute("email", "login@domain.com");
      request.setAttribute("user", new User("login@domain.com"));
      return true;
   }
}
xenteros
  • 15,586
  • 12
  • 56
  • 91
  • is there no way to do what I put in my example? I pretty like it that way, I've seen it in a project that uses Cloud Endpoints from Google and Google Sign-in – Ulises CT Nov 19 '19 at 21:04
  • @UlisesCT please have a try on what I just added to the answer – xenteros Nov 19 '19 at 21:09
  • 1
    I'd seriously consider this solution @UlisesCT. It has a big advantage over what you would like: you can inject the current user into any bean involved in the handling of the request. So if you need it in your service layer for example, you don't need to pass it from method to method. You just inject the CurrentUser bean. But yes, you can store the user in a request attribute in the interceptor, and use use `@RequestAttribute` in the controller method. – JB Nizet Nov 19 '19 at 21:09
  • @UlisesCT please have a look at the full answer – xenteros Nov 19 '19 at 21:16
  • Okay, sounds good. I'll give it a try and let you know. Thanks! – Ulises CT Nov 19 '19 at 21:18
  • @UlisesCT you might also be interested in this topic https://stackoverflow.com/q/38716703/4723795 – xenteros Nov 19 '19 at 21:28
  • @xenteros The suggested solution implies RestController's scope is `request` as well - but this is generally not the case (Controller's scope is generally `singleton`, which is wider than `request`). How is this supposed to work? – Alex Lipov Jun 07 '21 at 15:03
  • 1
    @AlexLipov look at the alternative solution from my answer - it doesn't touch the scopes. However it is possible to have a request scoped bean inside the singleton bean using technique called ScopedProxy – xenteros Jun 08 '21 at 09:37
  • @AlexLipov @xenteros I think the class `CurrentUser` should have an annotation `@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)`, not just `@Scope(value = "request")`, if it is meant to be a scoped proxy. And without this correction, I think the code in this post would not work properly as @AlexLipov suggested. – Gorisanson Jun 02 '22 at 10:19
  • @Gorisanson 100% agree. Done. Thanks for spotting that! – xenteros Jun 08 '22 at 09:54
0

You can use a local thread context object as follows - which will be handling one parameter per request thread (thread safe):

public abstract class LoggedUserContext {

private static ThreadLocal<User> currentLoggedUser = new ThreadLocal<>();

public static void setCurrentLoggedUser(User loggedUser) {
    if (currentLoggedUser == null) {
        currentLoggedUser = new ThreadLocal<>();
    }
    currentLoggedUser.set(loggedUser);
}

public static User getCurrentLoggedUser() {
    return currentLoggedUser != null ? currentLoggedUser.get() : null;
}

public static void clear() {
    if (currentLoggedUser != null) {
        currentLoggedUser.remove();
    }
}

}

Then in the interceptor prehandle function:

LoggedUserContext.setCurrentLoggedUser(loggedUser);

And in the interceptor postHandler function:

  LoggedUserContext.clear();

From any other place:

User loggedUser = LoggedUserContext.getCurrentLoggedUser();
user666
  • 1,750
  • 3
  • 18
  • 34