Skip to content

Commit e441ea3

Browse files
committed
enh(web): rememberMe cookie expiration is checked on the server
1 parent 42aa344 commit e441ea3

6 files changed

Lines changed: 186 additions & 44 deletions

File tree

core/src/main/java/org/apache/shiro/mgt/AbstractRememberMeManager.java

Lines changed: 56 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -25,17 +25,25 @@
2525
import org.apache.shiro.crypto.cipher.AesCipherService;
2626
import org.apache.shiro.crypto.cipher.ByteSourceBroker;
2727
import org.apache.shiro.crypto.cipher.CipherService;
28+
import org.apache.shiro.lang.io.ClassResolvingObjectInputStream;
2829
import org.apache.shiro.lang.io.DefaultSerializer;
2930
import org.apache.shiro.lang.io.Serializer;
3031
import org.apache.shiro.lang.util.ByteSource;
3132
import org.apache.shiro.lang.util.ByteUtils;
32-
import org.apache.shiro.lang.util.ClassUtils;
33+
import org.apache.shiro.lang.util.ClassUtils.ClassLoaderAccessor;
34+
import org.apache.shiro.lang.util.ClassUtils.ExceptionIgnoringAccessor;
3335
import org.apache.shiro.subject.PrincipalCollection;
3436
import org.apache.shiro.subject.Subject;
3537
import org.apache.shiro.subject.SubjectContext;
3638
import org.slf4j.Logger;
3739
import org.slf4j.LoggerFactory;
3840

41+
import java.io.IOException;
42+
import java.io.InputStream;
43+
import java.io.ObjectInputStream;
44+
import java.io.Serial;
45+
import java.io.Serializable;
46+
import java.time.Instant;
3947
import java.util.function.Supplier;
4048

4149
/**
@@ -66,16 +74,32 @@
6674
* @since 0.9
6775
*/
6876
public abstract class AbstractRememberMeManager implements RememberMeManager {
77+
protected record RememberedIdentity(PrincipalCollection principals, Instant creationTime) implements Serializable {
78+
@Serial
79+
private static final long serialVersionUID = 1L;
80+
}
6981

7082
/**
7183
* private inner log instance.
7284
*/
7385
private static final Logger LOGGER = LoggerFactory.getLogger(AbstractRememberMeManager.class);
7486

87+
private static final ClassLoaderAccessor ADDITIONAL_CL_ACCESSOR = new ExceptionIgnoringAccessor() {
88+
@Override
89+
protected ClassLoader doGetClassLoader() {
90+
return AbstractRememberMeManager.class.getClassLoader();
91+
}
92+
};
93+
7594
/**
7695
* Serializer to use for converting PrincipalCollection instances to/from byte arrays
7796
*/
78-
private Serializer<PrincipalCollection> serializer = new DefaultSerializer<>();
97+
private Serializer<RememberedIdentity> serializer = new DefaultSerializer<>() {
98+
@Override
99+
protected ObjectInputStream createObjectInputStream(InputStream inputStream) throws IOException {
100+
return new ClassResolvingObjectInputStream(inputStream, ADDITIONAL_CL_ACCESSOR);
101+
}
102+
};
79103

80104
/**
81105
* Cipher to use for encrypting/decrypting serialized byte arrays for added security
@@ -121,7 +145,7 @@ public AbstractRememberMeManager(Supplier<byte[]> keySupplier) {
121145
* @return the {@code Serializer} used to serialize and deserialize {@link PrincipalCollection} instances for
122146
* persistent remember me storage.
123147
*/
124-
public Serializer<PrincipalCollection> getSerializer() {
148+
public Serializer<RememberedIdentity> getSerializer() {
125149
return serializer;
126150
}
127151

@@ -134,7 +158,7 @@ public Serializer<PrincipalCollection> getSerializer() {
134158
* @param serializer the {@code Serializer} used to serialize and deserialize {@link PrincipalCollection} instances
135159
* for persistent remember me storage.
136160
*/
137-
public void setSerializer(Serializer<PrincipalCollection> serializer) {
161+
public void setSerializer(Serializer<RememberedIdentity> serializer) {
138162
this.serializer = serializer;
139163
}
140164

@@ -352,14 +376,14 @@ protected void rememberIdentity(Subject subject, PrincipalCollection accountPrin
352376
/**
353377
* Converts the given principal collection the byte array that will be persisted to be 'remembered' later.
354378
* <p/>
355-
* This implementation first {@link #serialize(org.apache.shiro.subject.PrincipalCollection) serializes} the
379+
* This implementation first {@link #serialize(RememberedIdentity) serializes} the
356380
* principals to a byte array and then {@link #encrypt(byte[]) encrypts} that byte array.
357381
*
358382
* @param principals the {@code PrincipalCollection} to convert to a byte array
359383
* @return the representative byte array to be persisted for remember me functionality.
360384
*/
361385
protected byte[] convertPrincipalsToBytes(PrincipalCollection principals) {
362-
byte[] bytes = serialize(principals);
386+
byte[] bytes = serialize(new RememberedIdentity(principals, now()));
363387
if (getCipherService() != null) {
364388
bytes = encrypt(bytes);
365389
}
@@ -433,7 +457,28 @@ protected PrincipalCollection convertBytesToPrincipals(byte[] bytes, SubjectCont
433457
if (getCipherService() != null) {
434458
bytes = decrypt(bytes);
435459
}
436-
return deserialize(bytes);
460+
RememberedIdentity remembered = deserialize(bytes);
461+
return checkExpiration(remembered);
462+
}
463+
464+
/**
465+
* Checks the given remembered identity for expiration. The default implementation does not perform any expiration
466+
* checks and simply returns the principals. Subclasses can override this method to perform expiration checks based on the
467+
* {@code creationTime} property of the {@code RememberedIdentity}
468+
* @param remembered identity
469+
* @return PrincipalCollection
470+
*/
471+
protected PrincipalCollection checkExpiration(RememberedIdentity remembered) {
472+
return remembered.principals();
473+
}
474+
475+
/**
476+
* Returns the current time as an {@link Instant}. Subclasses can override this method to provide a different time,
477+
* such as for testing
478+
* @return the current time as an {@link Instant}
479+
*/
480+
protected Instant now() {
481+
return Instant.now();
437482
}
438483

439484
/**
@@ -508,13 +553,8 @@ protected byte[] decrypt(byte[] encrypted) {
508553
* @param principals the principal collection to serialize to a byte array
509554
* @return the serialized principal collection in the form of a byte array
510555
*/
511-
protected byte[] serialize(PrincipalCollection principals) {
512-
ClassUtils.setAdditionalClassLoader(AbstractRememberMeManager.class.getClassLoader());
513-
try {
514-
return getSerializer().serialize(principals);
515-
} finally {
516-
ClassUtils.removeAdditionalClassLoader();
517-
}
556+
protected byte[] serialize(RememberedIdentity principals) {
557+
return getSerializer().serialize(principals);
518558
}
519559

520560
/**
@@ -524,13 +564,8 @@ protected byte[] serialize(PrincipalCollection principals) {
524564
* @param serializedIdentity the previously serialized {@code PrincipalCollection} as a byte array
525565
* @return the deserialized (reconstituted) {@code PrincipalCollection}
526566
*/
527-
protected PrincipalCollection deserialize(byte[] serializedIdentity) {
528-
ClassUtils.setAdditionalClassLoader(AbstractRememberMeManager.class.getClassLoader());
529-
try {
530-
return getSerializer().deserialize(serializedIdentity);
531-
} finally {
532-
ClassUtils.removeAdditionalClassLoader();
533-
}
567+
protected RememberedIdentity deserialize(byte[] serializedIdentity) {
568+
return getSerializer().deserialize(serializedIdentity);
534569
}
535570

536571
/**

lang/src/main/java/org/apache/shiro/lang/io/ClassResolvingObjectInputStream.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
package org.apache.shiro.lang.io;
2020

2121
import org.apache.shiro.lang.util.ClassUtils;
22+
import org.apache.shiro.lang.util.ClassUtils.ClassLoaderAccessor;
2223
import org.apache.shiro.lang.util.UnknownClassException;
2324

2425
import java.io.IOException;
@@ -33,9 +34,16 @@
3334
* @since 1.2
3435
*/
3536
public class ClassResolvingObjectInputStream extends ObjectInputStream {
37+
private final ClassLoaderAccessor additionalClassLoader;
3638

3739
public ClassResolvingObjectInputStream(InputStream inputStream) throws IOException {
40+
this(inputStream, null);
41+
}
42+
43+
public ClassResolvingObjectInputStream(InputStream inputStream,
44+
ClassLoaderAccessor additionalClassLoader) throws IOException {
3845
super(inputStream);
46+
this.additionalClassLoader = additionalClassLoader;
3947
}
4048

4149
/**
@@ -48,9 +56,9 @@ public ClassResolvingObjectInputStream(InputStream inputStream) throws IOExcepti
4856
* @throws ClassNotFoundException if the class could not be found in any known ClassLoader
4957
*/
5058
@Override
51-
protected Class<?> resolveClass(ObjectStreamClass osc) throws IOException, ClassNotFoundException {
59+
protected Class<?> resolveClass(ObjectStreamClass osc) throws ClassNotFoundException {
5260
try {
53-
return ClassUtils.forName(osc.getName());
61+
return ClassUtils.forName(osc.getName(), additionalClassLoader);
5462
} catch (UnknownClassException e) {
5563
throw new ClassNotFoundException("Unable to load ObjectStreamClass [" + osc + "]: ", e);
5664
}

lang/src/main/java/org/apache/shiro/lang/io/DefaultSerializer.java

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
import java.io.ByteArrayInputStream;
2424
import java.io.ByteArrayOutputStream;
2525
import java.io.IOException;
26+
import java.io.InputStream;
2627
import java.io.ObjectInputStream;
2728
import java.io.ObjectOutputStream;
2829

@@ -33,7 +34,6 @@
3334
* @since 0.9
3435
*/
3536
public class DefaultSerializer<T> implements Serializer<T> {
36-
3737
/**
3838
* This implementation serializes the Object by using an {@link ObjectOutputStream} backed by a
3939
* {@link ByteArrayOutputStream}. The {@code ByteArrayOutputStream}'s backing byte array is returned.
@@ -80,7 +80,7 @@ public T deserialize(byte[] serialized) throws SerializationException {
8080
ByteArrayInputStream bais = new ByteArrayInputStream(serialized);
8181
BufferedInputStream bis = new BufferedInputStream(bais);
8282
try {
83-
ObjectInputStream ois = new ClassResolvingObjectInputStream(bis);
83+
ObjectInputStream ois = createObjectInputStream(bis);
8484
@SuppressWarnings({"unchecked"})
8585
T deserialized = (T) ois.readObject();
8686
ois.close();
@@ -90,4 +90,8 @@ public T deserialize(byte[] serialized) throws SerializationException {
9090
throw new SerializationException(msg, e);
9191
}
9292
}
93+
94+
protected ObjectInputStream createObjectInputStream(InputStream inputStream) throws IOException {
95+
return new ClassResolvingObjectInputStream(inputStream);
96+
}
9397
}

lang/src/main/java/org/apache/shiro/lang/util/ClassUtils.java

Lines changed: 22 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -194,8 +194,25 @@ public static URL getResource(String name) {
194194
* @return the located class
195195
* @throws UnknownClassException if the class cannot be found.
196196
*/
197-
@SuppressWarnings("unchecked")
198197
public static <T> Class<T> forName(String fqcn) throws UnknownClassException {
198+
return forName(fqcn, null);
199+
}
200+
201+
/**
202+
* Attempts to load the specified class name from the current thread's
203+
* {@link Thread#getContextClassLoader() context class loader}, then the
204+
* current ClassLoader (<code>ClassUtils.class.getClassLoader()</code>), then the system/application
205+
* ClassLoader (<code>ClassLoader.getSystemClassLoader()</code>, in that order. If any of them cannot locate
206+
* the specified class, an <code>UnknownClassException</code> is thrown (our RuntimeException equivalent of
207+
* the JRE's <code>ClassNotFoundException</code>.
208+
*
209+
* @param fqcn the fully qualified class name to load
210+
* @param additionalClassLoader accessor to override additional class loader
211+
* @return the located class
212+
* @throws UnknownClassException if the class cannot be found.
213+
*/
214+
@SuppressWarnings("unchecked")
215+
public static <T> Class<T> forName(String fqcn, ClassLoaderAccessor additionalClassLoader) throws UnknownClassException {
199216
Class<?> clazz = THREAD_CL_ACCESSOR.loadClass(fqcn);
200217

201218
if (clazz == null) {
@@ -211,7 +228,8 @@ public static <T> Class<T> forName(String fqcn) throws UnknownClassException {
211228
LOGGER.trace("Unable to load class named [" + fqcn
212229
+ "] from the org.apache.shiro.lang ClassLoader. Trying the additionally set ClassLoader...");
213230
}
214-
clazz = ADDITIONAL_CL_ACCESSOR.loadClass(fqcn);
231+
clazz = additionalClassLoader != null ? additionalClassLoader.loadClass(fqcn)
232+
: ADDITIONAL_CL_ACCESSOR.loadClass(fqcn);
215233
}
216234

217235
if (clazz == null) {
@@ -337,7 +355,7 @@ public static void removeAdditionalClassLoader() {
337355
/**
338356
* @since 1.0
339357
*/
340-
private interface ClassLoaderAccessor {
358+
public interface ClassLoaderAccessor {
341359
Class<?> loadClass(String fqcn);
342360

343361
InputStream getResourceStream(String name);
@@ -348,8 +366,7 @@ private interface ClassLoaderAccessor {
348366
/**
349367
* @since 1.0
350368
*/
351-
private abstract static class ExceptionIgnoringAccessor implements ClassLoaderAccessor {
352-
369+
public abstract static class ExceptionIgnoringAccessor implements ClassLoaderAccessor {
353370
public Class<?> loadClass(String fqcn) {
354371
Class<?> clazz = null;
355372
ClassLoader cl = getClassLoader();

web/src/main/java/org/apache/shiro/web/mgt/CookieRememberMeManager.java

Lines changed: 28 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@
1818
*/
1919
package org.apache.shiro.web.mgt;
2020

21+
import java.time.Instant;
2122
import java.util.function.Supplier;
2223

2324
import org.apache.shiro.lang.codec.Base64;
2425
import org.apache.shiro.mgt.AbstractRememberMeManager;
26+
import org.apache.shiro.subject.PrincipalCollection;
2527
import org.apache.shiro.subject.Subject;
2628
import org.apache.shiro.subject.SubjectContext;
2729
import org.apache.shiro.web.servlet.Cookie;
@@ -38,7 +40,6 @@
3840
import jakarta.servlet.http.HttpServletResponse;
3941
import static org.apache.shiro.session.mgt.DefaultSessionManager.SECURE_COOKIE_DISABLED;
4042

41-
4243
/**
4344
* Remembers a Subject's identity by saving the Subject's {@link Subject#getPrincipals() principals} to a {@link Cookie}
4445
* for later retrieval.
@@ -72,7 +73,6 @@
7273
* @since 1.0
7374
*/
7475
public class CookieRememberMeManager extends AbstractRememberMeManager {
75-
7676
/**
7777
* The default name of the underlying rememberMe cookie which is {@code rememberMe}.
7878
*/
@@ -139,6 +139,7 @@ public void setCookie(Cookie cookie) {
139139
* @param subject the Subject for which the identity is being serialized.
140140
* @param serialized the serialized bytes to be persisted.
141141
*/
142+
@Override
142143
protected void rememberSerializedIdentity(Subject subject, byte[] serialized) {
143144

144145
if (!WebUtils.isHttp(subject)) {
@@ -191,6 +192,7 @@ private boolean isIdentityRemoved(WebSubjectContext subjectContext) {
191192
* lookup.
192193
* @return a previously serialized identity byte array or {@code null} if the byte array could not be acquired.
193194
*/
195+
@Override
194196
protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext) {
195197

196198
if (!WebUtils.isHttp(subjectContext)) {
@@ -246,6 +248,28 @@ protected byte[] getRememberedSerializedIdentity(SubjectContext subjectContext)
246248
}
247249
}
248250

251+
@Override
252+
protected PrincipalCollection checkExpiration(RememberedIdentity identity) {
253+
int maxAge = getCookie().getMaxAge();
254+
255+
// Negative maxAge means no explicit persistence lifetime is configured
256+
// (e.g. session cookie semantics), so nothing to enforce here.
257+
if (maxAge < 0) {
258+
return identity.principals();
259+
}
260+
261+
Instant expiresAt = identity.creationTime().plusSeconds(maxAge);
262+
if (now().isAfter(expiresAt)) {
263+
if (LOGGER.isDebugEnabled()) {
264+
LOGGER.debug("Remembered identity expired at [{}] based on cookie maxAge [{}] seconds.",
265+
expiresAt, maxAge);
266+
}
267+
return null;
268+
}
269+
270+
return identity.principals();
271+
}
272+
249273
/**
250274
* Sometimes a user agent will send the rememberMe cookie value without padding,
251275
* most likely because {@code =} is a separator in the cookie header.
@@ -276,6 +300,7 @@ protected String ensurePadding(String base64) {
276300
*
277301
* @param subject the subject instance for which identity data should be forgotten from the underlying persistence
278302
*/
303+
@Override
279304
protected void forgetIdentity(Subject subject) {
280305
if (WebUtils.isHttp(subject)) {
281306
HttpServletRequest request = WebUtils.getHttpRequest(subject);
@@ -293,6 +318,7 @@ protected void forgetIdentity(Subject subject) {
293318
*
294319
* @param subjectContext the contextual data, usually provided by a {@link Subject.Builder} implementation
295320
*/
321+
@Override
296322
public void forgetIdentity(SubjectContext subjectContext) {
297323
if (WebUtils.isHttp(subjectContext)) {
298324
HttpServletRequest request = WebUtils.getHttpRequest(subjectContext);

0 commit comments

Comments
 (0)