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.session.ExpiredSessionException; 022import org.apache.shiro.session.InvalidSessionException; 023import org.apache.shiro.session.StoppedSessionException; 024import org.apache.shiro.util.CollectionUtils; 025import org.slf4j.Logger; 026import org.slf4j.LoggerFactory; 027 028import java.io.IOException; 029import java.io.ObjectInputStream; 030import java.io.ObjectOutputStream; 031import java.io.Serializable; 032import java.text.DateFormat; 033import 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 * @since 0.1 041 */ 042public class SimpleSession implements ValidatingSession, Serializable { 043 044 // Serialization reminder: 045 // You _MUST_ change this number if you introduce a change to this class 046 // that is NOT serialization backwards compatible. Serialization-compatible 047 // changes do not require a change to this number. If you need to generate 048 // a new number in this case, use the JDK's 'serialver' program to generate it. 049 private static final long serialVersionUID = -7125642695178165650L; 050 051 //TODO - complete JavaDoc 052 private transient static final Logger log = LoggerFactory.getLogger(SimpleSession.class); 053 054 protected static final long MILLIS_PER_SECOND = 1000; 055 protected static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND; 056 protected static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE; 057 058 //serialization bitmask fields. DO NOT CHANGE THE ORDER THEY ARE DECLARED! 059 static int bitIndexCounter = 0; 060 private static final int ID_BIT_MASK = 1 << bitIndexCounter++; 061 private static final int START_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++; 062 private static final int STOP_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++; 063 private static final int LAST_ACCESS_TIME_BIT_MASK = 1 << bitIndexCounter++; 064 private static final int TIMEOUT_BIT_MASK = 1 << bitIndexCounter++; 065 private static final int EXPIRED_BIT_MASK = 1 << bitIndexCounter++; 066 private static final int HOST_BIT_MASK = 1 << bitIndexCounter++; 067 private static final int ATTRIBUTES_BIT_MASK = 1 << bitIndexCounter++; 068 069 // ============================================================== 070 // NOTICE: 071 // 072 // The following fields are marked as transient to avoid double-serialization. 073 // They are in fact serialized (even though 'transient' usually indicates otherwise), 074 // but they are serialized explicitly via the writeObject and readObject implementations 075 // in this class. 076 // 077 // If we didn't declare them as transient, the out.defaultWriteObject(); call in writeObject would 078 // serialize all non-transient fields as well, effectively doubly serializing the fields (also 079 // doubling the serialization size). 080 // 081 // This finding, with discussion, was covered here: 082 // 083 // http://mail-archives.apache.org/mod_mbox/shiro-user/201109.mbox/%3C4E81BCBD.8060909@metaphysis.net%3E 084 // 085 // ============================================================== 086 private transient Serializable id; 087 private transient Date startTimestamp; 088 private transient Date stopTimestamp; 089 private transient Date lastAccessTime; 090 private transient long timeout; 091 private transient boolean expired; 092 private transient String host; 093 private transient Map<Object, Object> attributes; 094 095 public SimpleSession() { 096 this.timeout = DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT; //TODO - remove concrete reference to DefaultSessionManager 097 this.startTimestamp = new Date(); 098 this.lastAccessTime = this.startTimestamp; 099 } 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() + ",id=" + getId()</code>. 416 * 417 * @return the string representation of this SimpleSession, equal to 418 * <code>getClass().getName() + ",id=" + 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}