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.Collection; 034import java.util.Collections; 035import java.util.Date; 036import java.util.HashMap; 037import java.util.Map; 038 039 040/** 041 * Simple {@link org.apache.shiro.session.Session} JavaBeans-compatible POJO implementation, intended to be used on the 042 * business/server tier. 043 * 044 * @since 0.1 045 */ 046@SuppressWarnings("checkstyle:MethodCount") 047public class SimpleSession implements ValidatingSession, Serializable { 048 049 protected static final long MILLIS_PER_SECOND = 1000; 050 protected static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND; 051 protected static final long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE; 052 053 //serialization bitmask fields. DO NOT CHANGE THE ORDER THEY ARE DECLARED! 054 static int bitIndexCounter; 055 056 // Serialization reminder: 057 // You _MUST_ change this number if you introduce a change to this class 058 // that is NOT serialization backwards compatible. Serialization-compatible 059 // changes do not require a change to this number. If you need to generate 060 // a new number in this case, use the JDK's 'serialver' program to generate it. 061 private static final long serialVersionUID = -7125642695178165650L; 062 063 private static final Logger LOGGER = LoggerFactory.getLogger(SimpleSession.class); 064 private static final int ID_BIT_MASK = 1 << bitIndexCounter++; 065 private static final int START_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++; 066 private static final int STOP_TIMESTAMP_BIT_MASK = 1 << bitIndexCounter++; 067 private static final int LAST_ACCESS_TIME_BIT_MASK = 1 << bitIndexCounter++; 068 private static final int TIMEOUT_BIT_MASK = 1 << bitIndexCounter++; 069 private static final int EXPIRED_BIT_MASK = 1 << bitIndexCounter++; 070 private static final int HOST_BIT_MASK = 1 << bitIndexCounter++; 071 private static final int ATTRIBUTES_BIT_MASK = 1 << bitIndexCounter++; 072 073 // ============================================================== 074 // NOTICE: 075 // 076 // The following fields are marked as transient to avoid double-serialization. 077 // They are in fact serialized (even though 'transient' usually indicates otherwise), 078 // but they are serialized explicitly via the writeObject and readObject implementations 079 // in this class. 080 // 081 // If we didn't declare them as transient, the out.defaultWriteObject(); call in writeObject would 082 // serialize all non-transient fields as well, effectively doubly serializing the fields (also 083 // doubling the serialization size). 084 // 085 // This finding, with discussion, was covered here: 086 // 087 // http://mail-archives.apache.org/mod_mbox/shiro-user/201109.mbox/%3C4E81BCBD.8060909@metaphysis.net%3E 088 // 089 // ============================================================== 090 private transient Serializable id; 091 private transient Date startTimestamp; 092 private transient Date stopTimestamp; 093 private transient Date lastAccessTime; 094 private transient long timeout; 095 private transient boolean expired; 096 private transient String host; 097 private transient Map<Object, Object> attributes; 098 099 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() + ",id=" + getId()</code>. 423 * 424 * @return the string representation of this SimpleSession, equal to 425 * <code>getClass().getName() + ",id=" + 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}