How to get current user in every request in Spring Boot?

Viewed 12040

I would like to get the username of the user in every request to add them to log file.

This is my solution:

First, I created a LoggedUser with a static property:

public class LoggedUser {

    private static final ThreadLocal<String> userHolder = 
        new ThreadLocal<>();

    public static void logIn(String user) {
        userHolder.set(user);
    }

    public static void logOut() {
        userHolder.remove();
    }

    public static String get() {
        return userHolder.get();
    }
}

Then I created a support class to get username:

public interface AuthenticationFacade {
    Authentication getAuthentication();
}
@Component
public class AuthenticationFacadeImpl implements AuthenticationFacade {
    @Override
    public Authentication getAuthentication() {
        return SecurityContextHolder.getContext().getAuthentication();
    }
}

Finally, I used them in my Controllers:

    @RestController
    public class ResourceController {

        Logger logger = LoggerFactory.getLogger(ResourceController.class);

        @Autowired
        private GenericService userService;
        @Autowired
        private AuthenticationFacade authenticationFacade;

        @RequestMapping(value ="/cities")
        public List<RandomCity> getCitiesAndLogWhoIsRequesting(){
        loggedUser.logIn(authenticationFacade.getAuthentication().getName());
        logger.info(LoggedUser.get()); //Log username
        return userService.findAllRandomCities();
        }
    }

The problem is I don't want to have AuthenticationFacade in every @Controller, If I have 10000 controllers, for example, it will be a lot of works.

Do you have any better solution for it?

5 Answers

The solution is called Fish Tagging. Every decent logging framework has this functionality. Some frameworks call it MDC(Mapped Diagnostic Context). You can read about it here and here.

The basic idea is to use ThreadLocal or InheritableThreadLocal to hold a few key-value pairs in a thread to track a request. Using logging configuration, you can configure how to print it in the log entries.

Basically, you can write a filter, where you would retrieve the username from the security context and put it into the MDC and just forget about it. In your controller you log only the business logic related stuff. The username will be printed in the log entries along with timestamp, log level etc. (as per your log configuration).

With Jhovanni's suggestion, I created an AOP annotation like this:

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface LogUsername {
}

In the same package, I added new @Aop @Component class with AuthenticationFacade injection:

@Aspect
@Component
public class LogUsernameAop {
    Logger logger = LoggerFactory.getLogger(LogUsernameAop.class);
    @Autowired
    private AuthenticationFacade authenticationFacade;

    @Before("@annotation(LogUsername)")
    public void logUsername() throws Throwable {
        logger.info(authenticationFacade.getAuthentication().getName());
        LoggedUser.logIn(authenticationFacade.getAuthentication().getName());
    }
}

Then, in every @GetMapping method, If I need to log the username, I can add an annotation before the method:

@PostMapping
@LogUsername
public Course createCourse(@RequestBody Course course){
    return courseService.saveCourse(course);
}

Finally, this is the result:

2018-10-21 08:29:07.206  INFO 8708 --- [nio-8080-exec-2] com.khoa.aop.LogUsername                 : john.doe

Well, you are already accesing authentication object directly from SecurityContextHolder, you can do it in your controller.

@RequestMapping(value ="/cities")
public List<RandomCity> getCitiesAndLogWhoIsRequesting(){
  Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
  if(authentication != null){
    //log user name
    logger.info(authentication.get());
  }
  return userService.findAllRandomCities();
}

If you do not want to put all this in every endpoint, an utility method can be created to extract authentication and return its name if found.

public class UserUtil {
  public static String userName(){
    Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
    return authentication == null ? null : authentication.getName();
  }
}

and call it in your endpoint like

@RequestMapping(value ="/cities")
public List<RandomCity> getCitiesAndLogWhoIsRequesting(){
  //log user name
  logger.info(UserUtil.username());
  return userService.findAllRandomCities();
}

However, you are still adding lines of code in every endpoint, and after a few of them it starts to feel wrong being forced to do it. Something I suggest you to do is try aspect oriented programming for this kind of stuff. It will require you to invest some time in learning how it works, create annotations or executions required. But you should have it in a day or two. With aspect oriented your endpoint could end like this

@RequestMapping(value ="/cities")
@LogUserName
public List<RandomCity> getCitiesAndLogWhoIsRequesting(){
  //LogUserName annotation will inform this request should log user name if found
  return userService.findAllRandomCities();
}

of course, you are able to remove @LogUserName custom annotation and configure the new aspect with being triggered by methods inside a package, or classes extending @Controller, etc. Definitely it is worth the time, because you can use aspect for more than just logging user name.

You can obtain the username via request or parameter in your controller method. If you add Principal principal as a parameter, Spring Ioc Container will inject the information regarding the user or it will be null for anonymous users.

@RequestMapping(value ="/cities")
public List<RandomCity> getCitiesAndLogWhoIsRequesting(Principal principal){
    if(principal == null){
         // anonymous user
    }
}

There are various ways in Spring Security to fetch the user details from the security context. But according to your requirement, you are only interested in username, so you can try this:

@RequestMapping(value ="/cities")
public List<RandomCity> getCitiesAndLogWhoIsRequesting(Authentication authentication){
    logger.info(authentication.getName()); //Log username
    return userService.findAllRandomCities();
}

Hope this helps!

Related