View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *     http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.shiro.session.mgt;
20  
21  import org.apache.shiro.session.ExpiredSessionException;
22  import org.apache.shiro.session.InvalidSessionException;
23  import org.apache.shiro.session.StoppedSessionException;
24  import org.apache.shiro.util.CollectionUtils;
25  import org.slf4j.Logger;
26  import org.slf4j.LoggerFactory;
27  
28  import java.io.IOException;
29  import java.io.ObjectInputStream;
30  import java.io.ObjectOutputStream;
31  import java.io.Serializable;
32  import java.text.DateFormat;
33  import java.util.*;
34  
35  
36  /**
37   * Simple {@link org.apache.shiro.session.Session} JavaBeans-compatible POJO implementation, intended to be used on the
38   * business/server tier.
39   *
40   * @since 0.1
41   */
42  public class SimpleSession implements ValidatingSession, Serializable {
43  
44      // Serialization reminder:
45      // You _MUST_ change this number if you introduce a change to this class
46      // that is NOT serialization backwards compatible.  Serialization-compatible
47      // changes do not require a change to this number.  If you need to generate
48      // a new number in this case, use the JDK's 'serialver' program to generate it.
49      private static final long serialVersionUID = -7125642695178165650L;
50  
51      //TODO - complete JavaDoc
52      private transient static final Logger log = LoggerFactory.getLogger(SimpleSession.class);
53  
54      protected static final long MILLIS_PER_SECOND = 1000;
55      protected static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND;
56      protected static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE;
57  
58      //serialization bitmask fields. DO NOT CHANGE THE ORDER THEY ARE DECLARED!
59      static int bitIndexCounter = 0;
60      private static final int ID_BIT_MASK = 1 << bitIndexCounter++;
61      private static final int START_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++;
62      private static final int STOP_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++;
63      private static final int LAST_ACCESS_TIME_BIT_MASK = 1 << bitIndexCounter++;
64      private static final int TIMEOUT_BIT_MASK = 1 << bitIndexCounter++;
65      private static final int EXPIRED_BIT_MASK = 1 << bitIndexCounter++;
66      private static final int HOST_BIT_MASK = 1 << bitIndexCounter++;
67      private static final int ATTRIBUTES_BIT_MASK = 1 << bitIndexCounter++;
68  
69      // ==============================================================
70      // NOTICE:
71      //
72      // The following fields are marked as transient to avoid double-serialization.
73      // They are in fact serialized (even though 'transient' usually indicates otherwise),
74      // but they are serialized explicitly via the writeObject and readObject implementations
75      // in this class.
76      //
77      // If we didn't declare them as transient, the out.defaultWriteObject(); call in writeObject would
78      // serialize all non-transient fields as well, effectively doubly serializing the fields (also
79      // doubling the serialization size).
80      //
81      // This finding, with discussion, was covered here:
82      //
83      // http://mail-archives.apache.org/mod_mbox/shiro-user/201109.mbox/%3C4E81BCBD.8060909@metaphysis.net%3E
84      //
85      // ==============================================================
86      private transient Serializable id;
87      private transient Date startTimestamp;
88      private transient Date stopTimestamp;
89      private transient Date lastAccessTime;
90      private transient long timeout;
91      private transient boolean expired;
92      private transient String host;
93      private transient Map<Object, Object> attributes;
94  
95      public SimpleSession() {
96          this.timeout = DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT; //TODO - remove concrete reference to DefaultSessionManager
97          this.startTimestamp = new Date();
98          this.lastAccessTime = this.startTimestamp;
99      }
100 
101     public SimpleSession(String host) {
102         this();
103         this.host = host;
104     }
105 
106     public Serializable getId() {
107         return this.id;
108     }
109 
110     public void setId(Serializable id) {
111         this.id = id;
112     }
113 
114     public Date getStartTimestamp() {
115         return startTimestamp;
116     }
117 
118     public void setStartTimestamp(Date startTimestamp) {
119         this.startTimestamp = startTimestamp;
120     }
121 
122     /**
123      * Returns the time the session was stopped, or <tt>null</tt> if the session is still active.
124      * <p/>
125      * A session may become stopped under a number of conditions:
126      * <ul>
127      * <li>If the user logs out of the system, their current session is terminated (released).</li>
128      * <li>If the session expires</li>
129      * <li>The application explicitly calls {@link #stop()}</li>
130      * <li>If there is an internal system error and the session state can no longer accurately
131      * reflect the user's behavior, such in the case of a system crash</li>
132      * </ul>
133      * <p/>
134      * Once stopped, a session may no longer be used.  It is locked from all further activity.
135      *
136      * @return The time the session was stopped, or <tt>null</tt> if the session is still
137      *         active.
138      */
139     public Date getStopTimestamp() {
140         return stopTimestamp;
141     }
142 
143     public void setStopTimestamp(Date stopTimestamp) {
144         this.stopTimestamp = stopTimestamp;
145     }
146 
147     public Date getLastAccessTime() {
148         return lastAccessTime;
149     }
150 
151     public void setLastAccessTime(Date lastAccessTime) {
152         this.lastAccessTime = lastAccessTime;
153     }
154 
155     /**
156      * Returns true if this session has expired, false otherwise.  If the session has
157      * expired, no further user interaction with the system may be done under this session.
158      *
159      * @return true if this session has expired, false otherwise.
160      */
161     public boolean isExpired() {
162         return expired;
163     }
164 
165     public void setExpired(boolean expired) {
166         this.expired = expired;
167     }
168 
169     public long getTimeout() {
170         return timeout;
171     }
172 
173     public void setTimeout(long timeout) {
174         this.timeout = timeout;
175     }
176 
177     public String getHost() {
178         return host;
179     }
180 
181     public void setHost(String host) {
182         this.host = host;
183     }
184 
185     public Map<Object, Object> getAttributes() {
186         return attributes;
187     }
188 
189     public void setAttributes(Map<Object, Object> attributes) {
190         this.attributes = attributes;
191     }
192 
193     public void touch() {
194         this.lastAccessTime = new Date();
195     }
196 
197     public void stop() {
198         if (this.stopTimestamp == null) {
199             this.stopTimestamp = new Date();
200         }
201     }
202 
203     protected boolean isStopped() {
204         return getStopTimestamp() != null;
205     }
206 
207     protected void expire() {
208         stop();
209         this.expired = true;
210     }
211 
212     /**
213      * @since 0.9
214      */
215     public boolean isValid() {
216         return !isStopped() && !isExpired();
217     }
218 
219     /**
220      * Determines if this session is expired.
221      *
222      * @return true if the specified session has expired, false otherwise.
223      */
224     protected boolean isTimedOut() {
225 
226         if (isExpired()) {
227             return true;
228         }
229 
230         long timeout = getTimeout();
231 
232         if (timeout >= 0l) {
233 
234             Date lastAccessTime = getLastAccessTime();
235 
236             if (lastAccessTime == null) {
237                 String msg = "session.lastAccessTime for session with id [" +
238                         getId() + "] is null.  This value must be set at " +
239                         "least once, preferably at least upon instantiation.  Please check the " +
240                         getClass().getName() + " implementation and ensure " +
241                         "this value will be set (perhaps in the constructor?)";
242                 throw new IllegalStateException(msg);
243             }
244 
245             // Calculate at what time a session would have been last accessed
246             // for it to be expired at this point.  In other words, subtract
247             // from the current time the amount of time that a session can
248             // be inactive before expiring.  If the session was last accessed
249             // before this time, it is expired.
250             long expireTimeMillis = System.currentTimeMillis() - timeout;
251             Date expireTime = new Date(expireTimeMillis);
252             return lastAccessTime.before(expireTime);
253         } else {
254             if (log.isTraceEnabled()) {
255                 log.trace("No timeout for session with id [" + getId() +
256                         "].  Session is not considered expired.");
257             }
258         }
259 
260         return false;
261     }
262 
263     public void validate() throws InvalidSessionException {
264         //check for stopped:
265         if (isStopped()) {
266             //timestamp is set, so the session is considered stopped:
267             String msg = "Session with id [" + getId() + "] has been " +
268                     "explicitly stopped.  No further interaction under this session is " +
269                     "allowed.";
270             throw new StoppedSessionException(msg);
271         }
272 
273         //check for expiration
274         if (isTimedOut()) {
275             expire();
276 
277             //throw an exception explaining details of why it expired:
278             Date lastAccessTime = getLastAccessTime();
279             long timeout = getTimeout();
280 
281             Serializable sessionId = getId();
282 
283             DateFormat df = DateFormat.getInstance();
284             String msg = "Session with id [" + sessionId + "] has expired. " +
285                     "Last access time: " + df.format(lastAccessTime) +
286                     ".  Current time: " + df.format(new Date()) +
287                     ".  Session timeout is set to " + timeout / MILLIS_PER_SECOND + " seconds (" +
288                     timeout / MILLIS_PER_MINUTE + " minutes)";
289             if (log.isTraceEnabled()) {
290                 log.trace(msg);
291             }
292             throw new ExpiredSessionException(msg);
293         }
294     }
295 
296     private Map<Object, Object> getAttributesLazy() {
297         Map<Object, Object> attributes = getAttributes();
298         if (attributes == null) {
299             attributes = new HashMap<Object, Object>();
300             setAttributes(attributes);
301         }
302         return attributes;
303     }
304 
305     public Collection<Object> getAttributeKeys() throws InvalidSessionException {
306         Map<Object, Object> attributes = getAttributes();
307         if (attributes == null) {
308             return Collections.emptySet();
309         }
310         return attributes.keySet();
311     }
312 
313     public Object getAttribute(Object key) {
314         Map<Object, Object> attributes = getAttributes();
315         if (attributes == null) {
316             return null;
317         }
318         return attributes.get(key);
319     }
320 
321     public void setAttribute(Object key, Object value) {
322         if (value == null) {
323             removeAttribute(key);
324         } else {
325             getAttributesLazy().put(key, value);
326         }
327     }
328 
329     public Object removeAttribute(Object key) {
330         Map<Object, Object> attributes = getAttributes();
331         if (attributes == null) {
332             return null;
333         } else {
334             return attributes.remove(key);
335         }
336     }
337 
338     /**
339      * Returns {@code true} if the specified argument is an {@code instanceof} {@code SimpleSession} and both
340      * {@link #getId() id}s are equal.  If the argument is a {@code SimpleSession} and either 'this' or the argument
341      * does not yet have an ID assigned, the value of {@link #onEquals(SimpleSession) onEquals} is returned, which
342      * does a necessary attribute-based comparison when IDs are not available.
343      * <p/>
344      * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to
345      * avoid the more expensive attributes-based comparison.
346      *
347      * @param obj the object to compare with this one for equality.
348      * @return {@code true} if this object is equivalent to the specified argument, {@code false} otherwise.
349      */
350     @Override
351     public boolean equals(Object obj) {
352         if (this == obj) {
353             return true;
354         }
355         if (obj instanceof SimpleSession) {
356             SimpleSession other = (SimpleSession) obj;
357             Serializable thisId = getId();
358             Serializable otherId = other.getId();
359             if (thisId != null && otherId != null) {
360                 return thisId.equals(otherId);
361             } else {
362                 //fall back to an attribute based comparison:
363                 return onEquals(other);
364             }
365         }
366         return false;
367     }
368 
369     /**
370      * Provides an attribute-based comparison (no ID comparison) - incurred <em>only</em> when 'this' or the
371      * session object being compared for equality do not have a session id.
372      *
373      * @param ss the SimpleSession instance to compare for equality.
374      * @return true if all the attributes, except the id, are equal to this object's attributes.
375      * @since 1.0
376      */
377     protected boolean onEquals(SimpleSession ss) {
378         return (getStartTimestamp() != null ? getStartTimestamp().equals(ss.getStartTimestamp()) : ss.getStartTimestamp() == null) &&
379                 (getStopTimestamp() != null ? getStopTimestamp().equals(ss.getStopTimestamp()) : ss.getStopTimestamp() == null) &&
380                 (getLastAccessTime() != null ? getLastAccessTime().equals(ss.getLastAccessTime()) : ss.getLastAccessTime() == null) &&
381                 (getTimeout() == ss.getTimeout()) &&
382                 (isExpired() == ss.isExpired()) &&
383                 (getHost() != null ? getHost().equals(ss.getHost()) : ss.getHost() == null) &&
384                 (getAttributes() != null ? getAttributes().equals(ss.getAttributes()) : ss.getAttributes() == null);
385     }
386 
387     /**
388      * Returns the hashCode.  If the {@link #getId() id} is not {@code null}, its hashcode is returned immediately.
389      * If it is {@code null}, an attributes-based hashCode will be calculated and returned.
390      * <p/>
391      * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to
392      * avoid the more expensive attributes-based calculation.
393      *
394      * @return this object's hashCode
395      * @since 1.0
396      */
397     @Override
398     public int hashCode() {
399         Serializable id = getId();
400         if (id != null) {
401             return id.hashCode();
402         }
403         int hashCode = getStartTimestamp() != null ? getStartTimestamp().hashCode() : 0;
404         hashCode = 31 * hashCode + (getStopTimestamp() != null ? getStopTimestamp().hashCode() : 0);
405         hashCode = 31 * hashCode + (getLastAccessTime() != null ? getLastAccessTime().hashCode() : 0);
406         hashCode = 31 * hashCode + Long.valueOf(Math.max(getTimeout(), 0)).hashCode();
407         hashCode = 31 * hashCode + Boolean.valueOf(isExpired()).hashCode();
408         hashCode = 31 * hashCode + (getHost() != null ? getHost().hashCode() : 0);
409         hashCode = 31 * hashCode + (getAttributes() != null ? getAttributes().hashCode() : 0);
410         return hashCode;
411     }
412 
413     /**
414      * Returns the string representation of this SimpleSession, equal to
415      * <code>getClass().getName() + &quot;,id=&quot; + getId()</code>.
416      *
417      * @return the string representation of this SimpleSession, equal to
418      *         <code>getClass().getName() + &quot;,id=&quot; + getId()</code>.
419      * @since 1.0
420      */
421     @Override
422     public String toString() {
423         StringBuilder sb = new StringBuilder();
424         sb.append(getClass().getName()).append(",id=").append(getId());
425         return sb.toString();
426     }
427 
428     /**
429      * Serializes this object to the specified output stream for JDK Serialization.
430      *
431      * @param out output stream used for Object serialization.
432      * @throws IOException if any of this object's fields cannot be written to the stream.
433      * @since 1.0
434      */
435     private void writeObject(ObjectOutputStream out) throws IOException {
436         out.defaultWriteObject();
437         short alteredFieldsBitMask = getAlteredFieldsBitMask();
438         out.writeShort(alteredFieldsBitMask);
439         if (id != null) {
440             out.writeObject(id);
441         }
442         if (startTimestamp != null) {
443             out.writeObject(startTimestamp);
444         }
445         if (stopTimestamp != null) {
446             out.writeObject(stopTimestamp);
447         }
448         if (lastAccessTime != null) {
449             out.writeObject(lastAccessTime);
450         }
451         if (timeout != 0l) {
452             out.writeLong(timeout);
453         }
454         if (expired) {
455             out.writeBoolean(expired);
456         }
457         if (host != null) {
458             out.writeUTF(host);
459         }
460         if (!CollectionUtils.isEmpty(attributes)) {
461             out.writeObject(attributes);
462         }
463     }
464 
465     /**
466      * Reconstitutes this object based on the specified InputStream for JDK Serialization.
467      *
468      * @param in the input stream to use for reading data to populate this object.
469      * @throws IOException            if the input stream cannot be used.
470      * @throws ClassNotFoundException if a required class needed for instantiation is not available in the present JVM
471      * @since 1.0
472      */
473     @SuppressWarnings({"unchecked"})
474     private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
475         in.defaultReadObject();
476         short bitMask = in.readShort();
477 
478         if (isFieldPresent(bitMask, ID_BIT_MASK)) {
479             this.id = (Serializable) in.readObject();
480         }
481         if (isFieldPresent(bitMask, START_TIMESTAMP_BIT_MASK)) {
482             this.startTimestamp = (Date) in.readObject();
483         }
484         if (isFieldPresent(bitMask, STOP_TIMESTAMP_BIT_MASK)) {
485             this.stopTimestamp = (Date) in.readObject();
486         }
487         if (isFieldPresent(bitMask, LAST_ACCESS_TIME_BIT_MASK)) {
488             this.lastAccessTime = (Date) in.readObject();
489         }
490         if (isFieldPresent(bitMask, TIMEOUT_BIT_MASK)) {
491             this.timeout = in.readLong();
492         }
493         if (isFieldPresent(bitMask, EXPIRED_BIT_MASK)) {
494             this.expired = in.readBoolean();
495         }
496         if (isFieldPresent(bitMask, HOST_BIT_MASK)) {
497             this.host = in.readUTF();
498         }
499         if (isFieldPresent(bitMask, ATTRIBUTES_BIT_MASK)) {
500             this.attributes = (Map<Object, Object>) in.readObject();
501         }
502     }
503 
504     /**
505      * Returns a bit mask used during serialization indicating which fields have been serialized. Fields that have been
506      * altered (not null and/or not retaining the class defaults) will be serialized and have 1 in their respective
507      * index, fields that are null and/or retain class default values have 0.
508      *
509      * @return a bit mask used during serialization indicating which fields have been serialized.
510      * @since 1.0
511      */
512     private short getAlteredFieldsBitMask() {
513         int bitMask = 0;
514         bitMask = id != null ? bitMask | ID_BIT_MASK : bitMask;
515         bitMask = startTimestamp != null ? bitMask | START_TIMESTAMP_BIT_MASK : bitMask;
516         bitMask = stopTimestamp != null ? bitMask | STOP_TIMESTAMP_BIT_MASK : bitMask;
517         bitMask = lastAccessTime != null ? bitMask | LAST_ACCESS_TIME_BIT_MASK : bitMask;
518         bitMask = timeout != 0l ? bitMask | TIMEOUT_BIT_MASK : bitMask;
519         bitMask = expired ? bitMask | EXPIRED_BIT_MASK : bitMask;
520         bitMask = host != null ? bitMask | HOST_BIT_MASK : bitMask;
521         bitMask = !CollectionUtils.isEmpty(attributes) ? bitMask | ATTRIBUTES_BIT_MASK : bitMask;
522         return (short) bitMask;
523     }
524 
525     /**
526      * Returns {@code true} if the given {@code bitMask} argument indicates that the specified field has been
527      * serialized and therefore should be read during deserialization, {@code false} otherwise.
528      *
529      * @param bitMask      the aggregate bitmask for all fields that have been serialized.  Individual bits represent
530      *                     the fields that have been serialized.  A bit set to 1 means that corresponding field has
531      *                     been serialized, 0 means it hasn't been serialized.
532      * @param fieldBitMask the field bit mask constant identifying which bit to inspect (corresponds to a class attribute).
533      * @return {@code true} if the given {@code bitMask} argument indicates that the specified field has been
534      *         serialized and therefore should be read during deserialization, {@code false} otherwise.
535      * @since 1.0
536      */
537     private static boolean isFieldPresent(short bitMask, int fieldBitMask) {
538         return (bitMask & fieldBitMask) != 0;
539     }
540 
541 }