From a5258fef165859be579d1f265b0646fa376afe9d Mon Sep 17 00:00:00 2001 From: lprimak Date: Wed, 20 May 2026 23:49:21 -0500 Subject: [PATCH 1/8] bugfix: using atomics for session updates --- .../shiro/session/mgt/SimpleSession.java | 81 ++++++++++--------- 1 file changed, 41 insertions(+), 40 deletions(-) diff --git a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java index fbe8df02a1..939f3d1375 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java @@ -34,8 +34,11 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; -import java.util.HashMap; import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; /** @@ -91,18 +94,19 @@ public class SimpleSession implements ValidatingSession, Serializable { // ============================================================== private transient Serializable id; private transient Date startTimestamp; - private transient Date stopTimestamp; - private transient Date lastAccessTime; - private transient long timeout; - private transient boolean expired; + private transient AtomicReference stopTimestamp; + private transient AtomicReference lastAccessTime; + private transient AtomicLong timeout; + private transient AtomicBoolean expired = new AtomicBoolean(); private transient String host; private transient Map attributes; public SimpleSession() { //TODO - remove concrete reference to DefaultSessionManager - this.timeout = DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT; + this.timeout = new AtomicLong(DefaultSessionManager.DEFAULT_GLOBAL_SESSION_TIMEOUT); this.startTimestamp = new Date(); - this.lastAccessTime = this.startTimestamp; + this.stopTimestamp = new AtomicReference<>(); + this.lastAccessTime = new AtomicReference<>(this.startTimestamp); } public SimpleSession(String host) { @@ -144,19 +148,19 @@ public void setStartTimestamp(Date startTimestamp) { * active. */ public Date getStopTimestamp() { - return stopTimestamp; + return stopTimestamp.get(); } public void setStopTimestamp(Date stopTimestamp) { - this.stopTimestamp = stopTimestamp; + this.stopTimestamp.set(stopTimestamp); } public Date getLastAccessTime() { - return lastAccessTime; + return lastAccessTime.get(); } public void setLastAccessTime(Date lastAccessTime) { - this.lastAccessTime = lastAccessTime; + this.lastAccessTime.set(lastAccessTime); } /** @@ -166,19 +170,19 @@ public void setLastAccessTime(Date lastAccessTime) { * @return true if this session has expired, false otherwise. */ public boolean isExpired() { - return expired; + return expired.get(); } public void setExpired(boolean expired) { - this.expired = expired; + this.expired.set(expired); } public long getTimeout() { - return timeout; + return timeout.get(); } public void setTimeout(long timeout) { - this.timeout = timeout; + this.timeout.set(timeout); } public String getHost() { @@ -194,17 +198,15 @@ public Map getAttributes() { } public void setAttributes(Map attributes) { - this.attributes = attributes; + this.attributes = new ConcurrentHashMap<>(attributes); } public void touch() { - this.lastAccessTime = new Date(); + this.lastAccessTime.set(new Date()); } public void stop() { - if (this.stopTimestamp == null) { - this.stopTimestamp = new Date(); - } + stopTimestamp.compareAndSet(null, new Date()); } protected boolean isStopped() { @@ -213,7 +215,7 @@ protected boolean isStopped() { protected void expire() { stop(); - this.expired = true; + this.expired.set(true); } /** @@ -303,10 +305,9 @@ public void validate() throws InvalidSessionException { private Map getAttributesLazy() { Map attributes = getAttributes(); if (attributes == null) { - attributes = new HashMap(); - setAttributes(attributes); + this.attributes = new ConcurrentHashMap<>(); } - return attributes; + return this.attributes; } public Collection getAttributeKeys() throws InvalidSessionException { @@ -451,17 +452,17 @@ private void writeObject(ObjectOutputStream out) throws IOException { if (startTimestamp != null) { out.writeObject(startTimestamp); } - if (stopTimestamp != null) { - out.writeObject(stopTimestamp); + if (stopTimestamp.get() != null) { + out.writeObject(stopTimestamp.get()); } - if (lastAccessTime != null) { - out.writeObject(lastAccessTime); + if (lastAccessTime.get() != null) { + out.writeObject(lastAccessTime.get()); } - if (timeout != 0L) { - out.writeLong(timeout); + if (timeout.get() != 0L) { + out.writeLong(timeout.get()); } - if (expired) { - out.writeBoolean(expired); + if (expired.get()) { + out.writeBoolean(expired.get()); } if (host != null) { out.writeUTF(host); @@ -491,16 +492,16 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE this.startTimestamp = (Date) in.readObject(); } if (isFieldPresent(bitMask, STOP_TIMESTAMP_BIT_MASK)) { - this.stopTimestamp = (Date) in.readObject(); + this.stopTimestamp = new AtomicReference<>((Date) in.readObject()); } if (isFieldPresent(bitMask, LAST_ACCESS_TIME_BIT_MASK)) { - this.lastAccessTime = (Date) in.readObject(); + this.lastAccessTime = new AtomicReference<>((Date) in.readObject()); } if (isFieldPresent(bitMask, TIMEOUT_BIT_MASK)) { - this.timeout = in.readLong(); + this.timeout = new AtomicLong(in.readLong()); } if (isFieldPresent(bitMask, EXPIRED_BIT_MASK)) { - this.expired = in.readBoolean(); + this.expired = new AtomicBoolean(in.readBoolean()); } if (isFieldPresent(bitMask, HOST_BIT_MASK)) { this.host = in.readUTF(); @@ -523,10 +524,10 @@ private short getAlteredFieldsBitMask() { int bitMask = 0; bitMask = id != null ? bitMask | ID_BIT_MASK : bitMask; bitMask = startTimestamp != null ? bitMask | START_TIMESTAMP_BIT_MASK : bitMask; - bitMask = stopTimestamp != null ? bitMask | STOP_TIMESTAMP_BIT_MASK : bitMask; - bitMask = lastAccessTime != null ? bitMask | LAST_ACCESS_TIME_BIT_MASK : bitMask; - bitMask = timeout != 0L ? bitMask | TIMEOUT_BIT_MASK : bitMask; - bitMask = expired ? bitMask | EXPIRED_BIT_MASK : bitMask; + bitMask = stopTimestamp.get() != null ? bitMask | STOP_TIMESTAMP_BIT_MASK : bitMask; + bitMask = lastAccessTime.get() != null ? bitMask | LAST_ACCESS_TIME_BIT_MASK : bitMask; + bitMask = timeout.get() != 0L ? bitMask | TIMEOUT_BIT_MASK : bitMask; + bitMask = expired.get() ? bitMask | EXPIRED_BIT_MASK : bitMask; bitMask = host != null ? bitMask | HOST_BIT_MASK : bitMask; bitMask = !CollectionUtils.isEmpty(attributes) ? bitMask | ATTRIBUTES_BIT_MASK : bitMask; return (short) bitMask; From 5b3ad3061be048574e55628f26c1300ebfc832ed Mon Sep 17 00:00:00 2001 From: lprimak Date: Wed, 20 May 2026 23:58:55 -0500 Subject: [PATCH 2/8] simplify getAttributesLazy() --- .../java/org/apache/shiro/session/mgt/SimpleSession.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java index 939f3d1375..bc62a20902 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java @@ -303,11 +303,10 @@ public void validate() throws InvalidSessionException { } private Map getAttributesLazy() { - Map attributes = getAttributes(); if (attributes == null) { - this.attributes = new ConcurrentHashMap<>(); + attributes = new ConcurrentHashMap<>(); } - return this.attributes; + return attributes; } public Collection getAttributeKeys() throws InvalidSessionException { From 991de8c478460d1c439f877df4f113055f2ad1d6 Mon Sep 17 00:00:00 2001 From: lprimak Date: Sat, 23 May 2026 22:34:20 -0500 Subject: [PATCH 3/8] enh: code review comments --- .../shiro/session/mgt/SimpleSession.java | 25 +++++++++++++++---- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java index bc62a20902..f7c5c9ba76 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java @@ -99,7 +99,7 @@ public class SimpleSession implements ValidatingSession, Serializable { private transient AtomicLong timeout; private transient AtomicBoolean expired = new AtomicBoolean(); private transient String host; - private transient Map attributes; + private transient volatile Map attributes; public SimpleSession() { //TODO - remove concrete reference to DefaultSessionManager @@ -198,7 +198,7 @@ public Map getAttributes() { } public void setAttributes(Map attributes) { - this.attributes = new ConcurrentHashMap<>(attributes); + this.attributes = attributes == null ? null : new ConcurrentHashMap<>(attributes); } public void touch() { @@ -303,10 +303,17 @@ public void validate() throws InvalidSessionException { } private Map getAttributesLazy() { - if (attributes == null) { - attributes = new ConcurrentHashMap<>(); + Map local = attributes; + if (local == null) { + synchronized (this) { + local = attributes; + if (local == null) { + local = new ConcurrentHashMap<>(); + attributes = local; + } + } } - return attributes; + return local; } public Collection getAttributeKeys() throws InvalidSessionException { @@ -492,15 +499,23 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE } if (isFieldPresent(bitMask, STOP_TIMESTAMP_BIT_MASK)) { this.stopTimestamp = new AtomicReference<>((Date) in.readObject()); + } else { + this.stopTimestamp = new AtomicReference<>(); } if (isFieldPresent(bitMask, LAST_ACCESS_TIME_BIT_MASK)) { this.lastAccessTime = new AtomicReference<>((Date) in.readObject()); + } else { + this.lastAccessTime = new AtomicReference<>(); } if (isFieldPresent(bitMask, TIMEOUT_BIT_MASK)) { this.timeout = new AtomicLong(in.readLong()); + } else { + this.timeout = new AtomicLong(); } if (isFieldPresent(bitMask, EXPIRED_BIT_MASK)) { this.expired = new AtomicBoolean(in.readBoolean()); + } else { + this.expired = new AtomicBoolean(); } if (isFieldPresent(bitMask, HOST_BIT_MASK)) { this.host = in.readUTF(); From 7aac58ca9228de134b4aa286d6e1ee3a5a9b572b Mon Sep 17 00:00:00 2001 From: lprimak Date: Sat, 23 May 2026 22:54:41 -0500 Subject: [PATCH 4/8] more code review comments --- .../shiro/session/mgt/SimpleSession.java | 26 +++++++++++++------ 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java index f7c5c9ba76..0d7c85e8f4 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java @@ -458,21 +458,31 @@ private void writeObject(ObjectOutputStream out) throws IOException { if (startTimestamp != null) { out.writeObject(startTimestamp); } - if (stopTimestamp.get() != null) { - out.writeObject(stopTimestamp.get()); + + var stopTimestamp = getStopTimestamp(); + if (stopTimestamp != null) { + out.writeObject(stopTimestamp); } - if (lastAccessTime.get() != null) { - out.writeObject(lastAccessTime.get()); + + var lastAccessTime = getLastAccessTime(); + if (lastAccessTime != null) { + out.writeObject(lastAccessTime); } - if (timeout.get() != 0L) { - out.writeLong(timeout.get()); + + var timeout = getTimeout(); + if (timeout != 0L) { + out.writeLong(timeout); } - if (expired.get()) { - out.writeBoolean(expired.get()); + + var expired = isExpired(); + if (expired) { + out.writeBoolean(expired); } if (host != null) { out.writeUTF(host); } + + var attributes = getAttributes(); if (!CollectionUtils.isEmpty(attributes)) { out.writeObject(attributes); } From 1a30c96835780206514fbcd7f003787a8499131c Mon Sep 17 00:00:00 2001 From: lprimak Date: Sun, 24 May 2026 19:56:43 -0500 Subject: [PATCH 5/8] more code review changes --- .../shiro/session/mgt/SimpleSession.java | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java index 0d7c85e8f4..251c0fed0f 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java @@ -126,10 +126,6 @@ public Date getStartTimestamp() { return startTimestamp; } - public void setStartTimestamp(Date startTimestamp) { - this.startTimestamp = startTimestamp; - } - /** * Returns the time the session was stopped, or null if the session is still active. *

@@ -151,10 +147,6 @@ public Date getStopTimestamp() { return stopTimestamp.get(); } - public void setStopTimestamp(Date stopTimestamp) { - this.stopTimestamp.set(stopTimestamp); - } - public Date getLastAccessTime() { return lastAccessTime.get(); } @@ -189,16 +181,13 @@ public String getHost() { return host; } - public void setHost(String host) { - this.host = host; - } - public Map getAttributes() { return attributes; } public void setAttributes(Map attributes) { - this.attributes = attributes == null ? null : new ConcurrentHashMap<>(attributes); + this.attributes = attributes == null ? null : attributes instanceof ConcurrentHashMap ? attributes + : new ConcurrentHashMap<>(attributes); } public void touch() { @@ -440,6 +429,10 @@ public String toString() { return sb.toString(); } + void setStartTimestamp(Date startTimestamp) { + this.startTimestamp = startTimestamp; + } + /** * Serializes this object to the specified output stream for JDK Serialization. * @@ -450,7 +443,14 @@ public String toString() { @SuppressWarnings("checkstyle:NPathComplexity") private void writeObject(ObjectOutputStream out) throws IOException { out.defaultWriteObject(); - short alteredFieldsBitMask = getAlteredFieldsBitMask(); + + var stopTimestamp = getStopTimestamp(); + var lastAccessTime = getLastAccessTime(); + var timeout = getTimeout(); + var expired = isExpired(); + var attributes = getAttributes(); + + short alteredFieldsBitMask = getAlteredFieldsBitMask(stopTimestamp, lastAccessTime, timeout, expired, attributes); out.writeShort(alteredFieldsBitMask); if (id != null) { out.writeObject(id); @@ -459,22 +459,18 @@ private void writeObject(ObjectOutputStream out) throws IOException { out.writeObject(startTimestamp); } - var stopTimestamp = getStopTimestamp(); if (stopTimestamp != null) { out.writeObject(stopTimestamp); } - var lastAccessTime = getLastAccessTime(); if (lastAccessTime != null) { out.writeObject(lastAccessTime); } - var timeout = getTimeout(); if (timeout != 0L) { out.writeLong(timeout); } - var expired = isExpired(); if (expired) { out.writeBoolean(expired); } @@ -482,7 +478,6 @@ private void writeObject(ObjectOutputStream out) throws IOException { out.writeUTF(host); } - var attributes = getAttributes(); if (!CollectionUtils.isEmpty(attributes)) { out.writeObject(attributes); } @@ -531,7 +526,7 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE this.host = in.readUTF(); } if (isFieldPresent(bitMask, ATTRIBUTES_BIT_MASK)) { - this.attributes = (Map) in.readObject(); + this.attributes = (ConcurrentHashMap) in.readObject(); } } @@ -544,14 +539,15 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE * @since 1.0 */ @SuppressWarnings("checkstyle:NPathComplexity") - private short getAlteredFieldsBitMask() { + private short getAlteredFieldsBitMask(Date stopTimestamp, Date lastAccessTime, long timeout, boolean expired, + Map attributes) { int bitMask = 0; bitMask = id != null ? bitMask | ID_BIT_MASK : bitMask; bitMask = startTimestamp != null ? bitMask | START_TIMESTAMP_BIT_MASK : bitMask; - bitMask = stopTimestamp.get() != null ? bitMask | STOP_TIMESTAMP_BIT_MASK : bitMask; - bitMask = lastAccessTime.get() != null ? bitMask | LAST_ACCESS_TIME_BIT_MASK : bitMask; - bitMask = timeout.get() != 0L ? bitMask | TIMEOUT_BIT_MASK : bitMask; - bitMask = expired.get() ? bitMask | EXPIRED_BIT_MASK : bitMask; + bitMask = stopTimestamp != null ? bitMask | STOP_TIMESTAMP_BIT_MASK : bitMask; + bitMask = lastAccessTime != null ? bitMask | LAST_ACCESS_TIME_BIT_MASK : bitMask; + bitMask = timeout != 0L ? bitMask | TIMEOUT_BIT_MASK : bitMask; + bitMask = expired ? bitMask | EXPIRED_BIT_MASK : bitMask; bitMask = host != null ? bitMask | HOST_BIT_MASK : bitMask; bitMask = !CollectionUtils.isEmpty(attributes) ? bitMask | ATTRIBUTES_BIT_MASK : bitMask; return (short) bitMask; @@ -572,5 +568,4 @@ private short getAlteredFieldsBitMask() { private static boolean isFieldPresent(short bitMask, int fieldBitMask) { return (bitMask & fieldBitMask) != 0; } - } From e5a8aae782e7c8564ff527b980bb6a2f7c39bb97 Mon Sep 17 00:00:00 2001 From: lprimak Date: Mon, 25 May 2026 23:08:40 -0500 Subject: [PATCH 6/8] enh(native-sessions): session version support --- .../shiro/mgt/DefaultSecurityManager.java | 8 +++ .../mgt/AbstractNativeSessionManager.java | 9 +++ .../session/mgt/DefaultSessionContext.java | 17 +++++- .../session/mgt/DefaultSessionManager.java | 5 ++ .../session/mgt/NativeSessionManager.java | 1 + .../shiro/session/mgt/SessionContext.java | 2 + .../shiro/session/mgt/SessionManager.java | 2 + .../shiro/session/mgt/SimpleSession.java | 61 +++++++++++++++++-- .../session/mgt/SimpleSessionFactory.java | 5 +- .../shiro/session/mgt/VersionedSession.java | 32 ++++++++++ .../session/mgt/eis/CachingSessionDAO.java | 34 ++++++++--- .../subject/support/DelegatingSubject.java | 3 + .../AbstractValidatingSessionManagerTest.java | 5 ++ .../web/servlet/AbstractShiroFilter.java | 14 +++++ .../mgt/ServletContainerSessionManager.java | 4 ++ .../subject/support/WebDelegatingSubject.java | 3 + 16 files changed, 187 insertions(+), 18 deletions(-) create mode 100644 core/src/main/java/org/apache/shiro/session/mgt/VersionedSession.java diff --git a/core/src/main/java/org/apache/shiro/mgt/DefaultSecurityManager.java b/core/src/main/java/org/apache/shiro/mgt/DefaultSecurityManager.java index 58149cd93c..3de38112ed 100644 --- a/core/src/main/java/org/apache/shiro/mgt/DefaultSecurityManager.java +++ b/core/src/main/java/org/apache/shiro/mgt/DefaultSecurityManager.java @@ -557,6 +557,9 @@ protected SessionContext createSessionContext(SubjectContext subjectContext) { if (host != null) { sessionContext.setHost(host); } + if (isVersioned()) { + sessionContext.setVersioned(true); + } return sessionContext; } @@ -599,6 +602,11 @@ public void logout(Subject subject) { } } + @Override + public boolean isVersioned() { + return getSessionManager().isVersioned(); + } + protected void stopSession(Subject subject) { Session s = subject.getSession(false); if (s != null) { diff --git a/core/src/main/java/org/apache/shiro/session/mgt/AbstractNativeSessionManager.java b/core/src/main/java/org/apache/shiro/session/mgt/AbstractNativeSessionManager.java index a172a01d03..248a45e3fb 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/AbstractNativeSessionManager.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/AbstractNativeSessionManager.java @@ -310,4 +310,13 @@ public void checkValid(SessionKey key) throws InvalidSessionException { protected void onChange(Session s) { } + + @Override + public long incrementVersion(SessionKey key) { + Session session = lookupRequiredSession(key); + if (session instanceof VersionedSession versionedSession && versionedSession.isVersioned()) { + return versionedSession.incrementVersion(); + } + return 0; + } } diff --git a/core/src/main/java/org/apache/shiro/session/mgt/DefaultSessionContext.java b/core/src/main/java/org/apache/shiro/session/mgt/DefaultSessionContext.java index 702148dbc8..8e6bdf13fd 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/DefaultSessionContext.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/DefaultSessionContext.java @@ -32,12 +32,12 @@ * @since 1.0 */ public class DefaultSessionContext extends MapContext implements SessionContext { - @Serial private static final long serialVersionUID = -1424160751361252966L; private static final String HOST = DefaultSessionContext.class.getName() + ".HOST"; private static final String SESSION_ID = DefaultSessionContext.class.getName() + ".SESSION_ID"; + private static final String VERSIONED_SESSION = DefaultSessionContext.class.getName() + ".VERSIONED_SESSION"; public DefaultSessionContext() { super(); @@ -47,21 +47,36 @@ public DefaultSessionContext(Map map) { super(map); } + @Override public String getHost() { return getTypedValue(HOST, String.class); } + @Override public void setHost(String host) { if (StringUtils.hasText(host)) { put(HOST, host); } } + @Override public Serializable getSessionId() { return getTypedValue(SESSION_ID, Serializable.class); } + @Override public void setSessionId(Serializable sessionId) { nullSafePut(SESSION_ID, sessionId); } + + @Override + public boolean isVersioned() { + var versioned = getTypedValue(VERSIONED_SESSION, Boolean.class); + return versioned != null && versioned; + } + + @Override + public void setVersioned(boolean versioned) { + put(VERSIONED_SESSION, versioned); + } } diff --git a/core/src/main/java/org/apache/shiro/session/mgt/DefaultSessionManager.java b/core/src/main/java/org/apache/shiro/session/mgt/DefaultSessionManager.java index 1f007209a9..b9ec5b4f15 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/DefaultSessionManager.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/DefaultSessionManager.java @@ -22,6 +22,7 @@ import org.apache.shiro.cache.CacheManagerAware; import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; +import org.apache.shiro.session.mgt.eis.CachingSessionDAO; import org.apache.shiro.session.mgt.eis.MemorySessionDAO; import org.apache.shiro.session.mgt.eis.SessionDAO; import org.slf4j.Logger; @@ -245,4 +246,8 @@ protected Collection getActiveSessions() { return active != null ? active : Collections.emptySet(); } + @Override + public boolean isVersioned() { + return sessionDAO instanceof CachingSessionDAO; + } } diff --git a/core/src/main/java/org/apache/shiro/session/mgt/NativeSessionManager.java b/core/src/main/java/org/apache/shiro/session/mgt/NativeSessionManager.java index 3e24bec6bb..efb35a9058 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/NativeSessionManager.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/NativeSessionManager.java @@ -178,4 +178,5 @@ public interface NativeSessionManager extends SessionManager { */ Object removeAttribute(SessionKey sessionKey, Object attributeKey) throws InvalidSessionException; + long incrementVersion(SessionKey sessionKey) throws InvalidSessionException; } diff --git a/core/src/main/java/org/apache/shiro/session/mgt/SessionContext.java b/core/src/main/java/org/apache/shiro/session/mgt/SessionContext.java index fb9950d3da..ec8311596b 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/SessionContext.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/SessionContext.java @@ -88,4 +88,6 @@ public interface SessionContext extends Map { void setSessionId(Serializable sessionId); + boolean isVersioned(); + void setVersioned(boolean versioned); } diff --git a/core/src/main/java/org/apache/shiro/session/mgt/SessionManager.java b/core/src/main/java/org/apache/shiro/session/mgt/SessionManager.java index 593e158af7..be6eb0db54 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/SessionManager.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/SessionManager.java @@ -58,4 +58,6 @@ public interface SessionManager { * @since 1.0 */ Session getSession(SessionKey key) throws SessionException; + + boolean isVersioned(); } diff --git a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java index 251c0fed0f..f25d10330a 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java @@ -35,6 +35,7 @@ import java.util.Collections; import java.util.Date; import java.util.Map; +import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicLong; @@ -48,7 +49,7 @@ * @since 0.1 */ @SuppressWarnings("checkstyle:MethodCount") -public class SimpleSession implements ValidatingSession, Serializable { +public class SimpleSession implements ValidatingSession, VersionedSession, Serializable { protected static final long MILLIS_PER_SECOND = 1000; protected static final long MILLIS_PER_MINUTE = 60 * MILLIS_PER_SECOND; @@ -63,7 +64,7 @@ public class SimpleSession implements ValidatingSession, Serializable { // changes do not require a change to this number. If you need to generate // a new number in this case, use the JDK's 'serialver' program to generate it. @Serial - private static final long serialVersionUID = -7125642695178165650L; + private static final long serialVersionUID = -7125642695178165651L; private static final Logger LOGGER = LoggerFactory.getLogger(SimpleSession.class); private static final int ID_BIT_MASK = 1 << bitIndexCounter++; @@ -74,6 +75,7 @@ public class SimpleSession implements ValidatingSession, Serializable { private static final int EXPIRED_BIT_MASK = 1 << bitIndexCounter++; private static final int HOST_BIT_MASK = 1 << bitIndexCounter++; private static final int ATTRIBUTES_BIT_MASK = 1 << bitIndexCounter++; + private static final int VERSION_BIT_MASK = 1 << bitIndexCounter++; // ============================================================== // NOTICE: @@ -99,6 +101,7 @@ public class SimpleSession implements ValidatingSession, Serializable { private transient AtomicLong timeout; private transient AtomicBoolean expired = new AtomicBoolean(); private transient String host; + private transient AtomicLong version; private transient volatile Map attributes; public SimpleSession() { @@ -114,6 +117,18 @@ public SimpleSession(String host) { this.host = host; } + public SimpleSession(boolean versioned) { + this(null, versioned); + } + + public SimpleSession(String host, boolean versioned) { + this(host); + if (versioned) { + version = new AtomicLong(); + } + } + + @Override public Serializable getId() { return this.id; } @@ -122,6 +137,7 @@ public void setId(Serializable id) { this.id = id; } + @Override public Date getStartTimestamp() { return startTimestamp; } @@ -147,6 +163,7 @@ public Date getStopTimestamp() { return stopTimestamp.get(); } + @Override public Date getLastAccessTime() { return lastAccessTime.get(); } @@ -155,6 +172,22 @@ public void setLastAccessTime(Date lastAccessTime) { this.lastAccessTime.set(lastAccessTime); } + @Override + public long getVersion() { + Objects.requireNonNull(version, "versioned session is required"); + return version.get(); + } + + @Override + public boolean isVersioned() { + return version != null; + } + + public long incrementVersion() { + Objects.requireNonNull(version, "versioned session is required"); + return version.incrementAndGet(); + } + /** * Returns true if this session has expired, false otherwise. If the session has * expired, no further user interaction with the system may be done under this session. @@ -169,6 +202,7 @@ public void setExpired(boolean expired) { this.expired.set(expired); } + @Override public long getTimeout() { return timeout.get(); } @@ -177,6 +211,7 @@ public void setTimeout(long timeout) { this.timeout.set(timeout); } + @Override public String getHost() { return host; } @@ -190,10 +225,12 @@ public void setAttributes(Map attributes) { : new ConcurrentHashMap<>(attributes); } + @Override public void touch() { this.lastAccessTime.set(new Date()); } + @Override public void stop() { stopTimestamp.compareAndSet(null, new Date()); } @@ -210,6 +247,7 @@ protected void expire() { /** * @since 0.9 */ + @Override public boolean isValid() { return !isStopped() && !isExpired(); } @@ -258,6 +296,7 @@ protected boolean isTimedOut() { return false; } + @Override public void validate() throws InvalidSessionException { //check for stopped: if (isStopped()) { @@ -305,6 +344,7 @@ private Map getAttributesLazy() { return local; } + @Override public Collection getAttributeKeys() throws InvalidSessionException { Map attributes = getAttributes(); if (attributes == null) { @@ -313,6 +353,7 @@ public Collection getAttributeKeys() throws InvalidSessionException { return attributes.keySet(); } + @Override public Object getAttribute(Object key) { Map attributes = getAttributes(); if (attributes == null) { @@ -321,6 +362,7 @@ public Object getAttribute(Object key) { return attributes.get(key); } + @Override public void setAttribute(Object key, Object value) { if (value == null) { removeAttribute(key); @@ -329,6 +371,7 @@ public void setAttribute(Object key, Object value) { } } + @Override public Object removeAttribute(Object key) { Map attributes = getAttributes(); if (attributes == null) { @@ -449,8 +492,10 @@ private void writeObject(ObjectOutputStream out) throws IOException { var timeout = getTimeout(); var expired = isExpired(); var attributes = getAttributes(); + var version = isVersioned() ? getVersion() : null; - short alteredFieldsBitMask = getAlteredFieldsBitMask(stopTimestamp, lastAccessTime, timeout, expired, attributes); + short alteredFieldsBitMask = getAlteredFieldsBitMask(stopTimestamp, lastAccessTime, timeout, expired, + attributes, version); out.writeShort(alteredFieldsBitMask); if (id != null) { out.writeObject(id); @@ -481,6 +526,10 @@ private void writeObject(ObjectOutputStream out) throws IOException { if (!CollectionUtils.isEmpty(attributes)) { out.writeObject(attributes); } + + if (version != null) { + out.writeLong(version); + } } /** @@ -528,6 +577,9 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE if (isFieldPresent(bitMask, ATTRIBUTES_BIT_MASK)) { this.attributes = (ConcurrentHashMap) in.readObject(); } + if (isFieldPresent(bitMask, VERSION_BIT_MASK)) { + this.version = new AtomicLong(in.readLong()); + } } /** @@ -540,7 +592,7 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE */ @SuppressWarnings("checkstyle:NPathComplexity") private short getAlteredFieldsBitMask(Date stopTimestamp, Date lastAccessTime, long timeout, boolean expired, - Map attributes) { + Map attributes, Long version) { int bitMask = 0; bitMask = id != null ? bitMask | ID_BIT_MASK : bitMask; bitMask = startTimestamp != null ? bitMask | START_TIMESTAMP_BIT_MASK : bitMask; @@ -550,6 +602,7 @@ private short getAlteredFieldsBitMask(Date stopTimestamp, Date lastAccessTime, l bitMask = expired ? bitMask | EXPIRED_BIT_MASK : bitMask; bitMask = host != null ? bitMask | HOST_BIT_MASK : bitMask; bitMask = !CollectionUtils.isEmpty(attributes) ? bitMask | ATTRIBUTES_BIT_MASK : bitMask; + bitMask = version != null ? bitMask | VERSION_BIT_MASK : bitMask; return (short) bitMask; } diff --git a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSessionFactory.java b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSessionFactory.java index 56b8c2c59d..697c73ca75 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSessionFactory.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSessionFactory.java @@ -36,10 +36,7 @@ public class SimpleSessionFactory implements SessionFactory { */ public Session createSession(SessionContext initData) { if (initData != null) { - String host = initData.getHost(); - if (host != null) { - return new SimpleSession(host); - } + return new SimpleSession(initData.getHost(), initData.isVersioned()); } return new SimpleSession(); } diff --git a/core/src/main/java/org/apache/shiro/session/mgt/VersionedSession.java b/core/src/main/java/org/apache/shiro/session/mgt/VersionedSession.java new file mode 100644 index 0000000000..bea2c0007f --- /dev/null +++ b/core/src/main/java/org/apache/shiro/session/mgt/VersionedSession.java @@ -0,0 +1,32 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.shiro.session.mgt; + +import org.apache.shiro.session.Session; + +/** + * Implements versioned session support + * + * @since 3.0 + */ +public interface VersionedSession extends Session { + boolean isVersioned(); + long getVersion(); + long incrementVersion(); +} diff --git a/core/src/main/java/org/apache/shiro/session/mgt/eis/CachingSessionDAO.java b/core/src/main/java/org/apache/shiro/session/mgt/eis/CachingSessionDAO.java index 211d0f6e94..170c2835e6 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/eis/CachingSessionDAO.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/eis/CachingSessionDAO.java @@ -24,10 +24,14 @@ import org.apache.shiro.session.Session; import org.apache.shiro.session.UnknownSessionException; import org.apache.shiro.session.mgt.ValidatingSession; +import org.apache.shiro.session.mgt.VersionedSession; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.io.Serializable; import java.util.Collection; import java.util.Collections; +import java.util.concurrent.atomic.AtomicReference; /** * An CachingSessionDAO is a SessionDAO that provides a transparent caching layer between the components that @@ -46,12 +50,12 @@ * @since 0.2 */ public abstract class CachingSessionDAO extends AbstractSessionDAO implements CacheManagerAware { - /** * The default active sessions cache name, equal to {@code shiro-activeSessionCache}. */ public static final String ACTIVE_SESSION_CACHE_NAME = "shiro-activeSessionCache"; + private static final Logger LOGGER = LoggerFactory.getLogger(CachingSessionDAO.class); /** * The CacheManager to use to acquire the Session cache. */ @@ -60,7 +64,7 @@ public abstract class CachingSessionDAO extends AbstractSessionDAO implements Ca /** * The Cache instance responsible for caching Sessions. */ - private Cache activeSessions; + private AtomicReference> activeSessions = new AtomicReference<>(); /** * The name of the session cache, defaults to {@link #ACTIVE_SESSION_CACHE_NAME}. @@ -123,7 +127,7 @@ public void setActiveSessionsCacheName(String activeSessionsCacheName) { * should be retrieved from the */ public Cache getActiveSessionsCache() { - return this.activeSessions; + return this.activeSessions.get(); } /** @@ -135,7 +139,7 @@ public Cache getActiveSessionsCache() { * acquired from the {@link #setCacheManager configured} {@code CacheManager}. */ public void setActiveSessionsCache(Cache cache) { - this.activeSessions = cache; + this.activeSessions.set(cache); } /** @@ -148,10 +152,8 @@ public void setActiveSessionsCache(Cache cache) { * @return the active sessions cache instance. */ private Cache getActiveSessionsCacheLazy() { - if (this.activeSessions == null) { - this.activeSessions = createActiveSessionsCache(); - } - return activeSessions; + activeSessions.compareAndSet(null, createActiveSessionsCache()); + return activeSessions.get(); } /** @@ -244,7 +246,21 @@ protected void cache(Session session, Serializable sessionId) { * @param cache the cache to store the session */ protected void cache(Session session, Serializable sessionId, Cache cache) { - cache.put(sessionId, session); + if (session instanceof VersionedSession versionedSession && versionedSession.isVersioned()) { + var previous = (VersionedSession) cache.get(versionedSession.getId()); + if (previous == null || previous.getVersion() <= versionedSession.getVersion()) { + cache.put(sessionId, session); + } else { + LOGGER.debug(""" + Not caching session with id [{}] because the version of the session in the cache + is greater than the version of the session being cached. + This is likely due to a concurrent update to the same session from another thread or JVM. + Cache version: [{}], Session version: [{}]""", + sessionId, previous.getVersion(), versionedSession.getVersion()); + } + } else { + cache.put(sessionId, session); + } } /** diff --git a/core/src/main/java/org/apache/shiro/subject/support/DelegatingSubject.java b/core/src/main/java/org/apache/shiro/subject/support/DelegatingSubject.java index 903006bc57..2584385eb1 100644 --- a/core/src/main/java/org/apache/shiro/subject/support/DelegatingSubject.java +++ b/core/src/main/java/org/apache/shiro/subject/support/DelegatingSubject.java @@ -349,6 +349,9 @@ protected SessionContext createSessionContext() { if (StringUtils.hasText(host)) { sessionContext.setHost(host); } + if (securityManager.isVersioned()) { + sessionContext.setVersioned(true); + } return sessionContext; } diff --git a/core/src/test/java/org/apache/shiro/session/mgt/AbstractValidatingSessionManagerTest.java b/core/src/test/java/org/apache/shiro/session/mgt/AbstractValidatingSessionManagerTest.java index d49567c06e..d43e79b4b8 100644 --- a/core/src/test/java/org/apache/shiro/session/mgt/AbstractValidatingSessionManagerTest.java +++ b/core/src/test/java/org/apache/shiro/session/mgt/AbstractValidatingSessionManagerTest.java @@ -85,6 +85,11 @@ protected Collection getActiveSessions() { sessions.add(invalidSession); return sessions; } + + @Override + public boolean isVersioned() { + return false; + } }; sessionManager.setSessionListeners(Arrays.asList(sessionListener)); diff --git a/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java b/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java index 975f81a697..ac38cbfaf3 100644 --- a/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java +++ b/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java @@ -20,6 +20,8 @@ import org.apache.shiro.SecurityUtils; import org.apache.shiro.session.Session; +import org.apache.shiro.session.mgt.DefaultSessionKey; +import org.apache.shiro.session.mgt.NativeSessionManager; import org.apache.shiro.subject.ExecutionException; import org.apache.shiro.subject.Subject; import org.apache.shiro.web.config.ShiroFilterConfiguration; @@ -336,6 +338,17 @@ protected void updateSessionLastAccessTime(ServletRequest request, ServletRespon } } + protected void incrementSessionVersion() { + if (!isHttpSessions()) { + var session = SecurityUtils.getSubject().getSession(false); + if (session != null) { + NativeSessionManager sm = (NativeSessionManager) SecurityUtils + .getSecurityManager(DefaultWebSecurityManager.class).getSessionManager(); + sm.incrementVersion(new DefaultSessionKey(session.getId())); + } + } + } + /** * {@code doFilterInternal} implementation that sets-up, executes, and cleans-up a Shiro-filtered request. It * performs the following ordered operations: @@ -375,6 +388,7 @@ protected void doFilterInternal(ServletRequest servletRequest, ServletResponse s subject.execute((Callable) () -> { updateSessionLastAccessTime(request, response); executeChain(request, response, chain); + incrementSessionVersion(); return null; }); } catch (ExecutionException ex) { diff --git a/web/src/main/java/org/apache/shiro/web/session/mgt/ServletContainerSessionManager.java b/web/src/main/java/org/apache/shiro/web/session/mgt/ServletContainerSessionManager.java index 1977e3477d..e5adb2b2ea 100644 --- a/web/src/main/java/org/apache/shiro/web/session/mgt/ServletContainerSessionManager.java +++ b/web/src/main/java/org/apache/shiro/web/session/mgt/ServletContainerSessionManager.java @@ -130,4 +130,8 @@ public boolean isServletContainerSessions() { return true; } + @Override + public boolean isVersioned() { + return false; + } } diff --git a/web/src/main/java/org/apache/shiro/web/subject/support/WebDelegatingSubject.java b/web/src/main/java/org/apache/shiro/web/subject/support/WebDelegatingSubject.java index cb9b76c57a..90f2af182d 100644 --- a/web/src/main/java/org/apache/shiro/web/subject/support/WebDelegatingSubject.java +++ b/web/src/main/java/org/apache/shiro/web/subject/support/WebDelegatingSubject.java @@ -96,6 +96,9 @@ protected SessionContext createSessionContext() { if (StringUtils.hasText(host)) { wsc.setHost(host); } + if (securityManager.isVersioned()) { + wsc.setVersioned(true); + } wsc.setServletRequest(this.servletRequest); wsc.setServletResponse(this.servletResponse); return wsc; From 1a1964a1200d448117b88f3b55d50942d6c305d9 Mon Sep 17 00:00:00 2001 From: lprimak Date: Tue, 26 May 2026 01:32:37 -0500 Subject: [PATCH 7/8] update after incrementing version --- .../shiro/session/mgt/AbstractNativeSessionManager.java | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/core/src/main/java/org/apache/shiro/session/mgt/AbstractNativeSessionManager.java b/core/src/main/java/org/apache/shiro/session/mgt/AbstractNativeSessionManager.java index 248a45e3fb..987f211d50 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/AbstractNativeSessionManager.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/AbstractNativeSessionManager.java @@ -314,9 +314,11 @@ protected void onChange(Session s) { @Override public long incrementVersion(SessionKey key) { Session session = lookupRequiredSession(key); + long version = 0; if (session instanceof VersionedSession versionedSession && versionedSession.isVersioned()) { - return versionedSession.incrementVersion(); + version = versionedSession.incrementVersion(); + onChange(session); } - return 0; + return version; } } From 9885308fe52335546d18bd3056f81b945aa9b088 Mon Sep 17 00:00:00 2001 From: lprimak Date: Tue, 26 May 2026 01:47:51 -0500 Subject: [PATCH 8/8] chore: code review --- .../shiro/session/mgt/NativeSessionManager.java | 16 ++++++++++++++++ .../apache/shiro/session/mgt/SessionManager.java | 14 ++++++++++++++ .../apache/shiro/session/mgt/SimpleSession.java | 10 ++++++++-- .../shiro/session/mgt/eis/CachingSessionDAO.java | 2 +- .../shiro/web/servlet/AbstractShiroFilter.java | 14 ++++++++------ 5 files changed, 47 insertions(+), 9 deletions(-) diff --git a/core/src/main/java/org/apache/shiro/session/mgt/NativeSessionManager.java b/core/src/main/java/org/apache/shiro/session/mgt/NativeSessionManager.java index efb35a9058..15536aa402 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/NativeSessionManager.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/NativeSessionManager.java @@ -178,5 +178,21 @@ public interface NativeSessionManager extends SessionManager { */ Object removeAttribute(SessionKey sessionKey, Object attributeKey) throws InvalidSessionException; + /** + * Increments the version of the associated session for implementations that track session versions to + * enforce consistency across concurrent updates. + *

+ * This method should be invoked once for each session mutation that must advance the session version, + * not once per request unless every request performs such a mutation. + *

+ * Calling this method updates the version as managed by the implementation, but it does not by itself + * guarantee any persistence behavior beyond the implementation's normal session persistence semantics. + * + * @param sessionKey the session key to use to look up the target session. + * @return the session version after the increment has been applied. If session versioning is disabled, + * implementations should perform no version change and return the current effective version, + * typically {@code 0}. + * @throws InvalidSessionException if the specified session has stopped or expired prior to calling this method. + */ long incrementVersion(SessionKey sessionKey) throws InvalidSessionException; } diff --git a/core/src/main/java/org/apache/shiro/session/mgt/SessionManager.java b/core/src/main/java/org/apache/shiro/session/mgt/SessionManager.java index be6eb0db54..2bc757d7d5 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/SessionManager.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/SessionManager.java @@ -59,5 +59,19 @@ public interface SessionManager { */ Session getSession(SessionKey key) throws SessionException; + /** + * Returns {@code true} if sessions managed by this instance are versioned. + *

+ * In this context, "versioned" means that the underlying session implementation maintains a version + * value for a session and updates that value as session state changes, typically to support persistence + * and/or concurrency control in a backing store. This flag does not control whether sessions can be + * created, how they are looked up, cache ordering semantics, or general session lifecycle behavior. + *

+ * Implementations that do not maintain session version metadata should return {@code false}. Implementations + * that track and expose such version state should return {@code true}. + * + * @return {@code true} if managed sessions maintain version state, {@code false} otherwise. + * @since 3.0 + */ boolean isVersioned(); } diff --git a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java index f25d10330a..8ada55acd6 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/SimpleSession.java @@ -64,7 +64,7 @@ public class SimpleSession implements ValidatingSession, VersionedSession, Seria // changes do not require a change to this number. If you need to generate // a new number in this case, use the JDK's 'serialver' program to generate it. @Serial - private static final long serialVersionUID = -7125642695178165651L; + private static final long serialVersionUID = -7125642695178165650L; private static final Logger LOGGER = LoggerFactory.getLogger(SimpleSession.class); private static final int ID_BIT_MASK = 1 << bitIndexCounter++; @@ -183,6 +183,7 @@ public boolean isVersioned() { return version != null; } + @Override public long incrementVersion() { Objects.requireNonNull(version, "versioned session is required"); return version.incrementAndGet(); @@ -575,7 +576,12 @@ private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundE this.host = in.readUTF(); } if (isFieldPresent(bitMask, ATTRIBUTES_BIT_MASK)) { - this.attributes = (ConcurrentHashMap) in.readObject(); + var attributes = (Map) in.readObject(); + if (attributes instanceof ConcurrentHashMap) { + this.attributes = attributes; + } else { + this.attributes = new ConcurrentHashMap<>(attributes); + } } if (isFieldPresent(bitMask, VERSION_BIT_MASK)) { this.version = new AtomicLong(in.readLong()); diff --git a/core/src/main/java/org/apache/shiro/session/mgt/eis/CachingSessionDAO.java b/core/src/main/java/org/apache/shiro/session/mgt/eis/CachingSessionDAO.java index 170c2835e6..b905d82e7b 100644 --- a/core/src/main/java/org/apache/shiro/session/mgt/eis/CachingSessionDAO.java +++ b/core/src/main/java/org/apache/shiro/session/mgt/eis/CachingSessionDAO.java @@ -247,7 +247,7 @@ protected void cache(Session session, Serializable sessionId) { */ protected void cache(Session session, Serializable sessionId, Cache cache) { if (session instanceof VersionedSession versionedSession && versionedSession.isVersioned()) { - var previous = (VersionedSession) cache.get(versionedSession.getId()); + var previous = (VersionedSession) cache.get(sessionId); if (previous == null || previous.getVersion() <= versionedSession.getVersion()) { cache.put(sessionId, session); } else { diff --git a/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java b/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java index ac38cbfaf3..1069f14cdf 100644 --- a/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java +++ b/web/src/main/java/org/apache/shiro/web/servlet/AbstractShiroFilter.java @@ -341,10 +341,9 @@ protected void updateSessionLastAccessTime(ServletRequest request, ServletRespon protected void incrementSessionVersion() { if (!isHttpSessions()) { var session = SecurityUtils.getSubject().getSession(false); - if (session != null) { - NativeSessionManager sm = (NativeSessionManager) SecurityUtils - .getSecurityManager(DefaultWebSecurityManager.class).getSessionManager(); - sm.incrementVersion(new DefaultSessionKey(session.getId())); + if (session != null && SecurityUtils.getSecurityManager(DefaultWebSecurityManager.class) + .getSessionManager() instanceof NativeSessionManager sessionManager) { + sessionManager.incrementVersion(new DefaultSessionKey(session.getId())); } } } @@ -387,8 +386,11 @@ protected void doFilterInternal(ServletRequest servletRequest, ServletResponse s subject.execute((Callable) () -> { updateSessionLastAccessTime(request, response); - executeChain(request, response, chain); - incrementSessionVersion(); + try { + executeChain(request, response, chain); + } finally { + incrementSessionVersion(); + } return null; }); } catch (ExecutionException ex) {