Using the new Spring Cache Abstraction in Spring Security


What the need?

Caching is handled by storing the UserDetails object being placed in the UserCache. This ensures that subsequent requests with the same username can be validated without needing to query the UserDetailsService. It should be noted that if a user appears to present an incorrect password, the UserDetailsService will be queried to confirm the most up-to-date password was used for comparison. 

Caching is only likely to be required for stateless applications. In a normal web application, for example, the SecurityContext is stored in the user's session and the user isn't reauthenticated on each request. 

The default cache implementation is therefore NullUserCache. An other implementation uses EhCache directly from the EhCache CacheManager. But why not deal with the new Spring Cache Abstraction implemented since the 3.1?

The solution

Here is a solution implementing the UserCache interface: 

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.cache.Cache;
import org.springframework.cache.Cache.ValueWrapper;
import org.springframework.security.core.userdetails.UserCache;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.Assert;

public class AdaptableUserCache implements UserCache, InitializingBean {

private static final Logger LOGGER = LoggerFactory.getLogger(AdaptableUserCache.class);

private Cache cache;

public AdaptableUserCache(Cache cache) {
this.cache = cache;
}

public void afterPropertiesSet() throws Exception {
Assert.notNull(cache, "cache mandatory");
}

@Override
public UserDetails getUserFromCache(String username) {
ValueWrapper element = cache.get(username.toLowerCase());

if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Cache hit: " + (element != null) + "; username: " + username.toLowerCase());
}

if (element == null) {
return null;
} else {
return (UserDetails) element.get();
}
}

@Override
public void putUserInCache(UserDetails user) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Cache put: " + user.getUsername().toLowerCase());
}

cache.put(user.getUsername().toLowerCase(), user);
}

@Override
public void removeUserFromCache(String username) {
if (LOGGER.isDebugEnabled()) {
LOGGER.debug("Cache remove: " + username.toLowerCase());
}
cache.evict(username.toLowerCase());
}
}

Then edit your SecurityConfig class in this way:

@Configuration
@ImportResource("classpath:spring/security.xml")
@ComponentScan(basePackages = "...")
public class SecurityConfig {

@Inject
private DaoAuthenticationProvider authenticationProvider;

@Inject
private CacheManager cacheManager;
  ...
@Bean
public UserCache userCache() throws Exception {
AdaptableUserCache userCache = new AdaptableUserCache(cacheManager.getCache("users"));
authenticationProvider.setUserCache(userCache);
return userCache;
}
}

Well done!
  

Labels: ,