I have web applicatoin who use spring boot, spring security and spring data. it is stateless.
I would like to avoid to alway call db for user acess. So i thinking using SpringCacheBasedUserCache.
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
CacheManager cacheManager() {
SimpleCacheManager cacheManager = new SimpleCacheManager();
cacheManager.setCaches(Arrays.asList(new ConcurrentMapCache("city"), new ConcurrentMapCache("userCache")));
return cacheManager;
}
@Bean
public UserCache userCache() throws Exception {
Cache cache = (Cache) cacheManager().getCache("userCache");
return new SpringCacheBasedUserCache(cache);
}
}
@EnableCaching
@Configuration
@EnableWebSecurity
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public UserDetailsService userDetailsServiceBean() throws Exception {
return new UserServiceImpl(commerceReposiotry, repository, defaultConfigRepository);
}
...
}
I have a class who implements UserDetails and another who implements UserDetailsService
@Service
public class UserServiceImpl implements UserDetailsService, UserService {
private final CommerceRepository commerceReposiotry;
private final UserAppRepository repository;
private final DefaultConfigRepository defaultConfigRepository;
@Autowired
private UserCache userCache;
@Autowired
private PasswordEncoder passwordEncoder;
@Autowired
public UserServiceImpl(final CommerceRepository commerceReposiotry, final UserAppRepository repository, final DefaultConfigRepository defaultConfigRepository) {
this.commerceReposiotry = commerceReposiotry;
this.repository = repository;
this.defaultConfigRepository = defaultConfigRepository;
}
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
UserDetails user = userCache.getUserFromCache(username);
UserApp userapp = null;
if (user == null) {
userapp = repository.findByUsername(username);
}
if (userapp == null) {
throw new UsernameNotFoundException("Username " + username + " not found");
}
userCache.putUserInCache(user);
return new CustomUserDetails(userapp);
}
...
}
In loadUserByUsername method, userCache is null
Either put @Bean
on the userDetailsServiceBean
method or (as suggested) remove caching from your UserDetailsService
completely and wrap it in a CachingUserDetailsService
and instead simply override the userDetailsService
method instead.
@Configuration
@EnableWebSecurity
public class ApplicationSecurity extends WebSecurityConfigurerAdapter {
@Autowired
private UserCache userCache;
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Override
public UserDetailsService userDetailsService() throws Exception {
UserServiceImpl userService = new UserServiceImpl(commerceReposiotry, repository, defaultConfigRepository);
CachingUserDetailsService cachingUserService = new CachingUserDetailsService(userService);
cachingUserService.setUserCache(this.userCache);
return cachingUserService;
}
...
}
You already have @EnableCaching
on your other configuration so no need to have that again. Simply inject the cache into the configuration class and construct a CachingUserDetailsService
which delegates to your UserDetailsService
to retrieve the user.
Ofcourse you will have to remove the caching from your own UserDetailsService
which can now be focused on user management/retrieval instead of being mixed with caching.
Edit(1): The constructor isn't public making it harder to create a bean. This can be achieved using BeanUtils
and ClassUtils
. Replace the call to new
with the following should create an instance.
private UserDetailsService cachingUserDetailsService(UserDetailsService delegate) {
Constructor<CachingUserDetailsService> ctor = ClassUtils.getConstructorIfAvailable(CachingUserDetailsService.class, UserDetailsService.class);
return BeanUtils.instantiateClass(ctor, delegate);
}
Edit (2): Apparently I already encountered this once already (about 2 years ago) and registered this issue for it.