Spring boot - Pass argument from interceptor to method in controller
Asked Answered
T

2

17

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?

Touchdown answered 19/11, 2019 at 20:50 Comment(1)
This is a great questionSullage
D
14

#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", "[email protected]");
      request.setAttribute("user", new User("[email protected]"));
      return true;
   }
}
Degrade answered 19/11, 2019 at 20:56 Comment(10)
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-inTouchdown
@UlisesCT please have a try on what I just added to the answerDegrade
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.Kinelski
@UlisesCT please have a look at the full answerDegrade
Okay, sounds good. I'll give it a try and let you know. Thanks!Touchdown
@UlisesCT you might also be interested in this topic https://mcmap.net/q/226165/-how-do-i-disable-resolving-login-parameters-passed-as-url-parameters-from-the-url/4723795Degrade
@Degrade 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?Gratulate
@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 ScopedProxyDegrade
@AlexLipov @Degrade 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.Aloysius
@Aloysius 100% agree. Done. Thanks for spotting that!Degrade
G
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();
Gandzha answered 9/1, 2023 at 14:34 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.