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 */
019 package org.apache.shiro.session.mgt;
020
021 import org.apache.shiro.session.ExpiredSessionException;
022 import org.apache.shiro.session.InvalidSessionException;
023 import org.apache.shiro.session.StoppedSessionException;
024 import org.apache.shiro.util.CollectionUtils;
025 import org.slf4j.Logger;
026 import org.slf4j.LoggerFactory;
027
028 import java.io.IOException;
029 import java.io.ObjectInputStream;
030 import java.io.ObjectOutputStream;
031 import java.io.Serializable;
032 import java.text.DateFormat;
033 import java.util.*;
034
035
036 /**
037 * Simple {@link org.apache.shiro.session.Session} JavaBeans-compatible POJO implementation, intended to be used on the
038 * business/server tier.
039 *
040 * @author Les Hazlewood
041 * @since 0.1
042 */
043 public class SimpleSession implements ValidatingSession, Serializable {
044
045 // Serialization reminder:
046 // You _MUST_ change this number if you introduce a change to this class
047 // that is NOT serialization backwards compatible. Serialization-compatible
048 // changes do not require a change to this number. If you need to generate
049 // a new number in this case, use the JDK's 'serialver' program to generate it.
050 private static final long serialVersionUID = -7125642695178165650L;
051
052 //TODO - complete JavaDoc
053 private transient static final Logger log = LoggerFactory.getLogger(SimpleSession.class);
054
055 protected static final long MILLIS_PER_SECOND = 1000;
056 protected static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND;
057 protected static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE;
058
059 //serialization bitmask fields. DO NOT CHANGE THE ORDER THEY ARE DECLARED!
060 static int bitIndexCounter = 0;
061 private static final int ID_BIT_MASK = 1 << bitIndexCounter++;
062 private static final int START_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++;
063 private static final int STOP_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++;
064 private static final int LAST_ACCESS_TIME_BIT_MASK = 1 << bitIndexCounter++;
065 private static final int TIMEOUT_BIT_MASK = 1 << bitIndexCounter++;
066 private static final int EXPIRED_BIT_MASK = 1 << bitIndexCounter++;
067 private static final int HOST_BIT_MASK = 1 << bitIndexCounter++;
068 private static final int ATTRIBUTES_BIT_MASK = 1 << bitIndexCounter++;
069
070 private Serializable id;
071 private Date startTimestamp;
072 private Date stopTimestamp;
073 private Date lastAccessTime;
074 private long timeout;
075 private boolean expired;
076 private String host;
077
078 private Map<Object, Object> attributes;
079
080 public SimpleSession() {
081 this.timeout = DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT; //TODO - remove concrete reference to DefaultSessionManager
082 this.startTimestamp = new Date();
083 this.lastAccessTime = this.startTimestamp;
084 }
085
086 public SimpleSession(String host) {
087 this();
088 this.host = host;
089 }
090
091 public Serializable getId() {
092 return this.id;
093 }
094
095 public void setId(Serializable id) {
096 this.id = id;
097 }
098
099 public Date getStartTimestamp() {
100 return startTimestamp;
101 }
102
103 public void setStartTimestamp(Date startTimestamp) {
104 this.startTimestamp = startTimestamp;
105 }
106
107 /**
108 * Returns the time the session was stopped, or <tt>null</tt> if the session is still active.
109 * <p/>
110 * A session may become stopped under a number of conditions:
111 * <ul>
112 * <li>If the user logs out of the system, their current session is terminated (released).</li>
113 * <li>If the session expires</li>
114 * <li>The application explicitly calls {@link #stop()}</li>
115 * <li>If there is an internal system error and the session state can no longer accurately
116 * reflect the user's behavior, such in the case of a system crash</li>
117 * </ul>
118 * <p/>
119 * Once stopped, a session may no longer be used. It is locked from all further activity.
120 *
121 * @return The time the session was stopped, or <tt>null</tt> if the session is still
122 * active.
123 */
124 public Date getStopTimestamp() {
125 return stopTimestamp;
126 }
127
128 public void setStopTimestamp(Date stopTimestamp) {
129 this.stopTimestamp = stopTimestamp;
130 }
131
132 public Date getLastAccessTime() {
133 return lastAccessTime;
134 }
135
136 public void setLastAccessTime(Date lastAccessTime) {
137 this.lastAccessTime = lastAccessTime;
138 }
139
140 /**
141 * Returns true if this session has expired, false otherwise. If the session has
142 * expired, no further user interaction with the system may be done under this session.
143 *
144 * @return true if this session has expired, false otherwise.
145 */
146 public boolean isExpired() {
147 return expired;
148 }
149
150 public void setExpired(boolean expired) {
151 this.expired = expired;
152 }
153
154 public long getTimeout() {
155 return timeout;
156 }
157
158 public void setTimeout(long timeout) {
159 this.timeout = timeout;
160 }
161
162 public String getHost() {
163 return host;
164 }
165
166 public void setHost(String host) {
167 this.host = host;
168 }
169
170 public Map<Object, Object> getAttributes() {
171 return attributes;
172 }
173
174 public void setAttributes(Map<Object, Object> attributes) {
175 this.attributes = attributes;
176 }
177
178 public void touch() {
179 this.lastAccessTime = new Date();
180 }
181
182 public void stop() {
183 if (this.stopTimestamp == null) {
184 this.stopTimestamp = new Date();
185 }
186 }
187
188 protected boolean isStopped() {
189 return getStopTimestamp() != null;
190 }
191
192 protected void expire() {
193 stop();
194 this.expired = true;
195 }
196
197 /**
198 * @since 0.9
199 */
200 public boolean isValid() {
201 return !isStopped() && !isExpired();
202 }
203
204 /**
205 * Determines if this session is expired.
206 *
207 * @return true if the specified session has expired, false otherwise.
208 */
209 protected boolean isTimedOut() {
210
211 if (isExpired()) {
212 return true;
213 }
214
215 long timeout = getTimeout();
216
217 if (timeout >= 0l) {
218
219 Date lastAccessTime = getLastAccessTime();
220
221 if (lastAccessTime == null) {
222 String msg = "session.lastAccessTime for session with id [" +
223 getId() + "] is null. This value must be set at " +
224 "least once, preferably at least upon instantiation. Please check the " +
225 getClass().getName() + " implementation and ensure " +
226 "this value will be set (perhaps in the constructor?)";
227 throw new IllegalStateException(msg);
228 }
229
230 // Calculate at what time a session would have been last accessed
231 // for it to be expired at this point. In other words, subtract
232 // from the current time the amount of time that a session can
233 // be inactive before expiring. If the session was last accessed
234 // before this time, it is expired.
235 long expireTimeMillis = System.currentTimeMillis() - timeout;
236 Date expireTime = new Date(expireTimeMillis);
237 return lastAccessTime.before(expireTime);
238 } else {
239 if (log.isTraceEnabled()) {
240 log.trace("No timeout for session with id [" + getId() +
241 "]. Session is not considered expired.");
242 }
243 }
244
245 return false;
246 }
247
248 public void validate() throws InvalidSessionException {
249 //check for stopped:
250 if (isStopped()) {
251 //timestamp is set, so the session is considered stopped:
252 String msg = "Session with id [" + getId() + "] has been " +
253 "explicitly stopped. No further interaction under this session is " +
254 "allowed.";
255 throw new StoppedSessionException(msg);
256 }
257
258 //check for expiration
259 if (isTimedOut()) {
260 expire();
261
262 //throw an exception explaining details of why it expired:
263 Date lastAccessTime = getLastAccessTime();
264 long timeout = getTimeout();
265
266 Serializable sessionId = getId();
267
268 DateFormat df = DateFormat.getInstance();
269 String msg = "Session with id [" + sessionId + "] has expired. " +
270 "Last access time: " + df.format(lastAccessTime) +
271 ". Current time: " + df.format(new Date()) +
272 ". Session timeout is set to " + timeout / MILLIS_PER_SECOND + " seconds (" +
273 timeout / MILLIS_PER_MINUTE + " minutes)";
274 if (log.isTraceEnabled()) {
275 log.trace(msg);
276 }
277 throw new ExpiredSessionException(msg);
278 }
279 }
280
281 private Map<Object, Object> getAttributesLazy() {
282 Map<Object, Object> attributes = getAttributes();
283 if (attributes == null) {
284 attributes = new HashMap<Object, Object>();
285 setAttributes(attributes);
286 }
287 return attributes;
288 }
289
290 public Collection<Object> getAttributeKeys() throws InvalidSessionException {
291 Map<Object, Object> attributes = getAttributes();
292 if (attributes == null) {
293 return Collections.emptySet();
294 }
295 return attributes.keySet();
296 }
297
298 public Object getAttribute(Object key) {
299 Map<Object, Object> attributes = getAttributes();
300 if (attributes == null) {
301 return null;
302 }
303 return attributes.get(key);
304 }
305
306 public void setAttribute(Object key, Object value) {
307 if (value == null) {
308 removeAttribute(key);
309 } else {
310 getAttributesLazy().put(key, value);
311 }
312 }
313
314 public Object removeAttribute(Object key) {
315 Map<Object, Object> attributes = getAttributes();
316 if (attributes == null) {
317 return null;
318 } else {
319 return attributes.remove(key);
320 }
321 }
322
323 /**
324 * Returns {@code true} if the specified argument is an {@code instanceof} {@code SimpleSession} and both
325 * {@link #getId() id}s are equal. If the argument is a {@code SimpleSession} and either 'this' or the argument
326 * does not yet have an ID assigned, the value of {@link #onEquals(SimpleSession) onEquals} is returned, which
327 * does a necessary attribute-based comparison when IDs are not available.
328 * <p/>
329 * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to
330 * avoid the more expensive attributes-based comparison.
331 *
332 * @param obj the object to compare with this one for equality.
333 * @return {@code true} if this object is equivalent to the specified argument, {@code false} otherwise.
334 */
335 @Override
336 public boolean equals(Object obj) {
337 if (this == obj) {
338 return true;
339 }
340 if (obj instanceof SimpleSession) {
341 SimpleSession other = (SimpleSession) obj;
342 Serializable thisId = getId();
343 Serializable otherId = other.getId();
344 if (thisId != null && otherId != null) {
345 return thisId.equals(otherId);
346 } else {
347 //fall back to an attribute based comparison:
348 return onEquals(other);
349 }
350 }
351 return false;
352 }
353
354 /**
355 * Provides an attribute-based comparison (no ID comparison) - incurred <em>only</em> when 'this' or the
356 * session object being compared for equality do not have a session id.
357 *
358 * @param ss the SimpleSession instance to compare for equality.
359 * @return true if all the attributes, except the id, are equal to this object's attributes.
360 * @since 1.0
361 */
362 protected boolean onEquals(SimpleSession ss) {
363 return (getStartTimestamp() != null ? getStartTimestamp().equals(ss.getStartTimestamp()) : ss.getStartTimestamp() == null) &&
364 (getStopTimestamp() != null ? getStopTimestamp().equals(ss.getStopTimestamp()) : ss.getStopTimestamp() == null) &&
365 (getLastAccessTime() != null ? getLastAccessTime().equals(ss.getLastAccessTime()) : ss.getLastAccessTime() == null) &&
366 (getTimeout() == ss.getTimeout()) &&
367 (isExpired() == ss.isExpired()) &&
368 (getHost() != null ? getHost().equals(ss.getHost()) : ss.getHost() == null) &&
369 (getAttributes() != null ? getAttributes().equals(ss.getAttributes()) : ss.getAttributes() == null);
370 }
371
372 /**
373 * Returns the hashCode. If the {@link #getId() id} is not {@code null}, its hashcode is returned immediately.
374 * If it is {@code null}, an attributes-based hashCode will be calculated and returned.
375 * <p/>
376 * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to
377 * avoid the more expensive attributes-based calculation.
378 *
379 * @return this object's hashCode
380 * @since 1.0
381 */
382 @Override
383 public int hashCode() {
384 Serializable id = getId();
385 if (id != null) {
386 return id.hashCode();
387 }
388 int hashCode = getStartTimestamp() != null ? getStartTimestamp().hashCode() : 0;
389 hashCode = 31 * hashCode + (getStopTimestamp() != null ? getStopTimestamp().hashCode() : 0);
390 hashCode = 31 * hashCode + (getLastAccessTime() != null ? getLastAccessTime().hashCode() : 0);
391 hashCode = 31 * hashCode + Long.valueOf(Math.max(getTimeout(), 0)).hashCode();
392 hashCode = 31 * hashCode + Boolean.valueOf(isExpired()).hashCode();
393 hashCode = 31 * hashCode + (getHost() != null ? getHost().hashCode() : 0);
394 hashCode = 31 * hashCode + (getAttributes() != null ? getAttributes().hashCode() : 0);
395 return hashCode;
396 }
397
398 /**
399 * Returns the string representation of this SimpleSession, equal to
400 * <code>getClass().getName() + ",id=" + getId()</code>.
401 *
402 * @return the string representation of this SimpleSession, equal to
403 * <code>getClass().getName() + ",id=" + getId()</code>.
404 * @since 1.0
405 */
406 @Override
407 public String toString() {
408 StringBuilder sb = new StringBuilder();
409 sb.append(getClass().getName()).append(",id=").append(getId());
410 return sb.toString();
411 }
412
413 /**
414 * Serializes this object to the specified output stream for JDK Serialization.
415 *
416 * @param out output stream used for Object serialization.
417 * @throws IOException if any of this object's fields cannot be written to the stream.
418 * @since 1.0
419 */
420 private void writeObject(ObjectOutputStream out) throws IOException {
421 out.defaultWriteObject();
422 short alteredFieldsBitMask = getAlteredFieldsBitMask();
423 out.writeShort(alteredFieldsBitMask);
424 if (id != null) {
425 out.writeObject(id);
426 }
427 if (startTimestamp != null) {
428 out.writeObject(startTimestamp);
429 }
430 if (stopTimestamp != null) {
431 out.writeObject(stopTimestamp);
432 }
433 if (lastAccessTime != null) {
434 out.writeObject(lastAccessTime);
435 }
436 if (timeout != 0l) {
437 out.writeLong(timeout);
438 }
439 if (expired) {
440 out.writeBoolean(expired);
441 }
442 if (host != null) {
443 out.writeUTF(host);
444 }
445 if (!CollectionUtils.isEmpty(attributes)) {
446 out.writeObject(attributes);
447 }
448 }
449
450 /**
451 * Reconstitutes this object based on the specified InputStream for JDK Serialization.
452 *
453 * @param in the input stream to use for reading data to populate this object.
454 * @throws IOException if the input stream cannot be used.
455 * @throws ClassNotFoundException if a required class needed for instantiation is not available in the present JVM
456 * @since 1.0
457 */
458 @SuppressWarnings({"unchecked"})
459 private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException {
460 in.defaultReadObject();
461 short bitMask = in.readShort();
462
463 if (isFieldPresent(bitMask, ID_BIT_MASK)) {
464 this.id = (Serializable) in.readObject();
465 }
466 if (isFieldPresent(bitMask, START_TIMESTAMP_BIT_MASK)) {
467 this.startTimestamp = (Date) in.readObject();
468 }
469 if (isFieldPresent(bitMask, STOP_TIMESTAMP_BIT_MASK)) {
470 this.stopTimestamp = (Date) in.readObject();
471 }
472 if (isFieldPresent(bitMask, LAST_ACCESS_TIME_BIT_MASK)) {
473 this.lastAccessTime = (Date) in.readObject();
474 }
475 if (isFieldPresent(bitMask, TIMEOUT_BIT_MASK)) {
476 this.timeout = in.readLong();
477 }
478 if (isFieldPresent(bitMask, EXPIRED_BIT_MASK)) {
479 this.expired = in.readBoolean();
480 }
481 if (isFieldPresent(bitMask, HOST_BIT_MASK)) {
482 this.host = in.readUTF();
483 }
484 if (isFieldPresent(bitMask, ATTRIBUTES_BIT_MASK)) {
485 this.attributes = (Map<Object, Object>) in.readObject();
486 }
487 }
488
489 /**
490 * Returns a bit mask used during serialization indicating which fields have been serialized. Fields that have been
491 * altered (not null and/or not retaining the class defaults) will be serialized and have 1 in their respective
492 * index, fields that are null and/or retain class default values have 0.
493 *
494 * @return a bit mask used during serialization indicating which fields have been serialized.
495 * @since 1.0
496 */
497 private short getAlteredFieldsBitMask() {
498 int bitMask = 0;
499 bitMask = id != null ? bitMask | ID_BIT_MASK : bitMask;
500 bitMask = startTimestamp != null ? bitMask | START_TIMESTAMP_BIT_MASK : bitMask;
501 bitMask = stopTimestamp != null ? bitMask | STOP_TIMESTAMP_BIT_MASK : bitMask;
502 bitMask = lastAccessTime != null ? bitMask | LAST_ACCESS_TIME_BIT_MASK : bitMask;
503 bitMask = timeout != 0l ? bitMask | TIMEOUT_BIT_MASK : bitMask;
504 bitMask = !expired ? bitMask | EXPIRED_BIT_MASK : bitMask;
505 bitMask = host != null ? bitMask | HOST_BIT_MASK : bitMask;
506 bitMask = !CollectionUtils.isEmpty(attributes) ? bitMask | ATTRIBUTES_BIT_MASK : bitMask;
507 return (short) bitMask;
508 }
509
510 /**
511 * Returns {@code true} if the given {@code bitMask} argument indicates that the specified field has been
512 * serialized and therefore should be read during deserialization, {@code false} otherwise.
513 *
514 * @param bitMask the aggregate bitmask for all fields that have been serialized. Individual bits represent
515 * the fields that have been serialized. A bit set to 1 means that corresponding field has
516 * been serialized, 0 means it hasn't been serialized.
517 * @param fieldBitMask the field bit mask constant identifying which bit to inspect (corresponds to a class attribute).
518 * @return {@code true} if the given {@code bitMask} argument indicates that the specified field has been
519 * serialized and therefore should be read during deserialization, {@code false} otherwise.
520 * @since 1.0
521 */
522 private static boolean isFieldPresent(short bitMask, int fieldBitMask) {
523 return (bitMask & fieldBitMask) != 0;
524 }
525
526 }