/* (c) 2014 Open Source Geospatial Foundation - all rights reserved
* (c) 2014 OpenPlans
* This code is licensed under the GPL 2.0 license, available at the root
* application directory.
*/
package org.geoserver.security.auth;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.geoserver.security.auth.AuthenticationCache;
import org.geoserver.security.auth.AuthenticationCacheEntry;
import org.geoserver.security.auth.AuthenticationCacheKey;
import org.geotools.util.logging.Logging;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.scheduling.concurrent.CustomizableThreadFactory;
import org.springframework.security.core.Authentication;
import com.google.common.cache.Cache;
import com.google.common.cache.CacheBuilder;
/**
* Implementation of GeoServer AuthenticationCache based on Guava Cache.
*
* @author Mauro Bartolomeoli (mauro.bartolomeoli at geo-solutions.it)
*/
public class GuavaAuthenticationCacheImpl implements AuthenticationCache, DisposableBean {
/**
* Default eviction interval (double of the idle time).
*/
public static final int DEFAULT_CLEANUP_TIME = DEFAULT_IDLE_TIME * 2;
/**
* Default concurrency level (allows guava cache to optimize internal size to
* serve the given # of threads at the same time).
*/
public static final int DEFAULT_CONCURRENCY_LEVEL = 3;
private int timeToIdleSeconds, timeToLiveSeconds;
private final ScheduledExecutorService scheduler;
private Cache<AuthenticationCacheKey, AuthenticationCacheEntry> cache;
static Logger LOGGER = Logging.getLogger("org.geoserver.security");
/**
* Eviction thread code. Delegates to guava Cache cleanUp.
*/
private Runnable evictionTask = new Runnable() {
@Override
public void run() {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache Eviction task running");
LOGGER.fine("Cache entries #: " + cache.size());
}
cache.cleanUp();
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache Eviction task completed");
LOGGER.fine("Cache entries #: " + cache.size());
}
}
};
public GuavaAuthenticationCacheImpl(int maxEntries) {
this(maxEntries, DEFAULT_IDLE_TIME, DEFAULT_LIVE_TIME,
DEFAULT_CLEANUP_TIME, DEFAULT_CONCURRENCY_LEVEL);
}
// Use a counter to ensure a unique prefix for each pool.
private static AtomicInteger poolCounter = new AtomicInteger();
private ThreadFactory getThreadFactory() {
CustomizableThreadFactory tFactory = new CustomizableThreadFactory(String.format("GuavaAuthCache-%d-", poolCounter.getAndIncrement()));
tFactory.setDaemon(true);
return tFactory;
}
public GuavaAuthenticationCacheImpl(int maxEntries, int timeToIdleSeconds,
int timeToLiveSeconds, int cleanUpSeconds, int concurrencyLevel) {
this.timeToIdleSeconds = timeToIdleSeconds;
this.timeToLiveSeconds = timeToLiveSeconds;
scheduler = Executors
.newScheduledThreadPool(1, getThreadFactory());
cache = CacheBuilder.newBuilder()
.maximumSize(maxEntries)
.expireAfterAccess(timeToIdleSeconds, TimeUnit.SECONDS)
.expireAfterWrite(timeToLiveSeconds, TimeUnit.SECONDS)
.concurrencyLevel(concurrencyLevel).build();
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("AuthenticationCache Initialized with " + maxEntries
+ " Max Entries, " + timeToIdleSeconds + " seconds idle time, "
+ timeToLiveSeconds + " seconds time to live and "
+ concurrencyLevel + " concurrency level");
}
// schedule eviction thread
scheduler.scheduleAtFixedRate(evictionTask, cleanUpSeconds, cleanUpSeconds,
TimeUnit.SECONDS);
if (LOGGER.isLoggable(Level.INFO)) {
LOGGER.info("AuthenticationCache Eviction Task created to run every "
+ cleanUpSeconds + " seconds");
}
}
@Override
public void removeAll() {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache removing all entries");
LOGGER.fine("Cache entries #: " + cache.size());
}
cache.invalidateAll();
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache removed all entries");
LOGGER.fine("Cache entries #: " + cache.size());
}
}
@Override
public void removeAll(String filterName) {
if (filterName == null)
return;
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache removing all entries for "
+ filterName);
LOGGER.fine("Cache entries #: " + cache.size());
}
Set<AuthenticationCacheKey> toBeRemoved = new HashSet<AuthenticationCacheKey>();
for (AuthenticationCacheKey key : cache.asMap().keySet()) {
if (filterName.equals(key.getFilterName()))
toBeRemoved.add(key);
}
cache.invalidateAll(toBeRemoved);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache removed " + toBeRemoved.size()
+ " entries for " + filterName);
LOGGER.fine("Cache entries #: " + cache.size());
}
}
@Override
public void remove(String filterName, String cacheKey) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache removing " + filterName + ", "
+ cacheKey + " entry");
LOGGER.fine("Cache entries #: " + cache.size());
}
cache.invalidate(new AuthenticationCacheKey(filterName, cacheKey));
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache removed " + filterName + ", "
+ cacheKey + " entry");
LOGGER.fine("Cache entries #: " + cache.size());
}
}
@Override
public Authentication get(String filterName, String cacheKey) {
AuthenticationCacheEntry entry = cache
.getIfPresent(new AuthenticationCacheKey(filterName, cacheKey));
if (entry == null) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache has no entry for " + filterName
+ ", " + cacheKey);
}
return null;
}
long currentTime=System.currentTimeMillis();
if(entry.hasExpired(currentTime)) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("Entry has expired");
}
cache.invalidate(entry);
return null;
}
entry.setLastAccessed(System.currentTimeMillis());
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache found an entry for " + filterName
+ ", " + cacheKey);
}
return entry.getAuthentication();
}
@Override
public void put(String filterName, String cacheKey, Authentication auth,
Integer timeToIdleSeconds, Integer timeToLiveSeconds) {
timeToIdleSeconds = timeToIdleSeconds != null ? timeToIdleSeconds
: this.timeToIdleSeconds;
timeToLiveSeconds = timeToLiveSeconds != null ? timeToLiveSeconds
: this.timeToLiveSeconds;
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache adding new entry for " + filterName
+ ", " + cacheKey);
LOGGER.fine("Cache entries #: " + cache.size());
}
cache.put(new AuthenticationCacheKey(filterName, cacheKey),
new AuthenticationCacheEntry(auth, timeToIdleSeconds,
timeToLiveSeconds));
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache added new entry for " + filterName
+ ", " + cacheKey);
LOGGER.fine("Cache entries #: " + cache.size());
}
}
@Override
public void put(String filterName, String cacheKey, Authentication auth) {
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache adding new entry for " + filterName
+ ", " + cacheKey);
LOGGER.fine("Cache entries #: " + cache.size());
}
put(filterName, cacheKey, auth, timeToIdleSeconds, timeToLiveSeconds);
if (LOGGER.isLoggable(Level.FINE)) {
LOGGER.fine("AuthenticationCache added new entry for " + filterName
+ ", " + cacheKey);
LOGGER.fine("Cache entries #: " + cache.size());
}
}
public boolean isEmpty() {
return cache.size() == 0;
}
@Override
public void destroy() {
scheduler.shutdown();
}
}