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