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..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 @@ -310,4 +310,15 @@ public void checkValid(SessionKey key) throws InvalidSessionException { 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()) { + version = versionedSession.incrementVersion(); + onChange(session); + } + return version; + } } 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..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,4 +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/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..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 @@ -58,4 +58,20 @@ public interface SessionManager { * @since 1.0 */ 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 fbe8df02a1..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 @@ -34,8 +34,12 @@ import java.util.Collection; import java.util.Collections; import java.util.Date; -import java.util.HashMap; 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; +import java.util.concurrent.atomic.AtomicReference; /** @@ -45,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; @@ -71,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: @@ -91,18 +96,20 @@ 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; + private transient AtomicLong version; + private transient volatile 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) { @@ -110,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; } @@ -118,14 +137,11 @@ public void setId(Serializable id) { this.id = id; } + @Override 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. *

@@ -144,19 +160,33 @@ public void setStartTimestamp(Date startTimestamp) { * active. */ public Date getStopTimestamp() { - return stopTimestamp; - } - - public void setStopTimestamp(Date stopTimestamp) { - this.stopTimestamp = stopTimestamp; + return stopTimestamp.get(); } + @Override public Date getLastAccessTime() { - return lastAccessTime; + return lastAccessTime.get(); } public void setLastAccessTime(Date lastAccessTime) { - this.lastAccessTime = 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; + } + + @Override + public long incrementVersion() { + Objects.requireNonNull(version, "versioned session is required"); + return version.incrementAndGet(); } /** @@ -166,45 +196,44 @@ 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); } + @Override public long getTimeout() { - return timeout; + return timeout.get(); } public void setTimeout(long timeout) { - this.timeout = timeout; + this.timeout.set(timeout); } + @Override 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; + this.attributes = attributes == null ? null : attributes instanceof ConcurrentHashMap ? attributes + : new ConcurrentHashMap<>(attributes); } + @Override public void touch() { - this.lastAccessTime = new Date(); + this.lastAccessTime.set(new Date()); } + @Override public void stop() { - if (this.stopTimestamp == null) { - this.stopTimestamp = new Date(); - } + stopTimestamp.compareAndSet(null, new Date()); } protected boolean isStopped() { @@ -213,12 +242,13 @@ protected boolean isStopped() { protected void expire() { stop(); - this.expired = true; + this.expired.set(true); } /** * @since 0.9 */ + @Override public boolean isValid() { return !isStopped() && !isExpired(); } @@ -267,6 +297,7 @@ protected boolean isTimedOut() { return false; } + @Override public void validate() throws InvalidSessionException { //check for stopped: if (isStopped()) { @@ -301,14 +332,20 @@ public void validate() throws InvalidSessionException { } private Map getAttributesLazy() { - Map attributes = getAttributes(); - if (attributes == null) { - attributes = new HashMap(); - setAttributes(attributes); + Map local = attributes; + if (local == null) { + synchronized (this) { + local = attributes; + if (local == null) { + local = new ConcurrentHashMap<>(); + attributes = local; + } + } } - return attributes; + return local; } + @Override public Collection getAttributeKeys() throws InvalidSessionException { Map attributes = getAttributes(); if (attributes == null) { @@ -317,6 +354,7 @@ public Collection getAttributeKeys() throws InvalidSessionException { return attributes.keySet(); } + @Override public Object getAttribute(Object key) { Map attributes = getAttributes(); if (attributes == null) { @@ -325,6 +363,7 @@ public Object getAttribute(Object key) { return attributes.get(key); } + @Override public void setAttribute(Object key, Object value) { if (value == null) { removeAttribute(key); @@ -333,6 +372,7 @@ public void setAttribute(Object key, Object value) { } } + @Override public Object removeAttribute(Object key) { Map attributes = getAttributes(); if (attributes == null) { @@ -433,6 +473,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. * @@ -443,7 +487,16 @@ 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(); + var version = isVersioned() ? getVersion() : null; + + short alteredFieldsBitMask = getAlteredFieldsBitMask(stopTimestamp, lastAccessTime, timeout, expired, + attributes, version); out.writeShort(alteredFieldsBitMask); if (id != null) { out.writeObject(id); @@ -451,24 +504,33 @@ private void writeObject(ObjectOutputStream out) throws IOException { if (startTimestamp != null) { out.writeObject(startTimestamp); } + if (stopTimestamp != null) { out.writeObject(stopTimestamp); } + if (lastAccessTime != null) { out.writeObject(lastAccessTime); } + if (timeout != 0L) { out.writeLong(timeout); } + if (expired) { out.writeBoolean(expired); } if (host != null) { out.writeUTF(host); } + if (!CollectionUtils.isEmpty(attributes)) { out.writeObject(attributes); } + + if (version != null) { + out.writeLong(version); + } } /** @@ -491,22 +553,38 @@ 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()); + } else { + this.stopTimestamp = new AtomicReference<>(); } if (isFieldPresent(bitMask, LAST_ACCESS_TIME_BIT_MASK)) { - this.lastAccessTime = (Date) in.readObject(); + this.lastAccessTime = new AtomicReference<>((Date) in.readObject()); + } else { + this.lastAccessTime = new AtomicReference<>(); } if (isFieldPresent(bitMask, TIMEOUT_BIT_MASK)) { - this.timeout = in.readLong(); + this.timeout = new AtomicLong(in.readLong()); + } else { + this.timeout = new AtomicLong(); } if (isFieldPresent(bitMask, EXPIRED_BIT_MASK)) { - this.expired = in.readBoolean(); + this.expired = new AtomicBoolean(in.readBoolean()); + } else { + this.expired = new AtomicBoolean(); } if (isFieldPresent(bitMask, HOST_BIT_MASK)) { this.host = in.readUTF(); } if (isFieldPresent(bitMask, ATTRIBUTES_BIT_MASK)) { - this.attributes = (Map) 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()); } } @@ -519,7 +597,8 @@ 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, Long version) { int bitMask = 0; bitMask = id != null ? bitMask | ID_BIT_MASK : bitMask; bitMask = startTimestamp != null ? bitMask | START_TIMESTAMP_BIT_MASK : bitMask; @@ -529,6 +608,7 @@ private short getAlteredFieldsBitMask() { 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; } @@ -547,5 +627,4 @@ private short getAlteredFieldsBitMask() { private static boolean isFieldPresent(short bitMask, int fieldBitMask) { return (bitMask & fieldBitMask) != 0; } - } 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..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 @@ -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(sessionId); + 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..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 @@ -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,16 @@ protected void updateSessionLastAccessTime(ServletRequest request, ServletRespon } } + protected void incrementSessionVersion() { + if (!isHttpSessions()) { + var session = SecurityUtils.getSubject().getSession(false); + if (session != null && SecurityUtils.getSecurityManager(DefaultWebSecurityManager.class) + .getSessionManager() instanceof NativeSessionManager sessionManager) { + sessionManager.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: @@ -374,7 +386,11 @@ protected void doFilterInternal(ServletRequest servletRequest, ServletResponse s subject.execute((Callable) () -> { updateSessionLastAccessTime(request, response); - executeChain(request, response, chain); + try { + executeChain(request, response, chain); + } finally { + 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;