Sridhar Patnaik
Sridhar Patnaik

Reputation: 1118

Spring Boot - Store current user in global variable and initialise from API call when @service bean is created

I am creating a microservice architectured project with Zuul as gateway. I have all authentication handled in a service called common-service. I have exposed a API from common-service to return current logged in user. This is working fine.

Now, I have another microservice called inventory. In service class of inventory, I want to use current loggedin username in multiple methods. So, I am making a webclient call to common-service and getting current username. This is working fine but I am making a webclient API call to common service everytime I require username. Example - if I add a new entry, doing API call, then on update again API call etc. this seems not to be an optimised way

so problem is - I want to make this API call at global level. i.e. whenever my service bean is autowired, this API call should be made and username should be store somewhere which I can use across methods in my service call.

I tried @PostConstruct and @SessionAttributes but not able to get exact problem solved.

Can somebody help me with best suited solution or concept for handling this issue.

Below are code snippets

public class LeadService 
{
@Autowired
    WebClient.Builder webClientBuilder;
    
    @Autowired
    UserDetailsService userDetailsService;
//more autowiring

private void setLeadFields(Lead lead, @Valid LeadCreateData payload,String type) 
        {
            //some logic
            if(type.equalsIgnoreCase("create"))
            {
                lead.setAsigneeId(userDetailsService.getCurrentUser().getId());
                lead.setCreatorId(userDetailsService.getCurrentUser().getId());
            }
            else if(type.equalsIgnoreCase("update"))
            {
                //some logic
            }
            
        }

private StatusEnum setLeadStatus(Lead lead, StatusEnum status,String string) 
        {
            LeadStatus lstatus=null;
            switch(string)
            {
                
                case "create":
                    lstatus = new LeadStatus(lead.getLeadId(),status,userDetailsService.getCurrentUser().getId(),userDetailsService.getCurrentUser().getId());
                    lsRepo.save(lstatus);
                    break;
                case "udpate":
                    lstatus= lsRepo.FindLeadStatusByLeadID(lead.getLeadId()).get(0);
                    if(!lstatus.getStatus().equals(lstatus))
                    {
                        lstatus = new LeadStatus(lead.getLeadId(),status,userDetailsService.getCurrentUser().getId(),userDetailsService.getCurrentUser().getId());
                        lsRepo.save(lstatus);
                    }
                    break;
            }
            return lstatus.getStatus();
        }

private Address setAddress(@Valid LeadCreateData payload,Address address) 
        {
            //some setters
            address.setCreator(userDetailsService.getCurrentUser().getId());
            return aRepo.save(address);
        }

As you can see, I am using userDetailsService.getCurrentUser().getId() in many places. I am getting this id from below autowired method. But my one API call is required everytime I need this id.

@Service
public class UserDetailsService 
{
    @Autowired
    WebClient.Builder webClientBuilder;
    
    @Autowired
    HttpServletRequest request;
    
    @Value("${common.serverurl}")
    private String reqUrl;
    
    public UserReturnData getCurrentUser()
    {
        UserReturnData userDetails = webClientBuilder.build()
                            .get()
                            .uri(reqUrl+"user/me")
                            .header("Authorization", request.getHeader("Authorization"))
                            .retrieve()
                            .bodyToMono(UserReturnData.class)
                            .block();
        return userDetails;
    }
}

I want a optimal way where I can call this API method to get current user only once. and I can use it throughout my @service class.

Upvotes: 2

Views: 8064

Answers (1)

  • Create OncePerPrequestFilter or GenericFilterBean which has your UserDetailsService autowired.

  • And also you want to create something similar to RequestContextHolder or SecurityContextHolder which can hold your UserReturnData in a ThreadLocal variable. Look at those two spring classes to get idea but yours can be much simpler. Lets call it UserReturnDataContextHolder.

  • In the filter, you created in step1, when the request comes in populate it and when the response is leaving, clear it.

  • Now you can access it anywhere in the service via UserReturnDataContextHolder.getUserReturnData() and you are not making multiple calls either

Edit: The section below is contributed by Sridhar Patnaik as reference -

Below code to get it working

Added a class to store currentuserid

public class CurrentUser 
{
    private Long currentUserId;
//getter setter
}

Added a current user filter to intercept request and fetch current user.

public class CurrentUserFilter implements Filter 
{

    @Autowired
    private CurrentUser currentUser;

    @Autowired
    UserDetailsService UserDetailsService;
    
    @Override
    public void init(FilterConfig arg0) throws ServletException {
        // NOOP
    }

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) servletRequest;
        try 
        {
            this.currentUser.setCurrentUserId(UserDetailsService.getCurrentUser().getId());
            chain.doFilter(servletRequest, servletResponse);
        } 
        finally 
        {
            this.currentUser.clear();
        }
    }

    @Override
    public void destroy() {
        // NOOP
    }
}

Added required AppConfig

@Configuration
public class AppConfig 
{
    @Bean
    public Filter currentUserFilter() {
        return new CurrentUserFilter();
    }

    @Bean
    public FilterRegistrationBean tenantFilterRegistration() {
        FilterRegistrationBean result = new FilterRegistrationBean();
        result.setFilter(this.currentUserFilter());
        result.setUrlPatterns(Lists.newArrayList("/*"));
        result.setName("Tenant Store Filter");
        result.setOrder(1);
        return result;
    }

    @Bean(destroyMethod = "destroy")
    public ThreadLocalTargetSource threadLocalTenantStore() {
        ThreadLocalTargetSource result = new ThreadLocalTargetSource();
        result.setTargetBeanName("tenantStore");
        return result;
    }

    @Primary
    @Bean(name = "proxiedThreadLocalTargetSource")
    public ProxyFactoryBean proxiedThreadLocalTargetSource(ThreadLocalTargetSource threadLocalTargetSource) {
        ProxyFactoryBean result = new ProxyFactoryBean();
        result.setTargetSource(threadLocalTargetSource);
        return result;
    }

    @Bean(name = "tenantStore")
    @Scope(scopeName = "prototype")
    public CurrentUser tenantStore() {
        return new CurrentUser();
    }
}

And then autowired CurrentUser to my existing service class.

{..
@Autowired
    CurrentUser currentUser;
...
private void setLeadFields(Lead lead, @Valid LeadCreateData payload,String type) 
        {
            //some logic
            
            if(type.equalsIgnoreCase("create"))
            {
                lead.setAsigneeId(currentUser.getCurrentUserId());
                lead.setCreatorId(currentUser.getCurrentUserId());
                lead.setAddress(setAddress(payload, new Address()));
            }
            else if(type.equalsIgnoreCase("update"))
            {
                lead.setAsigneeId(userDetailsService.getUserFromId(payload.getAssigneeId()).getId());
                lead.setAddress(setAddress(payload,lead.getAddress()));
            }
            
        }

Upvotes: 6

Related Questions