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