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 module hunt.shiro.session.mgt.SimpleSession; 20 21 import hunt.shiro.session.mgt.ValidatingSession; 22 import hunt.shiro.session.mgt.DefaultSessionManager; 23 import hunt.shiro.session.Session; 24 25 import hunt.shiro.Exceptions; 26 import hunt.shiro.util.CollectionUtils; 27 28 import hunt.collection; 29 import hunt.Exceptions; 30 import hunt.logging.Logger; 31 import hunt.util.StringBuilder; 32 import hunt.util.Common; 33 import hunt.util.DateTime; 34 35 import hunt.Long; 36 import std.array; 37 38 39 /** 40 * Simple {@link hunt.shiro.session.Session} JavaBeans-compatible POJO implementation, intended to be used on the 41 * business/server tier. 42 * 43 */ 44 class SimpleSession : ValidatingSession { 45 46 // Serialization reminder: 47 // You _MUST_ change this number if you introduce a change to this class 48 // that is NOT serialization backwards compatible. Serialization-compatible 49 // changes do not require a change to this number. If you need to generate 50 // a new number in this case, use the JDK's 'serialver' program to generate it. 51 52 protected enum long MILLIS_PER_SECOND = 1000; 53 protected enum long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND; 54 protected enum long MILLIS_PER_HOUR = 60 * MILLIS_PER_MINUTE; 55 56 //serialization bitmask fields. DO NOT CHANGE THE ORDER THEY ARE DECLARED! 57 enum int bitIndexCounter = 0; 58 private enum int ID_BIT_MASK = 1 << 0; 59 private enum int START_TIMESTAMP_BIT_MASK = 1 << 1; 60 private enum int STOP_TIMESTAMP_BIT_MASK = 1 << 2; 61 private enum int LAST_ACCESS_TIME_BIT_MASK = 1 << 3; 62 private enum int TIMEOUT_BIT_MASK = 1 << 4; 63 private enum int EXPIRED_BIT_MASK = 1 << 5; 64 private enum int HOST_BIT_MASK = 1 << 6; 65 private enum int ATTRIBUTES_BIT_MASK = 1 << 7; 66 67 // ============================================================== 68 // NOTICE: 69 // 70 // The following fields are marked as to avoid double-serialization. 71 // They are in fact serialized (even though '' usually indicates otherwise), 72 // but they are serialized explicitly via the writeObject and readObject implementations 73 // in this class. 74 // 75 // If we didn't declare them as , the out.defaultWriteObject(); call in writeObject would 76 // serialize all non- fields as well, effectively doubly serializing the fields (also 77 // doubling the serialization size). 78 // 79 // This finding, with discussion, was covered here: 80 // 81 // http://mail-archives.apache.org/mod_mbox/shiro-user/201109.mbox/%3C4E81BCBD.8060909@metaphysis.net%3E 82 // 83 // ============================================================== 84 private string id; 85 private Date startTimestamp; 86 private Long stopTimestamp; 87 private Date lastAccessTime; 88 private long timeout; 89 private bool expired; 90 private string host; 91 private Map!(Object, Object) attributes; 92 93 this() { 94 this.timeout = DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT; //TODO - remove concrete reference to DefaultSessionManager 95 this.startTimestamp = DateTime.currentTimeMillis(); 96 this.lastAccessTime = this.startTimestamp; 97 } 98 99 this(string host) { 100 this(); 101 this.host = host; 102 } 103 104 string getId() @trusted nothrow { 105 return this.id; 106 } 107 108 void setId(string id) @trusted nothrow { 109 this.id = id; 110 } 111 112 Date getStartTimestamp() { 113 return startTimestamp; 114 } 115 116 void setStartTimestamp(Date startTimestamp) { 117 this.startTimestamp = startTimestamp; 118 } 119 120 /** 121 * Returns the time the session was stopped, or <tt>null</tt> if the session is still active. 122 * <p/> 123 * A session may become stopped under a number of conditions: 124 * <ul> 125 * <li>If the user logs out of the system, their current session is terminated (released).</li> 126 * <li>If the session expires</li> 127 * <li>The application explicitly calls {@link #stop()}</li> 128 * <li>If there is an internal system error and the session state can no longer accurately 129 * reflect the user's behavior, such in the case of a system crash</li> 130 * </ul> 131 * <p/> 132 * Once stopped, a session may no longer be used. It is locked from all further activity. 133 * 134 * @return The time the session was stopped, or <tt>null</tt> if the session is still 135 * active. 136 */ 137 Long getStopTimestamp() { 138 return stopTimestamp; 139 } 140 141 void setStopTimestamp(Long stopTimestamp) { 142 this.stopTimestamp = stopTimestamp; 143 } 144 145 Date getLastAccessTime() { 146 return lastAccessTime; 147 } 148 149 void setLastAccessTime(Date lastAccessTime) { 150 this.lastAccessTime = lastAccessTime; 151 } 152 153 /** 154 * Returns true if this session has expired, false otherwise. If the session has 155 * expired, no further user interaction with the system may be done under this session. 156 * 157 * @return true if this session has expired, false otherwise. 158 */ 159 bool isExpired() { 160 return expired; 161 } 162 163 void setExpired(bool expired) { 164 this.expired = expired; 165 } 166 167 long getTimeout() { 168 return timeout; 169 } 170 171 void setTimeout(long timeout) { 172 this.timeout = timeout; 173 } 174 175 string getHost() { 176 return host; 177 } 178 179 void setHost(string host) { 180 this.host = host; 181 } 182 183 Map!(Object, Object) getAttributes() { 184 return attributes; 185 } 186 187 void setAttributes(Map!(Object, Object) attributes) { 188 this.attributes = attributes; 189 } 190 191 void touch() { 192 this.lastAccessTime = DateTime.currentTimeMillis(); 193 } 194 195 void stop() { 196 if (this.stopTimestamp is null) { 197 this.stopTimestamp = new Long(DateTime.currentTimeMillis()); 198 } 199 } 200 201 protected bool isStopped() { 202 return getStopTimestamp() !is null; 203 } 204 205 protected void expire() { 206 stop(); 207 this.expired = true; 208 } 209 210 /** 211 */ 212 bool isValid() { 213 return !isStopped() && !isExpired(); 214 } 215 216 /** 217 * Determines if this session is expired. 218 * 219 * @return true if the specified session has expired, false otherwise. 220 */ 221 protected bool isTimedOut() { 222 223 if (isExpired()) { 224 return true; 225 } 226 227 long timeout = getTimeout(); 228 229 if (timeout >= 0) { 230 231 Date lastAccessTime = getLastAccessTime(); 232 233 // if (lastAccessTime is null) { 234 // string msg = "session.lastAccessTime for session with id [" ~ 235 // getId() ~ "] is null. This value must be set at " ~ 236 // "least once, preferably at least upon instantiation. Please check the " ~ 237 // typeid(this).name ~ " implementation and ensure " ~ 238 // "this value will be set (perhaps in the constructor?)"; 239 // throw new IllegalStateException(msg); 240 // } 241 242 // Calculate at what time a session would have been last accessed 243 // for it to be expired at this point. In other words, subtract 244 // from the current time the amount of time that a session can 245 // be inactive before expiring. If the session was last accessed 246 // before this time, it is expired. 247 long expireTimeMillis = DateTime.currentTimeMillis() - timeout; 248 return lastAccessTime < expireTimeMillis; 249 } else { 250 version(HUNT_SHIRO_DEBUG) { 251 tracef("No timeout for session with id [" ~ getId() ~ 252 "]. Session is not considered expired."); 253 } 254 } 255 256 return false; 257 } 258 259 void validate(){ 260 //check for stopped: 261 if (isStopped()) { 262 //timestamp is set, so the session is considered stopped: 263 string msg = "Session with id [" ~ getId() ~ "] has been " ~ 264 "explicitly stopped. No further interaction under this session is " ~ 265 "allowed."; 266 throw new StoppedSessionException(msg); 267 } 268 269 //check for expiration 270 if (isTimedOut()) { 271 expire(); 272 273 //throw an exception explaining details of why it expired: 274 Date lastAccessTime = getLastAccessTime(); 275 long timeout = getTimeout(); 276 string sessionId = getId(); 277 278 // DateFormat df = DateFormat.getInstance(); 279 import std.conv; 280 string msg = "Session with id [" ~ sessionId ~ "] has expired. " ~ 281 "Last access time: " ~ lastAccessTime.to!string() ~ 282 ". Current time: " ~ DateTime.currentTimeMillis().to!string() ~ 283 ". Session timeout is set to " ~ to!string(timeout / MILLIS_PER_SECOND) ~ " seconds (" ~ 284 to!string(timeout / MILLIS_PER_MINUTE) ~ " minutes)"; 285 version(HUNT_DEBUG) { 286 warningf(msg); 287 } 288 throw new ExpiredSessionException(msg); 289 } 290 } 291 292 private Map!(Object, Object) getAttributesLazy() { 293 Map!(Object, Object) attributes = getAttributes(); 294 if (attributes is null) { 295 attributes = new HashMap!(Object, Object)(); 296 setAttributes(attributes); 297 } 298 return attributes; 299 } 300 301 Object[] getAttributeKeys(){ 302 Map!(Object, Object) attributes = getAttributes(); 303 if (attributes is null) { 304 return null; 305 } 306 return attributes.byKey.array; // .keySet(); 307 } 308 309 Object getAttribute(Object key) { 310 Map!(Object, Object) attributes = getAttributes(); 311 if (attributes is null) { 312 return null; 313 } 314 return attributes.get(key); 315 } 316 317 void setAttribute(Object key, Object value) { 318 if (value is null) { 319 removeAttribute(key); 320 } else { 321 getAttributesLazy().put(key, value); 322 } 323 } 324 325 Object removeAttribute(Object key) { 326 Map!(Object, Object) attributes = getAttributes(); 327 if (attributes is null) { 328 return null; 329 } else { 330 return attributes.remove(key); 331 } 332 } 333 334 /** 335 * Returns {@code true} if the specified argument is an {@code instanceof} {@code SimpleSession} and both 336 * {@link #getId() id}s are equal. If the argument is a {@code SimpleSession} and either 'this' or the argument 337 * does not yet have an ID assigned, the value of {@link #onEquals(SimpleSession) onEquals} is returned, which 338 * does a necessary attribute-based comparison when IDs are not available. 339 * <p/> 340 * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to 341 * avoid the more expensive attributes-based comparison. 342 * 343 * @param obj the object to compare with this one for equality. 344 * @return {@code true} if this object is equivalent to the specified argument, {@code false} otherwise. 345 */ 346 override 347 bool opEquals(Object obj) { 348 if (this == obj) { 349 return true; 350 } 351 352 SimpleSession other = cast(SimpleSession) obj; 353 if (other !is null) { 354 string thisId = getId(); 355 string otherId = other.getId(); 356 if (thisId !is null && otherId !is null) { 357 return thisId == otherId; 358 } else { 359 //fall back to an attribute based comparison: 360 return onEquals(other); 361 } 362 } 363 return false; 364 } 365 366 /** 367 * Provides an attribute-based comparison (no ID comparison) - incurred <em>only</em> when 'this' or the 368 * session object being compared for equality do not have a session id. 369 * 370 * @param ss the SimpleSession instance to compare for equality. 371 * @return true if all the attributes, except the id, are equal to this object's attributes. 372 */ 373 protected bool onEquals(SimpleSession ss) { 374 implementationMissing(false); 375 return true; 376 // return (getStartTimestamp() !is null ? getStartTimestamp()== ss.getStartTimestamp() : ss.getStartTimestamp() is null) && 377 // (getStopTimestamp() !is null ? getStopTimestamp()== ss.getStopTimestamp() : ss.getStopTimestamp() is null) && 378 // (getLastAccessTime() !is null ? getLastAccessTime()== ss.getLastAccessTime() : ss.getLastAccessTime() is null) && 379 // (getTimeout() == ss.getTimeout()) && 380 // (isExpired() == ss.isExpired()) && 381 // (getHost() !is null ? getHost()== ss.getHost() : ss.getHost() is null) && 382 // (getAttributes() !is null ? getAttributes()== ss.getAttributes() : ss.getAttributes() is null); 383 } 384 385 /** 386 * Returns the hashCode. If the {@link #getId() id} is not {@code null}, its hashcode is returned immediately. 387 * If it is {@code null}, an attributes-based hashCode will be calculated and returned. 388 * <p/> 389 * Do your best to ensure {@code SimpleSession} instances receive an ID very early in their lifecycle to 390 * avoid the more expensive attributes-based calculation. 391 * 392 * @return this object's hashCode 393 */ 394 override 395 size_t toHash() @trusted nothrow { 396 string id = getId(); 397 if (id !is null) { 398 return hashOf(id); 399 } 400 size_t hashCode; 401 try { 402 implementationMissing(false); 403 } catch(Exception) { 404 405 } 406 // size_t hashCode = getStartTimestamp() is null ? getStartTimestamp().hashCode() : 0; 407 // hashCode = 31 * hashCode + (getStopTimestamp() !is null ? getStopTimestamp().hashCode() : 0); 408 // hashCode = 31 * hashCode + (getLastAccessTime() !is null ? getLastAccessTime().hashCode() : 0); 409 // hashCode = 31 * hashCode + Long.valueOf(max(getTimeout(), 0)).hashCode(); 410 // hashCode = 31 * hashCode + bool.valueOf(isExpired()).hashCode(); 411 // hashCode = 31 * hashCode + (getHost() !is null ? getHost().hashCode() : 0); 412 // hashCode = 31 * hashCode + (getAttributes() !is null ? getAttributes().hashCode() : 0); 413 return hashCode; 414 } 415 416 /** 417 * Returns the string representation of this SimpleSession, equal to 418 * <typeid(code).name + ",id=" + getId()</code>. 419 * 420 * @return the string representation of this SimpleSession, equal to 421 * <typeid(code).name + ",id=" + getId()</code>. 422 */ 423 override 424 string toString() { 425 StringBuilder sb = new StringBuilder(); 426 sb.append(typeid(this).name).append(", id=").append(id); 427 return sb.toString(); 428 } 429 430 /** 431 * Returns {@code true} if the given {@code bitMask} argument indicates that the specified field has been 432 * serialized and therefore should be read during deserialization, {@code false} otherwise. 433 * 434 * @param bitMask the aggregate bitmask for all fields that have been serialized. Individual bits represent 435 * the fields that have been serialized. A bit set to 1 means that corresponding field has 436 * been serialized, 0 means it hasn't been serialized. 437 * @param fieldBitMask the field bit mask constant identifying which bit to inspect (corresponds to a class attribute). 438 * @return {@code true} if the given {@code bitMask} argument indicates that the specified field has been 439 * serialized and therefore should be read during deserialization, {@code false} otherwise. 440 */ 441 private static bool isFieldPresent(short bitMask, int fieldBitMask) { 442 return (bitMask & fieldBitMask) != 0; 443 } 444 445 }