001/*
002 * Licensed to the Apache Software Foundation (ASF) under one
003 * or more contributor license agreements.  See the NOTICE file
004 * distributed with this work for additional information
005 * regarding copyright ownership.  The ASF licenses this file
006 * to you under the Apache License, Version 2.0 (the
007 * "License"); you may not use this file except in compliance
008 * with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing,
013 * software distributed under the License is distributed on an
014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
015 * KIND, either express or implied.  See the License for the
016 * specific language governing permissions and limitations
017 * under the License.
018 */
019package org.apache.shiro.session.mgt;
020
021import org.apache.shiro.authz.AuthorizationException;
022import org.apache.shiro.session.ExpiredSessionException;
023import org.apache.shiro.session.InvalidSessionException;
024import org.apache.shiro.session.Session;
025import org.apache.shiro.session.UnknownSessionException;
026import org.apache.shiro.util.Destroyable;
027import org.apache.shiro.util.LifecycleUtils;
028import org.slf4j.Logger;
029import org.slf4j.LoggerFactory;
030
031import java.util.Collection;
032
033
034/**
035 * Default business-tier implementation of the {@link ValidatingSessionManager} interface.
036 *
037 * @since 0.1
038 */
039public abstract class AbstractValidatingSessionManager extends AbstractNativeSessionManager
040        implements ValidatingSessionManager, Destroyable {
041
042    //TODO - complete JavaDoc
043
044    private static final Logger log = LoggerFactory.getLogger(AbstractValidatingSessionManager.class);
045
046    /**
047     * The default interval at which sessions will be validated (1 hour);
048     * This can be overridden by calling {@link #setSessionValidationInterval(long)}
049     */
050    public static final long DEFAULT_SESSION_VALIDATION_INTERVAL = MILLIS_PER_HOUR;
051
052    protected boolean sessionValidationSchedulerEnabled;
053
054    /**
055     * Scheduler used to validate sessions on a regular basis.
056     */
057    protected SessionValidationScheduler sessionValidationScheduler;
058
059    protected long sessionValidationInterval;
060
061    public AbstractValidatingSessionManager() {
062        this.sessionValidationSchedulerEnabled = true;
063        this.sessionValidationInterval = DEFAULT_SESSION_VALIDATION_INTERVAL;
064    }
065
066    public boolean isSessionValidationSchedulerEnabled() {
067        return sessionValidationSchedulerEnabled;
068    }
069
070    @SuppressWarnings({"UnusedDeclaration"})
071    public void setSessionValidationSchedulerEnabled(boolean sessionValidationSchedulerEnabled) {
072        this.sessionValidationSchedulerEnabled = sessionValidationSchedulerEnabled;
073    }
074
075    public void setSessionValidationScheduler(SessionValidationScheduler sessionValidationScheduler) {
076        this.sessionValidationScheduler = sessionValidationScheduler;
077    }
078
079    public SessionValidationScheduler getSessionValidationScheduler() {
080        return sessionValidationScheduler;
081    }
082
083    private void enableSessionValidationIfNecessary() {
084        SessionValidationScheduler scheduler = getSessionValidationScheduler();
085        if (isSessionValidationSchedulerEnabled() && (scheduler == null || !scheduler.isEnabled())) {
086            enableSessionValidation();
087        }
088    }
089
090    /**
091     * If using the underlying default <tt>SessionValidationScheduler</tt> (that is, the
092     * {@link #setSessionValidationScheduler(SessionValidationScheduler) setSessionValidationScheduler} method is
093     * never called) , this method allows one to specify how
094     * frequently session should be validated (to check for orphans).  The default value is
095     * {@link #DEFAULT_SESSION_VALIDATION_INTERVAL}.
096     * <p/>
097     * If you override the default scheduler, it is assumed that overriding instance 'knows' how often to
098     * validate sessions, and this attribute will be ignored.
099     * <p/>
100     * Unless this method is called, the default value is {@link #DEFAULT_SESSION_VALIDATION_INTERVAL}.
101     *
102     * @param sessionValidationInterval the time in milliseconds between checking for valid sessions to reap orphans.
103     */
104    public void setSessionValidationInterval(long sessionValidationInterval) {
105        this.sessionValidationInterval = sessionValidationInterval;
106    }
107
108    public long getSessionValidationInterval() {
109        return sessionValidationInterval;
110    }
111
112    @Override
113    protected final Session doGetSession(final SessionKey key) throws InvalidSessionException {
114        enableSessionValidationIfNecessary();
115
116        log.trace("Attempting to retrieve session with key {}", key);
117
118        Session s = retrieveSession(key);
119        if (s != null) {
120            validate(s, key);
121        }
122        return s;
123    }
124
125    /**
126     * Looks up a session from the underlying data store based on the specified session key.
127     *
128     * @param key the session key to use to look up the target session.
129     * @return the session identified by {@code sessionId}.
130     * @throws UnknownSessionException if there is no session identified by {@code sessionId}.
131     */
132    protected abstract Session retrieveSession(SessionKey key) throws UnknownSessionException;
133
134    protected Session createSession(SessionContext context) throws AuthorizationException {
135        enableSessionValidationIfNecessary();
136        return doCreateSession(context);
137    }
138
139    protected abstract Session doCreateSession(SessionContext initData) throws AuthorizationException;
140
141    protected void validate(Session session, SessionKey key) throws InvalidSessionException {
142        try {
143            doValidate(session);
144        } catch (ExpiredSessionException ese) {
145            onExpiration(session, ese, key);
146            throw ese;
147        } catch (InvalidSessionException ise) {
148            onInvalidation(session, ise, key);
149            throw ise;
150        }
151    }
152
153    protected void onExpiration(Session s, ExpiredSessionException ese, SessionKey key) {
154        log.trace("Session with id [{}] has expired.", s.getId());
155        try {
156            onExpiration(s);
157            notifyExpiration(s);
158        } finally {
159            afterExpired(s);
160        }
161    }
162
163    protected void onExpiration(Session session) {
164        onChange(session);
165    }
166
167    protected void afterExpired(Session session) {
168    }
169
170    protected void onInvalidation(Session s, InvalidSessionException ise, SessionKey key) {
171        if (ise instanceof ExpiredSessionException) {
172            onExpiration(s, (ExpiredSessionException) ise, key);
173            return;
174        }
175        log.trace("Session with id [{}] is invalid.", s.getId());
176        try {
177            onStop(s);
178            notifyStop(s);
179        } finally {
180            afterStopped(s);
181        }
182    }
183
184    protected void doValidate(Session session) throws InvalidSessionException {
185        if (session instanceof ValidatingSession) {
186            ((ValidatingSession) session).validate();
187        } else {
188            String msg = "The " + getClass().getName() + " implementation only supports validating " +
189                    "Session implementations of the " + ValidatingSession.class.getName() + " interface.  " +
190                    "Please either implement this interface in your session implementation or override the " +
191                    AbstractValidatingSessionManager.class.getName() + ".doValidate(Session) method to perform validation.";
192            throw new IllegalStateException(msg);
193        }
194    }
195
196    /**
197     * Subclass template hook in case per-session timeout is not based on
198     * {@link org.apache.shiro.session.Session#getTimeout()}.
199     * <p/>
200     * <p>This implementation merely returns {@link org.apache.shiro.session.Session#getTimeout()}</p>
201     *
202     * @param session the session for which to determine session timeout.
203     * @return the time in milliseconds the specified session may remain idle before expiring.
204     */
205    protected long getTimeout(Session session) {
206        return session.getTimeout();
207    }
208
209    protected SessionValidationScheduler createSessionValidationScheduler() {
210        ExecutorServiceSessionValidationScheduler scheduler;
211
212        if (log.isDebugEnabled()) {
213            log.debug("No sessionValidationScheduler set.  Attempting to create default instance.");
214        }
215        scheduler = new ExecutorServiceSessionValidationScheduler(this);
216        scheduler.setInterval(getSessionValidationInterval());
217        if (log.isTraceEnabled()) {
218            log.trace("Created default SessionValidationScheduler instance of type [" + scheduler.getClass().getName() + "].");
219        }
220        return scheduler;
221    }
222
223    protected synchronized void enableSessionValidation() {
224        SessionValidationScheduler scheduler = getSessionValidationScheduler();
225        if (scheduler == null) {
226            scheduler = createSessionValidationScheduler();
227            setSessionValidationScheduler(scheduler);
228        }
229        // it is possible that that a scheduler was already created and set via 'setSessionValidationScheduler()'
230        // but would not have been enabled/started yet
231        if (!scheduler.isEnabled()) {
232            if (log.isInfoEnabled()) {
233                log.info("Enabling session validation scheduler...");
234            }
235            scheduler.enableSessionValidation();
236            afterSessionValidationEnabled();
237        }
238    }
239
240    protected void afterSessionValidationEnabled() {
241    }
242
243    protected synchronized void disableSessionValidation() {
244        beforeSessionValidationDisabled();
245        SessionValidationScheduler scheduler = getSessionValidationScheduler();
246        if (scheduler != null) {
247            try {
248                scheduler.disableSessionValidation();
249                if (log.isInfoEnabled()) {
250                    log.info("Disabled session validation scheduler.");
251                }
252            } catch (Exception e) {
253                if (log.isDebugEnabled()) {
254                    String msg = "Unable to disable SessionValidationScheduler.  Ignoring (shutting down)...";
255                    log.debug(msg, e);
256                }
257            }
258            LifecycleUtils.destroy(scheduler);
259            setSessionValidationScheduler(null);
260        }
261    }
262
263    protected void beforeSessionValidationDisabled() {
264    }
265
266    public void destroy() {
267        disableSessionValidation();
268    }
269
270    /**
271     * @see ValidatingSessionManager#validateSessions()
272     */
273    public void validateSessions() {
274        if (log.isInfoEnabled()) {
275            log.info("Validating all active sessions...");
276        }
277
278        int invalidCount = 0;
279
280        Collection<Session> activeSessions = getActiveSessions();
281
282        if (activeSessions != null && !activeSessions.isEmpty()) {
283            for (Session s : activeSessions) {
284                try {
285                    //simulate a lookup key to satisfy the method signature.
286                    //this could probably stand to be cleaned up in future versions:
287                    SessionKey key = new DefaultSessionKey(s.getId());
288                    validate(s, key);
289                } catch (InvalidSessionException e) {
290                    if (log.isDebugEnabled()) {
291                        boolean expired = (e instanceof ExpiredSessionException);
292                        String msg = "Invalidated session with id [" + s.getId() + "]" +
293                                (expired ? " (expired)" : " (stopped)");
294                        log.debug(msg);
295                    }
296                    invalidCount++;
297                }
298            }
299        }
300
301        if (log.isInfoEnabled()) {
302            String msg = "Finished session validation.";
303            if (invalidCount > 0) {
304                msg += "  [" + invalidCount + "] sessions were stopped.";
305            } else {
306                msg += "  No sessions were stopped.";
307            }
308            log.info(msg);
309        }
310    }
311
312    protected abstract Collection<Session> getActiveSessions();
313}