blob: 9aa920ecd866c14c37a2914956d0fa5e3caf7bbb [file] [log] [blame]
package org.apache.shiro.session.mgt;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.CacheManagerAware;
import org.apache.shiro.event.Publisher;
import org.apache.shiro.session.*;
import org.apache.shiro.session.event.InvalidSessionEvent;
import org.apache.shiro.session.event.SessionEvent;
import org.apache.shiro.session.event.StartedSessionEvent;
import org.apache.shiro.session.event.StoppedSessionEvent;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.util.Assert;
import org.apache.shiro.util.CollectionUtils;
import org.apache.shiro.util.Destroyable;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.Serializable;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
/**
* @since 1.3
*/
public class StandardSessionManager implements NativeSessionManager, ValidatingSessionManager, CacheManagerAware, Destroyable {
private static final Logger log = LoggerFactory.getLogger(StandardSessionManager.class);
protected static final long MILLIS_PER_SECOND = 1000;
protected static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND;
public static final long DEFAULT_SESSION_TIMEOUT = 30 * MILLIS_PER_MINUTE; //30 minutes
private long defaultSessionTimeout = DEFAULT_SESSION_TIMEOUT;
private boolean deleteInvalidSessions;
private SessionValidationScheduler sessionValidationScheduler;
protected SessionFactory sessionFactory;
protected SessionDAO sessionDAO;
protected CacheManager cacheManager;
protected Publisher publisher;
public StandardSessionManager() {
this.sessionValidationScheduler = new ExecutorServiceSessionValidationScheduler(this);
this.deleteInvalidSessions = true;
this.sessionFactory = new SimpleSessionFactory();
this.sessionDAO = new MemorySessionDAO();
}
public long getDefaultSessionTimeout() {
return this.defaultSessionTimeout;
}
public void setDefaultSessionTimeout(long defaultSessionTimeout) {
this.defaultSessionTimeout = defaultSessionTimeout;
}
/**
* Returns {@code true} if sessions should be automatically deleted after they are discovered to be invalid,
* {@code false} if invalid sessions will be manually deleted by some process external to Shiro's control. The
* default is {@code true} to ensure no orphans exist in the underlying data store.
* <h4>Usage</h4>
* It is ok to set this to {@code false} <b><em>ONLY</em></b> if you have some other process that you manage yourself
* that periodically deletes invalid sessions from the backing data store over time, such as via a Quartz or Cron
* job. If you do not do this, the invalid sessions will become 'orphans' and fill up the data store over time.
* <p/>
* This property is provided because some systems need the ability to perform querying/reporting against sessions in
* the data store, even after they have stopped or expired. Setting this attribute to {@code false} will allow
* such querying, but with the caveat that the application developer/configurer deletes the sessions themselves by
* some other means (cron, quartz, etc).
*
* @return {@code true} if sessions should be automatically deleted after they are discovered to be invalid,
* {@code false} if invalid sessions will be manually deleted by some process external to Shiro's control.
*/
public boolean isDeleteInvalidSessions() {
return deleteInvalidSessions;
}
/**
* Sets whether or not sessions should be automatically deleted after they are discovered to be invalid. Default
* value is {@code true} to ensure no orphans will exist in the underlying data store.
* <h4>WARNING</h4>
* Only set this value to {@code false} if you are manually going to delete sessions yourself by some process
* (quartz, cron, etc) external to Shiro's control. See the
* {@link #isDeleteInvalidSessions() isDeleteInvalidSessions()} JavaDoc for more.
*
* @param deleteInvalidSessions whether or not sessions should be automatically deleted after they are discovered
* to be invalid.
*/
@SuppressWarnings("UnusedDeclaration")
public void setDeleteInvalidSessions(boolean deleteInvalidSessions) {
this.deleteInvalidSessions = deleteInvalidSessions;
}
public SessionValidationScheduler getSessionValidationScheduler() {
return sessionValidationScheduler;
}
public void setSessionValidationScheduler(SessionValidationScheduler sessionValidationScheduler) {
this.sessionValidationScheduler = sessionValidationScheduler;
}
public SessionDAO getSessionDAO() {
return this.sessionDAO;
}
public void setSessionDAO(SessionDAO sessionDAO) {
this.sessionDAO = sessionDAO;
applyCacheManagerToSessionDAO();
}
@SuppressWarnings("UnusedDeclaration")
public CacheManager getCacheManager() {
return this.cacheManager;
}
public void setCacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
applyCacheManagerToSessionDAO();
}
/**
* Sets the internal {@code CacheManager} on the {@code SessionDAO} if it implements the
* {@link org.apache.shiro.cache.CacheManagerAware CacheManagerAware} interface.
* <p/>
* This method is called after setting a cacheManager via the
* {@link #setCacheManager(org.apache.shiro.cache.CacheManager) setCacheManager} method <em>em</em> when
* setting a {@code SessionDAO} via the {@link #setSessionDAO} method to allow it to be propagated
* in either case.
*/
private void applyCacheManagerToSessionDAO() {
if (this.cacheManager != null && this.sessionDAO != null && this.sessionDAO instanceof CacheManagerAware) {
((CacheManagerAware) this.sessionDAO).setCacheManager(this.cacheManager);
}
}
/**
* Returns the {@code SessionFactory} used to generate new {@link Session} instances. The default instance
* is a {@link SimpleSessionFactory}.
*
* @return the {@code SessionFactory} used to generate new {@link Session} instances.
*/
public SessionFactory getSessionFactory() {
return sessionFactory;
}
/**
* Sets the {@code SessionFactory} used to generate new {@link Session} instances. The default instance
* is a {@link SimpleSessionFactory}.
*
* @param sessionFactory the {@code SessionFactory} used to generate new {@link Session} instances.
*/
@SuppressWarnings("UnusedDeclaration")
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
@SuppressWarnings("UnusedDeclaration")
public Publisher getPublisher() {
return publisher;
}
@SuppressWarnings("UnusedDeclaration")
public void setPublisher(Publisher publisher) {
this.publisher = publisher;
}
/* =====================================================================
Destroyable implementation
===================================================================== */
public void destroy() throws Exception {
if (this.sessionValidationScheduler != null) {
this.sessionValidationScheduler.disableSessionValidation();
}
}
/* =====================================================================
SessionManager implementation
===================================================================== */
public Session start(SessionContext context) {
enableSessionValidationIfNecessary();
Session internal = createInternalSession(context);
//Don't expose the EIS-tier Session object to the client-tier:
Session exposed = createExposedSession(internal, context);
//StartedSessionEvent event = new StartedSessionEvent(exposed, context);
//notify(event);
return exposed;
}
protected Session createInternalSession(SessionContext context) {
Session session = getSessionFactory().createSession(context);
if (log.isTraceEnabled()) {
log.trace("Creating session for host {}", session.getHost());
}
session.setTimeout(getDefaultSessionTimeout());
if (log.isDebugEnabled()) {
log.debug("Creating new EIS record for new session instance [" + session + "]");
}
createInternalSession(session, context);
return session;
}
protected void createInternalSession(Session session, SessionContext context) {
getSessionDAO().create(session);
}
public Session getSession(SessionKey key) throws SessionException {
enableSessionValidationIfNecessary();
Session session = getInternalSession(key);
return session != null ? createExposedSession(session, key) : null;
}
protected Session getInternalSession(SessionKey key) {
log.trace("Attempting to retrieve session with key {}", key);
Serializable sessionId = getSessionId(key);
if (sessionId == null) {
log.debug("Unable to resolve session ID from SessionKey [{}]. Returning null to indicate a " +
"session could not be found.", key);
return null;
}
Session session = getInternalSession(key, sessionId);
if (session == null) {
//session ID was provided, meaning one is expected to be found, but we couldn't find one:
String msg = "Could not find session with ID [" + sessionId + "]";
throw new UnknownSessionException(msg);
}
validate(session, key);
return session;
}
protected Session getInternalSession(SessionKey sessionKey, Serializable resolvedSessionId) {
return getSessionDAO().readSession(resolvedSessionId);
}
protected final void enableSessionValidationIfNecessary() {
if (this.sessionValidationScheduler != null && !this.sessionValidationScheduler.isEnabled()) {
this.sessionValidationScheduler.enableSessionValidation();
}
}
protected Serializable getSessionId(SessionKey sessionKey) {
return sessionKey.getSessionId();
}
/* =====================================================================
ValidatingSessionManager methods
===================================================================== */
public void validateSessions() {
log.debug("Validating active sessions...");
Collection<Session> activeSessions = getSessionDAO().getActiveSessions();
int invalidCount = validate(activeSessions);
if (log.isDebugEnabled()) {
String msg = "Finished session validation.";
if (invalidCount > 0) {
msg += " [" + invalidCount + "] sessions were stopped.";
} else {
msg += " No sessions were stopped.";
}
log.debug(msg);
}
}
protected int validate(Collection<Session> activeSessions) {
int invalidCount = 0;
if (activeSessions != null) {
for (Session s : activeSessions) {
try {
//simulate a lookup key to satisfy the method signature.
//this could probably stand to be cleaned up in future versions:
SessionKey key = new DefaultSessionKey(s.getId());
validate(s, key);
} catch (InvalidSessionException e) {
if (log.isTraceEnabled()) {
boolean expired = (e instanceof ExpiredSessionException);
String msg = "Invalidated session with id [" + s.getId() + "]" +
(expired ? " (expired)" : " (stopped)");
log.trace(msg);
}
invalidCount++;
}
}
}
return invalidCount;
}
protected void validate(Session session, SessionKey key) throws InvalidSessionException {
Assert.isInstanceOf(ValidatingSession.class, session, StandardSessionManager.class.getName() +
" implementations require native sessions to implement " + ValidatingSession.class.getName());
try {
((ValidatingSession) session).validate();
} catch (InvalidSessionException ise) {
onStop(session, key, ise);
throw ise;
}
}
protected void onStop(Session session, SessionKey key, InvalidSessionException ise) {
boolean expired = ise instanceof ExpiredSessionException;
if (session instanceof SimpleSession) {
SimpleSession ss = (SimpleSession) session;
if (expired) {
ss.setExpired(expired);
} else {
Date stopTs = ss.getStopTimestamp();
ss.setLastAccessTime(stopTs);
}
}
Session immutable = beforeStopNotification(session);
SessionEvent event = (ise != null) ?
new InvalidSessionEvent(immutable, key, ise) :
new StoppedSessionEvent(immutable, key);
notify(event);
if (isDeleteInvalidSessions()) {
delete(session, key);
} else {
update(session, key);
}
}
protected void delete(Session session, SessionKey key) {
log.debug("Deleting DAO session {}", session.getId());
getSessionDAO().delete(session);
}
protected void update(Session session, SessionKey key) {
log.debug("Updating DAO session {}", session.getId());
getSessionDAO().update(session);
}
@SuppressWarnings("unchecked")
protected void notify(Object event) {
if (event != null && this.publisher != null) {
this.publisher.publish(event);
}
}
protected Session createExposedSession(Session session, Object context) {
return new DelegatingSession(this, new DefaultSessionKey(session.getId()));
}
/**
* Returns the session instance to use to pass to registered {@code SessionListener}s for notification
* that the session has been stopped (stopped or expired).
* <p/>
* The default implementation returns an {@link ImmutableProxiedSession ImmutableProxiedSession} instance to ensure
* that the specified {@code session} argument is not modified by any listeners.
*
* @param session the stopped {@code Session}.
* @return the {@code Session} instance to use for {@link #notify(Object) notification}.
*/
protected Session beforeStopNotification(Session session) {
return new ImmutableProxiedSession(session);
}
/* =====================================================================
NativeSessionManager implementation
===================================================================== */
public Date getStartTimestamp(SessionKey key) {
return getInternalSession(key).getStartTimestamp();
}
public Date getLastAccessTime(SessionKey key) {
return getInternalSession(key).getLastAccessTime();
}
public long getTimeout(SessionKey key) throws InvalidSessionException {
return getInternalSession(key).getTimeout();
}
public void setTimeout(SessionKey key, long maxIdleTimeInMillis) throws InvalidSessionException {
Session session = getInternalSession(key);
session.setTimeout(maxIdleTimeInMillis);
update(session, key);
}
public void touch(SessionKey key) throws InvalidSessionException {
Session session = getInternalSession(key);
session.touch();
update(session, key);
}
public String getHost(SessionKey key) {
return getInternalSession(key).getHost();
}
public Collection<Object> getAttributeKeys(SessionKey key) {
Collection<Object> c = getInternalSession(key).getAttributeKeys();
if (!CollectionUtils.isEmpty(c)) {
return Collections.unmodifiableCollection(c);
}
return Collections.emptySet();
}
public Object getAttribute(SessionKey sessionKey, Object attributeKey) throws InvalidSessionException {
return getInternalSession(sessionKey).getAttribute(attributeKey);
}
public void setAttribute(SessionKey key, Object attributeKey, Object value) throws InvalidSessionException {
if (value == null) {
removeAttribute(key, attributeKey);
} else {
Session s = getInternalSession(key);
s.setAttribute(attributeKey, value);
update(s, key);
}
}
public Object removeAttribute(SessionKey key, Object attributeKey) throws InvalidSessionException {
Session session = getInternalSession(key);
Object removed = session.removeAttribute(attributeKey);
if (removed != null) {
update(session, key);
}
return removed;
}
public boolean isValid(SessionKey key) {
try {
checkValid(key);
return true;
} catch (InvalidSessionException e) {
return false;
}
}
public void stop(SessionKey key) throws InvalidSessionException {
Session session = getInternalSession(key);
if (log.isDebugEnabled()) {
log.debug("Stopping session with id [" + session.getId() + "]");
}
session.stop();
onStop(session, key, null);
}
public void checkValid(SessionKey key) throws InvalidSessionException {
//just try to acquire it. If there is a problem, an exception will be thrown:
getInternalSession(key);
}
}