Skip to content

Commit 04020ba

Browse files
committed
cookie session store fix jooby-project#427
1 parent faddcb8 commit 04020ba

16 files changed

Lines changed: 1533 additions & 268 deletions

File tree

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
package org.jooby.issues;
2+
3+
import java.time.Instant;
4+
import java.time.ZoneId;
5+
import java.time.format.DateTimeFormatter;
6+
import java.util.LinkedHashMap;
7+
import java.util.Locale;
8+
import java.util.Map;
9+
import java.util.concurrent.atomic.AtomicInteger;
10+
11+
import org.jooby.Results;
12+
import org.jooby.Session;
13+
import org.jooby.test.ServerFeature;
14+
import org.junit.Test;
15+
16+
import com.typesafe.config.ConfigFactory;
17+
import com.typesafe.config.ConfigValueFactory;
18+
19+
public class Issue427 extends ServerFeature {
20+
21+
{
22+
use(ConfigFactory.empty().withValue("application.secret",
23+
ConfigValueFactory.fromAnyRef("1234Querty")));
24+
25+
cookieSession();
26+
27+
AtomicInteger inc = new AtomicInteger();
28+
29+
get("/427", req -> {
30+
Session session = req.session();
31+
session.set("foo", inc.incrementAndGet());
32+
Map<String, Object> hash = new LinkedHashMap<>(session.attributes());
33+
hash.put("id", session.id());
34+
hash.put("createdAt", session.createdAt());
35+
hash.put("accessedAt", session.accessedAt());
36+
hash.put("savedAt", session.savedAt());
37+
hash.put("expireAt", session.expiryAt());
38+
hash.put("toString", session.toString());
39+
return hash;
40+
});
41+
42+
get("/427/destroy", req -> {
43+
req.session().destroy();
44+
return Results.ok();
45+
});
46+
47+
get("/427/:name", req -> {
48+
return req.session().get(req.param("name").value()).value();
49+
});
50+
}
51+
52+
@Test
53+
public void sessionData() throws Exception {
54+
long maxAge = System.currentTimeMillis() + 60 * 1000;
55+
DateTimeFormatter.ofPattern("E, dd-MMM-yyyy HH:mm")
56+
.withZone(ZoneId.of("GMT"))
57+
.withLocale(Locale.ENGLISH);
58+
Instant.ofEpochMilli(maxAge);
59+
60+
request()
61+
.get("/427")
62+
.expect("{foo=1, id=cookieSession, createdAt=-1, accessedAt=-1, savedAt=-1, expireAt=-1, toString=cookieSession}")
63+
.header("Set-Cookie",
64+
"jooby.sid=Kq4J4jA6mChDXuIQxxrEibEzA09szjJ89IB3UQuWwAM|foo=1;Version=1;Path=/;HttpOnly");
65+
66+
request()
67+
.get("/427/foo")
68+
.expect("1");
69+
70+
request()
71+
.get("/427")
72+
.expect(200)
73+
.header("Set-Cookie",
74+
"jooby.sid=jajRvd/dtopEAwPK/vC59J3V5cACzfbnYMPfICaC4f8|foo=2;Version=1;Path=/;HttpOnly");
75+
76+
request()
77+
.get("/427/foo")
78+
.expect("2");
79+
80+
request()
81+
.get("/427/destroy")
82+
.expect(200)
83+
.header("Set-Cookie",
84+
"jooby.sid=;Version=1;Max-Age=0;Expires=Thu, 01-Jan-1970 00:00:00 GMT");
85+
}
86+
87+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
package org.jooby.issues;
2+
3+
import static org.junit.Assert.assertEquals;
4+
import static org.junit.Assert.assertTrue;
5+
6+
import java.time.Instant;
7+
import java.time.ZoneId;
8+
import java.time.format.DateTimeFormatter;
9+
import java.util.HashSet;
10+
import java.util.List;
11+
import java.util.Locale;
12+
import java.util.Set;
13+
import java.util.concurrent.atomic.AtomicInteger;
14+
15+
import org.jooby.Results;
16+
import org.jooby.Session;
17+
import org.jooby.test.ServerFeature;
18+
import org.junit.Test;
19+
20+
import com.google.common.base.Splitter;
21+
import com.google.common.collect.Lists;
22+
import com.typesafe.config.ConfigFactory;
23+
import com.typesafe.config.ConfigValueFactory;
24+
25+
public class Issue427b extends ServerFeature {
26+
27+
{
28+
use(ConfigFactory.empty()
29+
.withValue("application.secret", ConfigValueFactory.fromAnyRef("1234Querty"))
30+
.withValue("session.cookie.maxAge", ConfigValueFactory.fromAnyRef(30)));
31+
32+
cookieSession();
33+
34+
AtomicInteger inc = new AtomicInteger();
35+
36+
get("/427", req -> {
37+
Session session = req.session();
38+
session.set("foo", inc.incrementAndGet());
39+
return Results.ok();
40+
});
41+
42+
get("/427/destroy", req -> {
43+
req.session().destroy();
44+
return Results.ok();
45+
});
46+
47+
get("/427/:name", req -> {
48+
return req.session().get(req.param("name").value()).value();
49+
});
50+
}
51+
52+
@Test
53+
public void sessionDataWithMaxAge() throws Exception {
54+
long maxAge = System.currentTimeMillis() + 30 * 1000;
55+
// remove seconds to make sure test always work
56+
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("E, dd-MMM-yyyy HH:mm")
57+
.withZone(ZoneId.of("GMT"))
58+
.withLocale(Locale.ENGLISH);
59+
Instant instant = Instant.ofEpochMilli(maxAge);
60+
61+
Set<String> values = new HashSet<>();
62+
request()
63+
.get("/427")
64+
.expect(200)
65+
.header("Set-Cookie", value -> {
66+
values.add(value);
67+
List<String> setCookie = Lists.newArrayList(
68+
Splitter.onPattern(";\\s*")
69+
.splitToList(value));
70+
71+
assertTrue(setCookie.remove(0).startsWith("jooby.sid"));
72+
assertTrue(setCookie.remove("Path=/") || setCookie.remove("Path=\"/\""));
73+
assertTrue(setCookie.remove("HttpOnly") || setCookie.remove("HTTPOnly"));
74+
assertTrue(value, setCookie.remove("Max-Age=30"));
75+
assertTrue(value, setCookie.remove("Version=1"));
76+
assertTrue(setCookie.remove(0).startsWith(
77+
"Expires=" + formatter.format(instant).replace("GMT", "")));
78+
});
79+
80+
Thread.sleep(1000L);
81+
request()
82+
.get("/427/foo")
83+
.expect("1")
84+
.header("Set-Cookie", value -> {
85+
values.add(value);
86+
List<String> setCookie = Lists.newArrayList(
87+
Splitter.onPattern(";\\s*")
88+
.splitToList(value));
89+
90+
assertTrue(setCookie.remove(0).startsWith("jooby.sid"));
91+
assertTrue(setCookie.remove("Path=/") || setCookie.remove("Path=\"/\""));
92+
assertTrue(setCookie.remove("HttpOnly") || setCookie.remove("HTTPOnly"));
93+
assertTrue(value, setCookie.remove("Max-Age=30"));
94+
assertTrue(value, setCookie.remove("Version=1"));
95+
assertTrue(setCookie.remove(0).startsWith(
96+
"Expires=" + formatter.format(instant).replace("GMT", "")));
97+
});
98+
99+
assertEquals(2, values.size());
100+
}
101+
102+
}

jooby/src/main/java/org/jooby/Cookie.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -401,7 +401,7 @@ public class Signature {
401401
* Signed value looks like:
402402
*
403403
* <pre>
404-
* [raw value] '|' [signed value]
404+
* [signed value] '|' [raw value]
405405
* </pre>
406406
*
407407
* @param value A value to sign.
@@ -416,7 +416,7 @@ public static String sign(final String value, final String secret) {
416416
Mac mac = Mac.getInstance(HMAC_SHA256);
417417
mac.init(new SecretKeySpec(secret.getBytes(), HMAC_SHA256));
418418
byte[] bytes = mac.doFinal(value.getBytes());
419-
return value + SEP + EQ.matcher(BaseEncoding.base64().encode(bytes)).replaceAll("");
419+
return EQ.matcher(BaseEncoding.base64().encode(bytes)).replaceAll("") + SEP + value;
420420
} catch (Exception ex) {
421421
throw new IllegalArgumentException("Can't sing value", ex);
422422
}
@@ -437,7 +437,7 @@ public static String unsign(final String value, final String secret) {
437437
if (sep <= 0) {
438438
return null;
439439
}
440-
String str = value.substring(0, sep);
440+
String str = value.substring(sep + 1);
441441
String mac = sign(str, secret);
442442

443443
return mac.equals(value) ? str : null;

jooby/src/main/java/org/jooby/Jooby.java

Lines changed: 84 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,7 @@
7676
import java.util.Optional;
7777
import java.util.Set;
7878
import java.util.TimeZone;
79+
import java.util.concurrent.TimeUnit;
7980
import java.util.concurrent.atomic.AtomicBoolean;
8081
import java.util.function.Consumer;
8182
import java.util.function.Function;
@@ -93,6 +94,7 @@
9394
import org.jooby.internal.AppPrinter;
9495
import org.jooby.internal.BuiltinParser;
9596
import org.jooby.internal.BuiltinRenderer;
97+
import org.jooby.internal.CookieSessionManager;
9698
import org.jooby.internal.DefaulErrRenderer;
9799
import org.jooby.internal.HttpHandlerImpl;
98100
import org.jooby.internal.JvmInfo;
@@ -101,6 +103,7 @@
101103
import org.jooby.internal.RequestScope;
102104
import org.jooby.internal.RouteMetadata;
103105
import org.jooby.internal.ServerLookup;
106+
import org.jooby.internal.ServerSessionManager;
104107
import org.jooby.internal.SessionManager;
105108
import org.jooby.internal.TypeConverters;
106109
import org.jooby.internal.handlers.HeadHandler;
@@ -131,6 +134,7 @@
131134
import com.google.inject.Guice;
132135
import com.google.inject.Injector;
133136
import com.google.inject.Key;
137+
import com.google.inject.Provider;
134138
import com.google.inject.Stage;
135139
import com.google.inject.TypeLiteral;
136140
import com.google.inject.multibindings.Multibinder;
@@ -1101,8 +1105,8 @@ public Route.OneArgHandler promise(final Deferred.Initializer0 initializer) {
11011105
}
11021106

11031107
/**
1104-
* Setup a session store to use. Useful if you want/need to persist sessions between shutdowns.
1105-
* Sessions are not persisted by defaults.
1108+
* Setup a session store to use. Useful if you want/need to persist sessions between shutdowns, or
1109+
* save data in redis, memcached, mongodb, couchbase, etc..
11061110
*
11071111
* @param store A session store.
11081112
* @return A session store definition.
@@ -1113,8 +1117,28 @@ public Session.Definition session(final Class<? extends Session.Store> store) {
11131117
}
11141118

11151119
/**
1116-
* Setup a session store to use. Useful if you want/need to persist sessions between shutdowns.
1117-
* Sessions are not persisted by defaults.
1120+
* Setup a session store that saves data in a the session cookie. It makes the application
1121+
* stateless, which help to scale easily. Keep in mind that a cookie has a limited size (up to
1122+
* 4kb) so you must pay attention to what you put in the session object (don't use as cache).
1123+
*
1124+
* Cookie session signed data using the <code>application.secret</code> property, so you must
1125+
* provide an <code>application.secret</code> value. On dev environment you can set it in your
1126+
* <code>.conf</code> file. In prod is probably better to provide as command line argument and/or
1127+
* environment variable. Just make sure to keep it private.
1128+
*
1129+
* Please note {@link Session#id()}, {@link Session#accessedAt()}, etc.. make no sense for cookie
1130+
* sessions, just the {@link Session#attributes()}.
1131+
*
1132+
* @return A session definition/configuration object.
1133+
*/
1134+
public Session.Definition cookieSession() {
1135+
this.session = new Session.Definition();
1136+
return this.session;
1137+
}
1138+
1139+
/**
1140+
* Setup a session store to use. Useful if you want/need to persist sessions between shutdowns, or
1141+
* save data in redis, memcached, mongodb, couchbase, etc..
11181142
*
11191143
* @param store A session store.
11201144
* @return A session store definition.
@@ -3830,6 +3854,11 @@ private Injector bootstrap(final Config args,
38303854
finalEnv = env;
38313855
}
38323856

3857+
boolean cookieSession = session.store() == null;
3858+
if (cookieSession && !finalConfig.hasPath("application.secret")) {
3859+
throw new IllegalStateException("Required property 'application.secret' is missing");
3860+
}
3861+
38333862
/** dependency injection */
38343863
@SuppressWarnings("unchecked")
38353864
Injector injector = Guice.createInjector(stage, binder -> {
@@ -3951,14 +3980,21 @@ private Injector bootstrap(final Config args,
39513980
binder.bindScope(RequestScoped.class, requestScope);
39523981

39533982
/** session manager */
3954-
binder.bind(SessionManager.class).asEagerSingleton();
3955-
binder.bind(Session.Definition.class).toInstance(session);
3983+
binder.bind(Session.Definition.class)
3984+
.toProvider(session(finalConfig.getConfig("session"), session))
3985+
.asEagerSingleton();
39563986
Object sstore = session.store();
3957-
if (sstore instanceof Class) {
3958-
binder.bind(Session.Store.class).to((Class<? extends Store>) sstore)
3987+
if (cookieSession) {
3988+
binder.bind(SessionManager.class).to(CookieSessionManager.class)
39593989
.asEagerSingleton();
39603990
} else {
3961-
binder.bind(Session.Store.class).toInstance((Store) sstore);
3991+
binder.bind(SessionManager.class).to(ServerSessionManager.class).asEagerSingleton();
3992+
if (sstore instanceof Class) {
3993+
binder.bind(Session.Store.class).to((Class<? extends Store>) sstore)
3994+
.asEagerSingleton();
3995+
} else {
3996+
binder.bind(Session.Store.class).toInstance((Store) sstore);
3997+
}
39623998
}
39633999

39644000
binder.bind(Request.class).toProvider(Providers.outOfScope(Request.class))
@@ -3986,6 +4022,45 @@ private Injector bootstrap(final Config args,
39864022
return injector;
39874023
}
39884024

4025+
private static Provider<Session.Definition> session(final Config $session,
4026+
final Session.Definition session) {
4027+
return () -> {
4028+
// save interval
4029+
session.saveInterval(session.saveInterval()
4030+
.orElse($session.getDuration("saveInterval", TimeUnit.MILLISECONDS)));
4031+
4032+
// build cookie
4033+
Cookie.Definition source = session.cookie();
4034+
4035+
source.name(source.name()
4036+
.orElse($session.getString("cookie.name")));
4037+
4038+
if (!source.comment().isPresent() && $session.hasPath("cookie.comment")) {
4039+
source.comment($session.getString("cookie.comment"));
4040+
}
4041+
if (!source.domain().isPresent() && $session.hasPath("cookie.domain")) {
4042+
source.domain($session.getString("cookie.domain"));
4043+
}
4044+
source.httpOnly(source.httpOnly()
4045+
.orElse($session.getBoolean("cookie.httpOnly")));
4046+
4047+
Object maxAge = $session.getAnyRef("cookie.maxAge");
4048+
if (maxAge instanceof String) {
4049+
maxAge = $session.getDuration("cookie.maxAge", TimeUnit.SECONDS);
4050+
}
4051+
source.maxAge(source.maxAge()
4052+
.orElse(((Number) maxAge).intValue()));
4053+
4054+
source.path(source.path()
4055+
.orElse($session.getString("cookie.path")));
4056+
4057+
source.secure(source.secure()
4058+
.orElse($session.getBoolean("cookie.secure")));
4059+
4060+
return session;
4061+
};
4062+
}
4063+
39894064
private static Consumer<? super Object> bindService(final Set<Object> src,
39904065
final Config conf,
39914066
final Env env,

0 commit comments

Comments
 (0)