| 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); |
| } |
| } |