An improved Tomcat SessionManager that uses Redis via Jedis, with native encryption support. Faster and lightweight compared to Redisson, updated and maintained as needed.
This project is an alternative to other Redis based Tomcat managers. We feel that Redis is a perfect backend for Tomcat sessions, given the KVP nature of Redis and Sessions.
We've tried other Redis Based Tomcat Managers. We decided to write our own as we weren't super happy with the others.
- memcached-session-manager
- https://github.com/magro/memcached-session-manager
- Venerable
- The OG
- Simple to use
- Not really updated anymore, forks contain updates but are scattered
- No encryption
- Redisson
- https://redisson.org/articles/redis-based-tomcat-session-management.html
- Commercially supported
- Well documented
- Very complicated
- Tons of dependencies
- Tons of threads
- Does not shade the
redisson-alljar, so will cause major headaches and classpath conflicts (Jackson, SnakeYaml, Slf4j, others) - No encryption
This project operates much in the way of after-request mode of the memcached-session-manager project.
This project installs a Tomcat Valve SessionReplicationValve into Tomcat's stack.
At the same two Redis listeners are activated, one for session eviction SessionEvictionListener (meaning another Tomcat server updated the session), and one for session destruction SessionDestructionListener (meaning another server destroyed the session).
When the sessionManager is asked to load a session, it first checks it's locally stored sessions. If it can't find the session, it attempts to retrieve it from Redis.
At the end of the request this valve invokes ImprovedRedissonSessionManager.requestComplete(). The manager checks to see if the URI is on the ignore list. If the request URL is not ignored, and a session is active, or a session creation cookie is being sent to the client, it creates a Redis transaction and sends all of the attributes to Redis to be stored as a Hash type. This batch includes a session eviction event to let other Tomcat servers know they need to evict their in-memory map of the user's session and so they'll be forced to retrieve a fresh copy of the session from Redis.
With sticky sessions and plenty of Tomcat servers, this should scale to hundreds of thousands of users. Eventually you'll hit limits with Redis events eviction/destruction, but that's dependent on your application's usage patterns.
Right now, all session destruction and cache eviction notices are directed all all nodes in the cluster. This should be fine for most sane workloads. If a workload has very frequent session expiration (10s of thousands of destruction/eviction events per second), we probably need to write an enhancement to only have the managers subscribe to events for sessions they have cached. Let me know if you reach that limit. I'd be very interested to check it out.
-
1.1.0
- Fix #3 - Allow the developer to change the Jedis connection pool parameters
- Fix #9 -
getHostName()race condition: process stream read beforewaitFor() - Fix #10 - Character class name typo in
AutoDataOutputStream - Fix #11 - Replace reflective field access with direct access in
ImprovedRedisSession - Fix #12 - TOCTOU race condition in
JedisRedisService.loadSessionMap() - Fix #13 - Support running without encryption (
keyPasswordis now optional) - Fix #14 - Default
ignorePatternto a match-nothing pattern instead ofnull - Fix #15 - Hardcoded PBKDF2 salt is now configurable via
keySaltparameter
-
1.0.5
- Fix Bug #7 -
session.activate()Async operations, super(true) not invoked to indicate valve is async compatible - Fix Bug #8 -
session.activate()needs to be called, and session needs to be bound to the current manager
- Fix Bug #7 -
-
1.0.4
- No physical code changes, just resigning a release
-
1.0.3
- Minor code improvements
- Refuse to start if
keyPrefixconfig parameter ends up being blank,null, orROOT
-
1.0.2
- Changed connection pool parameters to slow down idle connection eviction (now customizable in 1.1.0)
-
1.0.1
- Initial Release
- Batches up Redis operations to be executed as single Redis operation
- Encryption Support (for non-basic types)
- Ignore URI pattern Support
- Encryption is optional. If
keyPasswordis not configured, all session data is stored as plaintext in Redis. - When encryption is enabled, session attributes that are Java basic types (long, boolean, int, String, etc) are serialized, but are not encrypted when stored in Redis. So don't put the user's password as a session attribute!
- When encryption is enabled, any other object type is encrypted after being serialized. So the user's
Principalobject is encrypted when stored in Redis and is encrypted at rest.
- All objects in the object graph of an object being put into the session must implement
Serializable- Therefore, all
@SessionScopedand@ViewScopedbeans must implementSerializable
- Therefore, all
- redex-sm ONLY supports session tracking mode
cookie - Ideally, start your Tomcat/TomEE instance with a server property called
server.hostnameso you can tell which node wrote the session into Redis.
- redex-sm has only been tested with Sticky Sessions using a load balancer
- Theoretically it should work without them, but this is an untested, and an unnecessary complication for no real-world gains
- Sticky Sessions do not mean that when a server goes down, the users attached to that server lose their session forever
- Think of sticky sessions as "session affinity", not "session super glue"
- If your load balancer notices a server is down, it will route the person to a working server and rebalance the load automatically
- Hence the term "load balancer"
- This is an important performance optimization :) You will likely see far worse performacnce and your users will experience downtime when you deploy if you don't use sticky sessions!
See the pom.xml. Any library marked as compile scope must be present on the classpath at runtime at the same level as redex-sm
- So if you're putting redex-sm in your tomcat/lib directory, you must include the compile dependencies in tomcat/lib
org.apache.commons:commons-pool2:2.12.0org.apache.commons:commons-lang3:3.17.0redis.clients:jedis:4.4.8org.slf4j:slf4j-api:1.7.36
<plugin>
<groupId>org.apache.tomee.maven</groupId>
<artifactId>tomee-maven-plugin</artifactId>
<executions>
<execution>
<id>tomee-exec</id>
<phase>package</phase>
<goals>
<goal>exec</goal>
</goals>
</execution>
</executions>
<configuration>
<attach>false</attach>
<tomeeClassifier>plus</tomeeClassifier>
<tomeeVersion>8.0.15</tomeeVersion>
<context>${project.artifactId}</context>
<runtimeWorkingDir>${project.build.finalName}-exec</runtimeWorkingDir>
<libs>
<!-- Note TomEE includes commons-lang3 and commons-pool2 by default; here for completeness -->
<!-- <lib>org.apache.commons:commons-lang3:3.17.0</lib> -->
<!-- <lib>org.apache.commons:commons-pool2:2.12.0</lib> -->
<lib>com.github.exabrial:redex-sm:1.1.0</lib>
<lib>redis.clients:jedis:4.4.8</lib>
</libs>
</configuration>
</plugin>
- Tell Tomcat you want to use a custom SessionManager
- To do this, create the following file:
src/main/webapp/META-INF/context.xml - See Tomcat documentation for other ways to configure a session manager on an application level
- Notice the ignorePattern below ignores a lot of things you may not want it to ignore depending on your use case
- To do this, create the following file:
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Manager
className="com.github.exabrial.redexsm.ImprovedRedisSessionManager"
redisUrl="${redex.redisUrl}"
keyPassword="${redex.keyPassword}"
keySalt="${redex.keySalt}"
ignorePattern="(?:^.*\/javax\.faces\.resource\/.*$)|(?:^.*\.(ico|svg|png|gif|jpg|jpeg|css|js|tts|otf|woff|woff2|eot)$)" />
</Context>
Required configuration:
redisUrl: A Redis URL- example:
rediss://default:aPassword@redis-0.prod.example.com:6380 - Best to use TLS
- See the Jedis documentation for URL format
- example:
Optional configuration:
keyPassword: An encryption key- If omitted, session attributes are stored unencrypted in Redis. Only omit this if your Redis instance is on a trusted network and you accept the risk of plaintext session data at rest.
- If provided, non-basic session attributes (e.g. the user's
Principal) are encrypted with AES-GCM before being stored in Redis - Goto https://random.org and generate 20ish mixed case characters
- The keyPassword is ran through a KDF with the configured salt
- Warning: If you have existing encrypted sessions in Redis and remove
keyPassword, those sessions will fail to load. Users with encrypted sessions will need to re-authenticate.
keySalt: A Base64-encoded salt for the PBKDF2 key derivation function- If omitted, a built-in default salt is used (backward compatible with previous versions)
- Recommended for new deployments: generate a unique 32-byte salt per deployment to provide cross-deployment isolation
- All nodes in a cluster must share the same
keySalt - To generate a salt:
openssl rand -base64 32 - Warning: Changing the salt invalidates all existing encrypted sessions. Users will need to re-authenticate.
In the above context.xml in lieu of compiling values into the application, we are deferring the configuration to the following system properties so they can be changed at runtime:
redex.redisUrlredex.keyPasswordredex.keySalt
This substitution is performed by the Tomcat container itself. Feel free to use any system property you desire. You can further defer these system properties to environment variables by reading the Tomcat documentation on creating a tomcat/bin/setenv.sh file and setting these system properties in said script:
Example setenv.sh
#!/bin/bash
export JAVA_OPTS="$JAVA_OPTS\
-Dredex.redisUrl=$REDEX_REDIS_URL\
-Dredex.keyPassword=$REDEX_KEY_PASSWORD\
-Dredex.keySalt=$REDEX_KEY_SALT\
"
keyPrefix: Override the keyPrefix. Default is the context name. Used to differentiate Redis entries and events.nodeId: Override the nodeId. Default ishostname + keyprefix + a UUID. This should be unique so the sessionManager can filter out inbound events.ignorePattern: Compiled to a Java Pattern. If the URL matches the pattern, the session will not be replicated to Redis. It's recommended your static assets match this pattern, but this is also useful for things like REST Apis.
The following optional parameters control the Redis connection pool. The defaults should work well for the vast majority of cases:
poolMinIdle: Minimum number of idle connections in the pool. Default:1poolMaxIdle: Maximum number of idle connections in the pool. Default:1poolMaxTotal: Maximum total number of connections in the pool. Default:15poolMaxWaitMillis: Maximum time in milliseconds to wait for a connection from the pool. Default:5000poolMinEvictableIdleTimeMillis: Minimum time in milliseconds a connection can sit idle before being eligible for eviction. Default:3600000(1 hour)
Example context.xml with pool customization:
<?xml version="1.0" encoding="UTF-8"?>
<Context>
<Manager
className="com.github.exabrial.redexsm.ImprovedRedisSessionManager"
redisUrl="${redex.redisUrl}"
keyPassword="${redex.keyPassword}"
poolMaxTotal="30"
poolMaxWaitMillis="10000"
ignorePattern="(?:^.*\/javax\.faces\.resource\/.*$)|(?:^.*\.(ico|svg|png|gif|jpg|jpeg|css|js|tts|otf|woff|woff2|eot)$)" />
</Context>
The environment load balancer will insert a sticky cookie:
backend your-backend-name
balance leastconn
retry-on 503 0rtt-rejected conn-failure
option httpchk
http-check send meth GET uri /health ver HTTP/1.1 hdr Host your-app.example.com
http-check expect rstatus 2[0-9][0-9]
cookie sticky insert indirect nocache maxidle 30m maxlife 2h httponly secure attr "SameSite=Strict"
server app-1.prod.example.com app-1.prod.example.com:443 check cookie app-1 ssl verify required ca-file /usr/local/share/ca-certificates/your-ca.crt
- All files in this project are copyrighted
- All files in this project are licensed under EUPL-1.2
- This license allows you to safely use this code in closed-source commercial projects, without ever having to reveal your company's proprietary application code
- However: note that if you modify/extend redex-sm, and offer online access to apps through a modified/extended redex-sm, it is required by law that the source code for your redex-sm changeset be made available first, before offering said access to your app
- Again, this does not include your proprietary application source code, just the changeset to redex-sm
- Redis, Apache, Tomcat, Redisson, Jedis, and other names are trademarks; this project is not endorsed by nor affiliated with them