Compare commits

..

32 Commits

Author SHA1 Message Date
Ranjith Manickam d0de8bb6e2
Update README.md 2020-12-23 13:00:58 +05:30
Ranjith Manickam 8593f88243 update downloads badge 2020-12-23 12:50:14 +05:30
Ranjith Manickam 9da24f2328 Add maven repo 2020-12-23 12:38:24 +05:30
Ranjith Manickam e31ea59636 Add maven repo 2020-12-23 12:28:11 +05:30
Ranjith Manickam 45b0b98725
Update README.md 2020-11-08 09:58:03 +05:30
Ranjith Manickam 2e505deee1
Update README.md 2020-05-15 16:11:45 +05:30
Ranjith Manickam df80fa0da3
Update README.md 2020-05-13 11:10:57 +05:30
Ranjith Manickam f4c6322793
Update pom.xml 2020-05-13 11:08:04 +05:30
Ranjith Manickam a0eab9407e version update 2020-05-13 11:03:04 +05:30
Ranjith Manickam 1f6e57c164 Fix for - NPE in SessionHandlerValve.java accessing Tomcat's /manager app 2020-05-13 11:02:21 +05:30
Ranjith Manickam 5d0c7aa7a1 Update README.md 2020-05-02 14:53:18 +05:30
Ranjith Manickam 3916e217a5
Update pom.xml 2020-05-02 14:45:13 +05:30
Ranjith Manickam 39df08832a changes to support single-sign-on (sso) 2020-05-02 14:39:17 +05:30
Ranjith Manickam 372332e8dc changes to support single-sign-on (sso) 2020-04-10 07:23:00 +05:30
Ranjith Manickam 7ace9f2b7b changes to support single-sign-on (sso) 2020-04-08 20:59:24 +05:30
Ranjith Manickam 0ecdfc2661 changes to support single-sign-on (sso) 2020-04-08 20:57:07 +05:30
Ranjith Manickam 80acc01418 changes to support single-sign-on (sso) 2020-04-02 12:44:40 +05:30
Ranjith Manickam ab04189b93
Update pom.xml 2020-03-22 00:27:41 +05:30
Ranjith Manickam 246b64def9
Merge pull request #52 from ran-jit/sso
changes to support single-sign-on (sso)
2020-03-22 00:25:18 +05:30
Ranjith Manickam 2b13fdadc9 changes to support single-sign-on (sso) 2020-03-20 23:51:51 +05:30
Ranjith Manickam 16fffca534 updated download summary 2020-02-09 16:23:54 +05:30
Ranjith Manickam b1d33afd0a update 2020-02-09 16:20:57 +05:30
Ranjith Manickam 920662654e
Update README.md 2020-02-09 16:17:36 +05:30
Ranjith Manickam 905ae577c5
Update README.md 2020-02-08 19:13:06 +05:30
Ranjith Manickam 45b1c5a9b0
Update README.md 2020-02-08 19:10:26 +05:30
Ranjith Manickam 9a409e43a4
Update README.md 2020-02-06 10:28:09 +05:30
Ranjith Manickam 113933975c application config system property support changes 2020-02-05 19:37:31 +05:30
Ranjith Manickam f45e1b23a9 application config system property support changes 2020-02-05 17:29:47 +05:30
Ranjith Manickam b73c1b78c2
Update README.md 2019-10-03 12:42:21 +05:30
Ranjith Manickam 094f612138
Update README.md 2019-09-04 22:03:25 +05:30
Ranjith Manickam 3d5afda53e
Update README.md 2019-08-15 13:14:55 +05:30
Ranjith Manickam 8902534782
Update README.md 2019-08-12 19:41:01 +05:30
25 changed files with 1126 additions and 244 deletions

View File

@ -14,15 +14,31 @@ Going forward, we no need to enable sticky session (JSESSIONID) in Load Balancer
- Apache Tomcat 7
- Apache Tomcat 8
- Apache Tomcat 9
- Apache Tomcat 10
## Downloads:
- [latest version (3.0.2)](https://github.com/ran-jit/tomcat-cluster-redis-session-manager/releases/tag/3.0.2)
## Downloads: [![Total Downloads](https://img.shields.io/github/downloads/ran-jit/tomcat-cluster-redis-session-manager/total.svg)](https://github.com/ran-jit/tomcat-cluster-redis-session-manager/wiki)
- [latest version (4.0)](https://github.com/ran-jit/tomcat-cluster-redis-session-manager/releases/tag/4.0)
- [older versions](https://github.com/ran-jit/tomcat-cluster-redis-session-manager/wiki)
<p align="center">
<a href="https://www.paypal.com/cgi-bin/webscr?cmd=_s-xclick&hosted_button_id=6M9A39423UMYS&source=https://github.com/ran-jit/tomcat-cluster-redis-session-manager" target="_blank"><img alt="Donate" height="30%" width="30%" src="https://github.com/ran-jit/tomcat-cluster-redis-session-manager/blob/master/src/main/resources/donate.png"></a>
<a href="https://www.buymeacoffee.com/ranmanic" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/v2/default-red.png" alt="Buy Me A Coffee" style="height: 60px !important;width: 217px !important;" ></a>
</p>
## Maven configuration
```
<repository>
<id>repsy</id>
<name>tomcat-cluster-redis-session-manager-repo</name>
<url>https://repo.repsy.io/mvn/ranmanic/tomcat-session-manager</url>
</repository>
<dependency>
<groupId>tomcat-session-manager</groupId>
<artifactId>redis</artifactId>
<version>4.0</version>
</dependency>
```
#### Pre-requisite:
1. jedis.jar
2. commons-pool2.jar
@ -33,23 +49,33 @@ more details.. https://github.com/ran-jit/tomcat-cluster-redis-session-manager/w
#### Steps to be done,
1. Copy the downloaded jars to your tomcat/lib directory.
- **tomcat/lib/**
```
tomcat/lib/
```
2. Add tomcat system property "catalina.base".
- **catalina.base="TOMCAT_LOCATION"**
* example: export catalina.base=/opt/tomcat
```
catalina.base="TOMCAT_LOCATION"
example: env "catalina.base=/opt/tomcat" bash
```
3. Copy the redis-data-cache.properties file to your tomcat/conf directory and update your Redis server details.
- **tomcat/conf/redis-data-cache.properties**
```
tomcat/conf/redis-data-cache.properties
```
4. Add the below two lines in your tomcat/conf/context.xml file.
- **&#60;Valve className="tomcat.request.session.redis.SessionHandlerValve" &#47;&#62;**
- **&#60;Manager className="tomcat.request.session.redis.SessionManager" &#47;&#62;**
```
<Valve className="tomcat.request.session.redis.SessionHandlerValve" />
<Manager className="tomcat.request.session.redis.SessionManager" />
```
5. Verify the session expiration time in tomcat/conf/web.xml file.
- **&#60;session-config&#62;**
- &nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp; **&#60;session-timeout&#62;60&#60;&#47;session-timeout&#62;**
- **&#60;&#47;session-config&#62;**
```
<session-config>
<session-timeout>60</session-timeout>
</session-config>
```
### Note:
- **All your session attribute values must implement java.io.Serializable.**
@ -67,6 +93,8 @@ more details.. https://github.com/ran-jit/tomcat-cluster-redis-session-manager/w
<tr><td>redis.sentinel.enabled</td><td>To enable redis sentinel mode<br/>- default: false<br>- supported values: true/false</td></tr>
<tr><td>redis.sentinel.master</td><td>Redis sentinel master name<br/>- default: mymaster</td></tr>
<tr><td>lb.sticky-session.enabled</td><td>To enable redis and standard session mode<br><br>If enabled,<ol><li>Must be enabled sticky session in your load balancer configuration. Else this manager may not return the updated session values</li><li>Session values are stored in local jvm and redis</li><li>If redis is down/not responding, requests uses jvm stored session values to process user requests. Redis comes back the values will be synced</li></ol>- default: false</td></tr>
<tr><td>session.persistent.policies</td><td>session persistent policies.<br/><br/>- policies - DEFAULT, SAVE_ON_CHANGE, ALWAYS_SAVE_AFTER_REQUEST <br/><ol><li>SAVE_ON_CHANGE: every time session.setAttribute() or session.removeAttribute() is called the session will be saved.</li><li>ALWAYS_SAVE_AFTER_REQUEST: force saving after every request, regardless of whether or not the manager has detected changes to the session.</li></ol>- default: DEFAULT</td></tr>
<tr><td>redis.sso.timeout</td><td>single-sign-on session timeout.<br/>- default: 0 ms (-no expiry)</td></tr>
</table>
</body>
</html>

48
pom.xml
View File

@ -2,9 +2,9 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>tomcat-cluster-redis-session-manager</groupId>
<artifactId>tomcat-cluster-redis-session-manager</artifactId>
<version>3.0.3</version>
<groupId>tomcat-session-manager</groupId>
<artifactId>redis</artifactId>
<version>4.0</version>
<packaging>jar</packaging>
<name>tomcat-cluster-redis-session-manager</name>
@ -32,12 +32,18 @@
<target-java.version>${source-java.version}</target-java.version>
<!-- For local development properties begins.. -->
<!--<tomcat-catalina.version>apache-tomcat-8.5.32</tomcat-catalina.version>-->
<!--<tomcat-servlet-api.version>apache-tomcat-8.5.32</tomcat-servlet-api.version>-->
<!--<tomcat-api.version>apache-tomcat-8.5.32</tomcat-api.version>-->
<tomcat.version>8.5.32</tomcat.version>
<!-- For local development properties end.. -->
</properties>
<distributionManagement>
<repository>
<id>repsy</id>
<name>tomcat-cluster-redis-session-manager-repo</name>
<url>https://repo.repsy.io/mvn/ranmanic/tomcat-session-manager</url>
</repository>
</distributionManagement>
<dependencies>
<dependency>
<groupId>redis.clients</groupId>
@ -56,21 +62,21 @@
</dependency>
<!-- For local development dependency begins.. -->
<!--<dependency>-->
<!--<groupId>apache-tomcat</groupId>-->
<!--<artifactId>catalina</artifactId>-->
<!--<version>${tomcat-catalina.version}</version>-->
<!--</dependency>-->
<!--<dependency>-->
<!--<groupId>apache-tomcat</groupId>-->
<!--<artifactId>servlet-api</artifactId>-->
<!--<version>${tomcat-servlet-api.version}</version>-->
<!--</dependency>-->
<!--<dependency>-->
<!--<groupId>apache-tomcat</groupId>-->
<!--<artifactId>tomcat-api</artifactId>-->
<!--<version>${tomcat-api.version}</version>-->
<!--</dependency>-->
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-catalina</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-servlet-api</artifactId>
<version>${tomcat.version}</version>
</dependency>
<dependency>
<groupId>org.apache.tomcat</groupId>
<artifactId>tomcat-api</artifactId>
<version>${tomcat.version}</version>
</dependency>
<!-- For local development dependency end.. -->
</dependencies>

View File

@ -0,0 +1,24 @@
package tomcat.request.session.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/** author: Ranjith Manickam @ 5 Feb' 2020 */
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Property {
String name() default "";
String defaultValue() default "";
PropertyType type() default PropertyType.STRING;
enum PropertyType {
STRING,
BOOLEAN,
INTEGER,
LONG
}
}

View File

@ -1,11 +1,8 @@
package tomcat.request.session;
package tomcat.request.session.constant;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
public interface SessionConstants {
byte[] NULL_SESSION = "null".getBytes();
String CATALINA_BASE = "catalina.base";
String CONF = "conf";
String SESSION_PERSISTENT_POLICIES = "session.persistent.policies";
enum SessionPolicy {
DEFAULT, SAVE_ON_CHANGE, ALWAYS_SAVE_AFTER_REQUEST;

View File

@ -45,4 +45,12 @@ public interface DataCache {
* @return - Returns the number of keys that were removed.
*/
Long delete(String key);
/**
* Check the key exists in data-cache.
*
* @param key - key with which the specified value is to be associated.
* @return - Returns true, if the key exists.
*/
Boolean exists(String key);
}

View File

@ -1,40 +0,0 @@
package tomcat.request.session.data.cache;
/** author: Ranjith Manickam @ 3 Dec' 2018 */
public interface DataCacheConstants {
// redis properties file name
String APPLICATION_PROPERTIES_FILE = "redis-data-cache.properties";
// redis properties
String REDIS_HOSTS = "redis.hosts";
String REDIS_CLUSTER_ENABLED = "redis.cluster.enabled:false";
String REDIS_SENTINEL_ENABLED = "redis.sentinel.enabled:false";
String LB_STICKY_SESSION_ENABLED = "lb.sticky-session.enabled:false";
String REDIS_MAX_ACTIVE = "redis.max.active:10";
String REDIS_TEST_ONBORROW = "redis.test.onBorrow:true";
String REDIS_TEST_ONRETURN = "redis.test.onReturn:true";
String REDIS_MAX_IDLE = "redis.max.idle:5";
String REDIS_MIN_IDLE = "redis.min.idle:1";
String REDIS_TEST_WHILEIDLE = "redis.test.whileIdle:true";
String REDIS_TEST_NUMPEREVICTION = "redis.test.numPerEviction:10";
String REDIS_TIME_BETWEENEVICTION = "redis.time.betweenEviction:60000";
String REDIS_PASSWORD = "redis.password";
String REDIS_DATABASE = "redis.database:0";
String REDIS_TIMEOUT = "redis.timeout:2000";
String REDIS_SENTINEL_MASTER = "redis.sentinel.master:mymaster";
String REDIS_CONN_FAILED_RETRY_MSG = "Jedis connection failed, retrying...";
String SESSION_EXPIRY_JOB_INTERVAL = "redis.session.expiry.job.interval:60";
String SESSION_DATA_SYNC_JOB_INTERVAL = "redis.session.data-sync.job.interval:10";
enum RedisConfigType {
DEFAULT,
SENTINEL,
CLUSTER
}
}

View File

@ -2,49 +2,24 @@ package tomcat.request.session.data.cache;
import tomcat.request.session.data.cache.impl.StandardDataCache;
import tomcat.request.session.data.cache.impl.redis.RedisCache;
import java.util.Properties;
import tomcat.request.session.model.Config;
/** author: Ranjith Manickam @ 3 Dec' 2018 */
public class DataCacheFactory {
private final Properties properties;
private final Config config;
private final int sessionExpiryTime;
public DataCacheFactory(Properties properties, int sessionExpiryTime) {
this.properties = properties;
public DataCacheFactory(Config config, int sessionExpiryTime) {
this.config = config;
this.sessionExpiryTime = sessionExpiryTime;
}
/** To get data cache. */
public DataCache getDataCache() {
if (Boolean.parseBoolean(getProperty(this.properties, DataCacheConstants.LB_STICKY_SESSION_ENABLED))) {
return new StandardDataCache(this.properties, this.sessionExpiryTime);
if (this.config.getLbStickySessionEnabled()) {
return new StandardDataCache(this.config, this.sessionExpiryTime);
}
return new RedisCache(this.properties);
}
/**
* To get property with the specified key in this properties list.
*
* @param properties - properties list.
* @param key - search key.
* @return - Returns the property value.
*/
public static String getProperty(Properties properties, String key) {
return getProperty(properties, key, null);
}
/**
* To get property with the specified key in this properties list.
*
* @param properties - properties list.
* @param key - search key.
* @param defaultValue - default value.
* @return - - Returns the property value.
*/
public static String getProperty(Properties properties, String key, String defaultValue) {
String[] keyValue = key.split(":");
return properties.getProperty(keyValue[0], (keyValue.length > 1 && defaultValue == null) ? keyValue[1] : defaultValue);
return new RedisCache(this.config);
}
}

View File

@ -3,14 +3,12 @@ package tomcat.request.session.data.cache.impl;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tomcat.request.session.data.cache.DataCache;
import tomcat.request.session.data.cache.DataCacheConstants;
import tomcat.request.session.data.cache.DataCacheFactory;
import tomcat.request.session.data.cache.impl.redis.RedisCache;
import tomcat.request.session.model.Config;
import java.io.Serializable;
import java.util.Date;
import java.util.Map;
import java.util.Properties;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@ -32,15 +30,15 @@ public class StandardDataCache extends RedisCache {
private final Executor expiryJobExecutor;
private final Executor dataSyncJobExecutor;
public StandardDataCache(Properties properties, int sessionExpiryTime) {
super(properties);
public StandardDataCache(Config config, int sessionExpiryTime) {
super(config);
this.sessionExpiryTime = sessionExpiryTime;
this.sessionData = new ConcurrentHashMap<>();
this.expiryJob = new Date().getTime();
this.dataSyncJob = new Date().getTime();
this.processDataSync = false;
this.expiryJobTriggerInterval = TimeUnit.MINUTES.toMillis(Integer.parseInt(DataCacheFactory.getProperty(properties, DataCacheConstants.SESSION_EXPIRY_JOB_INTERVAL)));
this.dataSyncJobTriggerInterval = TimeUnit.MINUTES.toMillis(Integer.parseInt(DataCacheFactory.getProperty(properties, DataCacheConstants.SESSION_DATA_SYNC_JOB_INTERVAL)));
this.expiryJobTriggerInterval = TimeUnit.MINUTES.toMillis(config.getRedisSessionExpiryJobInterval());
this.dataSyncJobTriggerInterval = TimeUnit.MINUTES.toMillis(config.getRedisSessionDataSyncJobInterval());
this.expiryJobExecutor = Executors.newSingleThreadExecutor();
this.dataSyncJobExecutor = Executors.newSingleThreadExecutor();
}
@ -112,6 +110,12 @@ public class StandardDataCache extends RedisCache {
return (value == null) ? 0L : 1L;
}
/** {@inheritDoc} */
@Override
public Boolean exists(String key) {
return this.sessionData.containsKey(key);
}
/** Session data. */
private static class SessionData implements Serializable {
private byte[] value;

View File

@ -2,17 +2,14 @@ package tomcat.request.session.data.cache.impl.redis;
import redis.clients.jedis.HostAndPort;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;
import tomcat.request.session.data.cache.DataCache;
import tomcat.request.session.data.cache.DataCacheConstants;
import tomcat.request.session.data.cache.DataCacheConstants.RedisConfigType;
import tomcat.request.session.data.cache.DataCacheFactory;
import tomcat.request.session.model.Config;
import tomcat.request.session.model.Config.RedisConfigType;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashSet;
import java.util.List;
import java.util.Properties;
import java.util.Set;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
@ -20,72 +17,71 @@ public class RedisCache implements DataCache {
private DataCache dataCache;
public RedisCache(Properties properties) {
initialize(properties);
public RedisCache(Config config) {
initialize(config);
}
/** {@inheritDoc} */
@Override
public byte[] set(String key, byte[] value) {
return dataCache.set(key, value);
return this.dataCache.set(key, value);
}
/** {@inheritDoc} */
@Override
public Long setnx(String key, byte[] value) {
return dataCache.setnx(key, value);
return this.dataCache.setnx(key, value);
}
/** {@inheritDoc} */
@Override
public Long expire(String key, int seconds) {
return dataCache.expire(key, seconds);
return this.dataCache.expire(key, seconds);
}
/** {@inheritDoc} */
@Override
public byte[] get(String key) {
return dataCache.get(key);
return this.dataCache.get(key);
}
/** {@inheritDoc} */
@Override
public Long delete(String key) {
return dataCache.delete(key);
return this.dataCache.delete(key);
}
private void initialize(Properties properties) {
RedisConfigType configType;
if (Boolean.parseBoolean(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_CLUSTER_ENABLED))) {
configType = RedisConfigType.CLUSTER;
} else if (Boolean.parseBoolean(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_SENTINEL_ENABLED))) {
configType = RedisConfigType.SENTINEL;
} else {
configType = RedisConfigType.DEFAULT;
}
/** {@inheritDoc} */
@Override
public Boolean exists(String key) {
return this.dataCache.exists(key);
}
String hosts = DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_HOSTS, String.format("%s:%s", Protocol.DEFAULT_HOST, Protocol.DEFAULT_PORT));
Collection<?> nodes = getJedisNodes(hosts, configType);
String password = DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_PASSWORD);
password = (password != null && !password.isEmpty()) ? password : null;
int database = Integer.parseInt(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_DATABASE));
int timeout = Integer.parseInt(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_TIMEOUT));
timeout = Math.max(timeout, Protocol.DEFAULT_TIMEOUT);
JedisPoolConfig poolConfig = getPoolConfig(properties);
switch (configType) {
private void initialize(Config config) {
Collection<?> nodes = getJedisNodes(config.getRedisHosts(), config.getRedisConfigType());
JedisPoolConfig poolConfig = getPoolConfig(config);
switch (config.getRedisConfigType()) {
case CLUSTER:
dataCache = new RedisClusterManager((Set<HostAndPort>) nodes, password, timeout, poolConfig);
this.dataCache = new RedisClusterManager((Set<HostAndPort>) nodes,
config.getRedisPassword(),
config.getRedisTimeout(),
poolConfig);
break;
case SENTINEL:
String masterName = String.valueOf(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_SENTINEL_MASTER));
dataCache = new RedisSentinelManager((Set<String>) nodes, masterName, password, database, timeout, poolConfig);
this.dataCache = new RedisSentinelManager((Set<String>) nodes,
config.getRedisSentinelMaster(),
config.getRedisPassword(),
config.getRedisDatabase(),
config.getRedisTimeout(),
poolConfig);
break;
default:
dataCache = new RedisStandardManager(((List<String>) nodes).get(0), Integer.parseInt(((List<String>) nodes).get(1)), password, database, timeout, poolConfig);
this.dataCache = new RedisStandardManager(((List<String>) nodes).get(0),
Integer.parseInt(((List<String>) nodes).get(1)),
config.getRedisPassword(),
config.getRedisDatabase(),
config.getRedisTimeout(),
poolConfig);
break;
}
}
@ -93,34 +89,19 @@ public class RedisCache implements DataCache {
/**
* To get redis pool config.
*
* @param properties - Redis data cache properties.
* @param config - Application config.
* @return - Returns the redis pool config.
*/
private JedisPoolConfig getPoolConfig(Properties properties) {
private JedisPoolConfig getPoolConfig(Config config) {
JedisPoolConfig poolConfig = new JedisPoolConfig();
int maxActive = Integer.parseInt(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_MAX_ACTIVE));
poolConfig.setMaxTotal(maxActive);
boolean testOnBorrow = Boolean.parseBoolean(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_TEST_ONBORROW));
poolConfig.setTestOnBorrow(testOnBorrow);
boolean testOnReturn = Boolean.parseBoolean(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_TEST_ONRETURN));
poolConfig.setTestOnReturn(testOnReturn);
int maxIdle = Integer.parseInt(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_MAX_IDLE));
poolConfig.setMaxIdle(maxIdle);
int minIdle = Integer.parseInt(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_MIN_IDLE));
poolConfig.setMinIdle(minIdle);
boolean testWhileIdle = Boolean.parseBoolean(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_TEST_WHILEIDLE));
poolConfig.setTestWhileIdle(testWhileIdle);
int testNumPerEviction = Integer.parseInt(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_TEST_NUMPEREVICTION));
poolConfig.setNumTestsPerEvictionRun(testNumPerEviction);
long timeBetweenEviction = Long.parseLong(DataCacheFactory.getProperty(properties, DataCacheConstants.REDIS_TIME_BETWEENEVICTION));
poolConfig.setTimeBetweenEvictionRunsMillis(timeBetweenEviction);
poolConfig.setMaxTotal(config.getRedisMaxActive());
poolConfig.setTestOnBorrow(config.getRedisTestOnBorrow());
poolConfig.setTestOnReturn(config.getRedisTestOnReturn());
poolConfig.setMaxIdle(config.getRedisMaxIdle());
poolConfig.setMinIdle(config.getRedisMinIdle());
poolConfig.setTestWhileIdle(config.getRedisTestWhileIdle());
poolConfig.setNumTestsPerEvictionRun(config.getRedisTestNumPerEviction());
poolConfig.setTimeBetweenEvictionRunsMillis(config.getRedisTimeBetweenEviction());
return poolConfig;
}

View File

@ -15,15 +15,15 @@ class RedisClusterManager extends RedisManager {
private final JedisCluster cluster;
private static final int NUM_RETRIES = 30;
private static final int DEFAULT_MAX_REDIRECTIONS = 5;
private static final long FAILIURE_WAIT_TIME = 4000L;
private static final int DEFAULT_MAX_RE_DIRECTIONS = 5;
private static final long FAILURE_WAIT_TIME = 4000L;
RedisClusterManager(Set<HostAndPort> nodes,
String password,
int timeout,
JedisPoolConfig poolConfig) {
super(null, FAILIURE_WAIT_TIME);
this.cluster = new JedisCluster(nodes, timeout, Protocol.DEFAULT_TIMEOUT, DEFAULT_MAX_REDIRECTIONS, password, poolConfig);
super(null, FAILURE_WAIT_TIME);
this.cluster = new JedisCluster(nodes, timeout, Protocol.DEFAULT_TIMEOUT, DEFAULT_MAX_RE_DIRECTIONS, password, poolConfig);
}
/** {@inheritDoc} */
@ -115,4 +115,22 @@ class RedisClusterManager extends RedisManager {
} while (retry && tries <= NUM_RETRIES);
return retVal;
}
/** {@inheritDoc} */
@Override
public Boolean exists(String key) {
int tries = 0;
boolean retry = true;
Boolean retVal = null;
do {
tries++;
try {
retVal = this.cluster.exists(key);
retry = false;
} catch (JedisRedirectionException | JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return retVal;
}
}

View File

@ -6,7 +6,6 @@ import redis.clients.jedis.Jedis;
import redis.clients.jedis.exceptions.JedisConnectionException;
import redis.clients.jedis.util.Pool;
import tomcat.request.session.data.cache.DataCache;
import tomcat.request.session.data.cache.DataCacheConstants;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
abstract class RedisManager implements DataCache {
@ -14,12 +13,14 @@ abstract class RedisManager implements DataCache {
private static final int NUM_RETRIES = 3;
private static final Logger LOGGER = LoggerFactory.getLogger(RedisManager.class);
private final Pool<Jedis> pool;
private final long failiureWaitTime;
private static final String REDIS_CONN_FAILED_RETRY_MSG = "Jedis connection failed, retrying...";
RedisManager(Pool<Jedis> pool, long failiureWaitTime) {
private final Pool<Jedis> pool;
private final long failureWaitTime;
RedisManager(Pool<Jedis> pool, long failureWaitTime) {
this.pool = pool;
this.failiureWaitTime = failiureWaitTime;
this.failureWaitTime = failureWaitTime;
}
/** {@inheritDoc} */
@ -112,6 +113,24 @@ abstract class RedisManager implements DataCache {
return retVal;
}
/** {@inheritDoc} */
@Override
public Boolean exists(String key) {
int tries = 0;
boolean retry = true;
Boolean retVal = null;
do {
tries++;
try (Jedis jedis = this.pool.getResource()) {
retVal = jedis.exists(key);
retry = false;
} catch (JedisConnectionException ex) {
handleException(tries, ex);
}
} while (retry && tries <= NUM_RETRIES);
return retVal;
}
/**
* To handle jedis exception.
*
@ -119,12 +138,12 @@ abstract class RedisManager implements DataCache {
* @param ex - jedis exception.
*/
void handleException(int tries, RuntimeException ex) {
LOGGER.error(DataCacheConstants.REDIS_CONN_FAILED_RETRY_MSG + tries);
LOGGER.error(REDIS_CONN_FAILED_RETRY_MSG + tries);
if (tries == NUM_RETRIES) {
throw ex;
}
try {
Thread.sleep(this.failiureWaitTime);
Thread.sleep(this.failureWaitTime);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}

View File

@ -8,7 +8,7 @@ import java.util.Set;
/** author: Ranjith Manickam @ 3 Dec' 2018 */
class RedisSentinelManager extends RedisManager {
private static final long FAILIURE_WAIT_TIME = 2000L;
private static final long FAILURE_WAIT_TIME = 2000L;
RedisSentinelManager(Set<String> nodes,
String masterName,
@ -16,6 +16,6 @@ class RedisSentinelManager extends RedisManager {
int database,
int timeout,
JedisPoolConfig poolConfig) {
super(new JedisSentinelPool(masterName, nodes, poolConfig, timeout, password, database), FAILIURE_WAIT_TIME);
super(new JedisSentinelPool(masterName, nodes, poolConfig, timeout, password, database), FAILURE_WAIT_TIME);
}
}

View File

@ -6,7 +6,7 @@ import redis.clients.jedis.JedisPoolConfig;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
class RedisStandardManager extends RedisManager {
private static final long FAILIURE_WAIT_TIME = 2000L;
private static final long FAILURE_WAIT_TIME = 2000L;
RedisStandardManager(String host,
int port,
@ -14,6 +14,6 @@ class RedisStandardManager extends RedisManager {
int database,
int timeout,
JedisPoolConfig poolConfig) {
super(new JedisPool(poolConfig, host, port, timeout, password, database), FAILIURE_WAIT_TIME);
super(new JedisPool(poolConfig, host, port, timeout, password, database), FAILURE_WAIT_TIME);
}
}

View File

@ -0,0 +1,266 @@
package tomcat.request.session.model;
import redis.clients.jedis.Protocol;
import tomcat.request.session.annotation.Property;
import java.io.Serializable;
import static tomcat.request.session.annotation.Property.PropertyType.BOOLEAN;
import static tomcat.request.session.annotation.Property.PropertyType.INTEGER;
/** author: Ranjith Manickam @ 5 Feb' 2020 */
public class Config implements Serializable {
private static final long serialVersionUID = 3480402257971437776L;
public static final String APPLICATION_PROPERTIES_FILE = "redis-data-cache.properties";
/** Redis config type. */
public enum RedisConfigType {
DEFAULT,
SENTINEL,
CLUSTER
}
@Property(name = "redis.hosts", defaultValue = "127.0.0.1:6379")
private String redisHosts;
@Property(name = "redis.cluster.enabled", type = BOOLEAN, defaultValue = "false")
private Boolean redisClusterEnabled;
@Property(name = "redis.sentinel.enabled", type = BOOLEAN, defaultValue = "false")
private Boolean redisSentinelEnabled;
@Property(name = "lb.sticky-session.enabled", type = BOOLEAN, defaultValue = "false")
private Boolean lbStickySessionEnabled;
@Property(name = "redis.max.active", type = INTEGER, defaultValue = "10")
private Integer redisMaxActive;
@Property(name = "redis.test.onBorrow", type = BOOLEAN, defaultValue = "true")
private Boolean redisTestOnBorrow;
@Property(name = "redis.test.onReturn", type = BOOLEAN, defaultValue = "true")
private Boolean redisTestOnReturn;
@Property(name = "redis.max.idle", type = INTEGER, defaultValue = "5")
private Integer redisMaxIdle;
@Property(name = "redis.min.idle", type = INTEGER, defaultValue = "1")
private Integer redisMinIdle;
@Property(name = "redis.test.whileIdle", type = BOOLEAN, defaultValue = "true")
private Boolean redisTestWhileIdle;
@Property(name = "redis.test.numPerEviction", type = INTEGER, defaultValue = "10")
private Integer redisTestNumPerEviction;
@Property(name = "redis.time.betweenEviction", type = INTEGER, defaultValue = "60000")
private Integer redisTimeBetweenEviction;
@Property(name = "redis.password")
private String redisPassword;
@Property(name = "redis.database", type = INTEGER, defaultValue = "0")
private Integer redisDatabase;
@Property(name = "redis.timeout", type = INTEGER, defaultValue = "2000")
private Integer redisTimeout;
@Property(name = "redis.sentinel.master", defaultValue = "mymaster")
private String redisSentinelMaster;
@Property(name = "redis.session.expiry.job.interval", type = INTEGER, defaultValue = "60")
private Integer redisSessionExpiryJobInterval;
@Property(name = "redis.session.data-sync.job.interval", type = INTEGER, defaultValue = "10")
private Integer redisSessionDataSyncJobInterval;
@Property(name = "session.persistent.policies", defaultValue = "DEFAULT")
private String sessionPersistentPolicies;
@Property(name = "redis.sso.timeout", type = INTEGER, defaultValue = "0")
private Integer redisSSOTimeout;
public Config() {
}
public Config(String redisHosts,
Boolean redisClusterEnabled,
Boolean redisSentinelEnabled,
Boolean lbStickySessionEnabled,
Integer redisMaxActive,
Boolean redisTestOnBorrow,
Boolean redisTestOnReturn,
Integer redisMaxIdle,
Integer redisMinIdle,
Boolean redisTestWhileIdle,
Integer redisTestNumPerEviction,
Integer redisTimeBetweenEviction,
String redisPassword,
Integer redisDatabase,
Integer redisTimeout,
String redisSentinelMaster,
Integer redisSessionExpiryJobInterval,
Integer redisSessionDataSyncJobInterval,
String sessionPersistentPolicies,
Integer redisSSOTimeout) {
this.redisHosts = redisHosts;
this.redisClusterEnabled = redisClusterEnabled;
this.redisSentinelEnabled = redisSentinelEnabled;
this.lbStickySessionEnabled = lbStickySessionEnabled;
this.redisMaxActive = redisMaxActive;
this.redisTestOnBorrow = redisTestOnBorrow;
this.redisTestOnReturn = redisTestOnReturn;
this.redisMaxIdle = redisMaxIdle;
this.redisMinIdle = redisMinIdle;
this.redisTestWhileIdle = redisTestWhileIdle;
this.redisTestNumPerEviction = redisTestNumPerEviction;
this.redisTimeBetweenEviction = redisTimeBetweenEviction;
this.redisPassword = redisPassword;
this.redisDatabase = redisDatabase;
this.redisTimeout = redisTimeout;
this.redisSentinelMaster = redisSentinelMaster;
this.redisSessionExpiryJobInterval = redisSessionExpiryJobInterval;
this.redisSessionDataSyncJobInterval = redisSessionDataSyncJobInterval;
this.sessionPersistentPolicies = sessionPersistentPolicies;
this.redisSSOTimeout = redisSSOTimeout;
}
/** To get 'redis.hosts' value. */
public String getRedisHosts() {
return redisHosts;
}
/** To get 'redis.cluster.enabled' value. */
public Boolean getRedisClusterEnabled() {
return redisClusterEnabled;
}
/** To get 'redis.sentinel.enabled' value. */
public Boolean getRedisSentinelEnabled() {
return redisSentinelEnabled;
}
/** To get 'lb.sticky-session.enabled' value. */
public Boolean getLbStickySessionEnabled() {
return lbStickySessionEnabled;
}
/** To get 'redis.max.active' value. */
public Integer getRedisMaxActive() {
return redisMaxActive;
}
/** To get 'redis.test.onBorrow' value. */
public Boolean getRedisTestOnBorrow() {
return redisTestOnBorrow;
}
/** To get 'redis.test.onReturn' value. */
public Boolean getRedisTestOnReturn() {
return redisTestOnReturn;
}
/** To get 'redis.max.idle' value. */
public Integer getRedisMaxIdle() {
return redisMaxIdle;
}
/** To get 'redis.min.idle' value. */
public Integer getRedisMinIdle() {
return redisMinIdle;
}
/** To get 'redis.test.whileIdle' value. */
public Boolean getRedisTestWhileIdle() {
return redisTestWhileIdle;
}
/** To get 'redis.test.numPerEviction' value. */
public Integer getRedisTestNumPerEviction() {
return redisTestNumPerEviction;
}
/** To get 'redis.time.betweenEviction' value. */
public Integer getRedisTimeBetweenEviction() {
return redisTimeBetweenEviction;
}
/** To get 'redis.password' value. */
public String getRedisPassword() {
return (redisPassword == null || redisPassword.isEmpty()) ? null : redisPassword;
}
/** To get 'redis.database' value. */
public Integer getRedisDatabase() {
return redisDatabase;
}
/** To get 'redis.timeout' value. */
public Integer getRedisTimeout() {
return Math.max(redisTimeout, Protocol.DEFAULT_TIMEOUT);
}
/** To get 'redis.sentinel.master' value. */
public String getRedisSentinelMaster() {
return redisSentinelMaster;
}
/** To get 'redis.session.expiry.job.interval' value. */
public Integer getRedisSessionExpiryJobInterval() {
return redisSessionExpiryJobInterval;
}
/** To get 'redis.session.data-sync.job.interval' value. */
public Integer getRedisSessionDataSyncJobInterval() {
return redisSessionDataSyncJobInterval;
}
/** To get 'session.persistent.policies' value */
public String getSessionPersistentPolicies() {
return sessionPersistentPolicies;
}
/** To get 'redis.sso.timeout' value */
public Integer getRedisSSOTimeout() {
return redisSSOTimeout;
}
/** {@inheritDoc} */
@Override
public String toString() {
return "Config{" +
"redisHosts='" + redisHosts + '\'' +
", redisClusterEnabled=" + redisClusterEnabled +
", redisSentinelEnabled=" + redisSentinelEnabled +
", lbStickySessionEnabled=" + lbStickySessionEnabled +
", redisMaxActive=" + redisMaxActive +
", redisTestOnBorrow=" + redisTestOnBorrow +
", redisTestOnReturn=" + redisTestOnReturn +
", redisMaxIdle=" + redisMaxIdle +
", redisMinIdle=" + redisMinIdle +
", redisTestWhileIdle=" + redisTestWhileIdle +
", redisTestNumPerEviction=" + redisTestNumPerEviction +
", redisTimeBetweenEviction=" + redisTimeBetweenEviction +
", redisPassword='" + redisPassword + '\'' +
", redisDatabase=" + redisDatabase +
", redisTimeout=" + redisTimeout +
", redisSentinelMaster='" + redisSentinelMaster + '\'' +
", redisSessionExpiryJobInterval=" + redisSessionExpiryJobInterval +
", redisSessionDataSyncJobInterval=" + redisSessionDataSyncJobInterval +
", sessionPersistentPolicies='" + sessionPersistentPolicies + '\'' +
", redisSSOTimeout='" + redisSSOTimeout + '\'' +
'}';
}
/** To get redis config type. */
public RedisConfigType getRedisConfigType() {
if (this.getRedisClusterEnabled()) {
return RedisConfigType.CLUSTER;
} else if (this.getRedisSentinelEnabled()) {
return RedisConfigType.SENTINEL;
}
return RedisConfigType.DEFAULT;
}
}

View File

@ -1,4 +1,8 @@
package tomcat.request.session;
package tomcat.request.session.model;
import org.apache.catalina.Manager;
import org.apache.catalina.session.StandardSession;
import tomcat.request.session.redis.SessionManager;
import java.io.IOException;
import java.io.ObjectInputStream;
@ -8,11 +12,6 @@ import java.util.Enumeration;
import java.util.HashMap;
import java.util.Map;
import org.apache.catalina.Manager;
import org.apache.catalina.session.StandardSession;
import tomcat.request.session.redis.SessionManager;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
public class Session extends StandardSession {

View File

@ -1,4 +1,4 @@
package tomcat.request.session;
package tomcat.request.session.model;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
public class SessionContext {

View File

@ -1,4 +1,4 @@
package tomcat.request.session;
package tomcat.request.session.model;
import java.io.IOException;
import java.io.ObjectInputStream;

View File

@ -0,0 +1,100 @@
package tomcat.request.session.model;
import org.apache.catalina.Session;
import org.apache.catalina.authenticator.SingleSignOnListener;
import org.apache.catalina.authenticator.SingleSignOnSessionKey;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.security.Principal;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
/** author: Ranjith Manickam @ 20 Mar' 2020 */
public class SingleSignOnEntry implements Serializable {
private static final long serialVersionUID = 4590485271396917062L;
private String authType;
private String password;
private Principal principal;
private String username;
private boolean canReauthenticate = false;
private final ConcurrentMap<SingleSignOnSessionKey, SingleSignOnSessionKey> sessionKeys;
public SingleSignOnEntry() {
this.sessionKeys = new ConcurrentHashMap<>();
}
public SingleSignOnEntry(Principal principal, String authType, String username, String password) {
this.sessionKeys = new ConcurrentHashMap<>();
this.updateCredentials(principal, authType, username, password);
}
public void addSession(String ssoId, Session session) {
SingleSignOnSessionKey key = new SingleSignOnSessionKey(session);
SingleSignOnSessionKey currentKey = this.sessionKeys.putIfAbsent(key, key);
if (currentKey == null) {
session.addSessionListener(new SingleSignOnListener(ssoId));
}
}
public void removeSession(Session session) {
SingleSignOnSessionKey key = new SingleSignOnSessionKey(session);
this.sessionKeys.remove(key);
}
public Set<SingleSignOnSessionKey> findSessions() {
return this.sessionKeys.keySet();
}
public String getAuthType() {
return this.authType;
}
public boolean getCanReauthenticate() {
return this.canReauthenticate;
}
public String getPassword() {
return this.password;
}
public Principal getPrincipal() {
return this.principal;
}
public String getUsername() {
return this.username;
}
public synchronized void updateCredentials(Principal principal, String authType, String username, String password) {
this.principal = principal;
this.authType = authType;
this.username = username;
this.password = password;
this.canReauthenticate = "BASIC".equals(authType) || "FORM".equals(authType);
}
public void writeObjectData(ObjectOutputStream out) throws IOException {
try (ObjectOutputStream outputStream = new ObjectOutputStream(out)) {
outputStream.writeObject(this);
outputStream.flush();
}
}
public void readObjectData(ObjectInputStream in) throws IOException, ClassNotFoundException {
try (ObjectInputStream inputStream = new ObjectInputStream(in)) {
SingleSignOnEntry entry = (SingleSignOnEntry) inputStream.readObject();
this.authType = entry.authType;
this.password = entry.password;
this.principal = entry.principal;
this.username = entry.username;
this.canReauthenticate = entry.canReauthenticate;
this.sessionKeys.putAll(entry.sessionKeys);
}
}
}

View File

@ -31,7 +31,9 @@ public class SessionHandlerValve extends ValveBase {
LOGGER.error("Error processing request", ex);
throw new BackendException();
} finally {
manager.afterRequest();
if (this.manager != null) {
this.manager.afterRequest();
}
}
}
}

View File

@ -9,24 +9,22 @@ import org.apache.catalina.Valve;
import org.apache.catalina.session.ManagerBase;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tomcat.request.session.SerializationUtil;
import tomcat.request.session.Session;
import tomcat.request.session.SessionConstants;
import tomcat.request.session.SessionConstants.SessionPolicy;
import tomcat.request.session.SessionContext;
import tomcat.request.session.SessionMetadata;
import tomcat.request.session.constant.SessionConstants;
import tomcat.request.session.constant.SessionConstants.SessionPolicy;
import tomcat.request.session.data.cache.DataCache;
import tomcat.request.session.data.cache.DataCacheConstants;
import tomcat.request.session.data.cache.DataCacheFactory;
import tomcat.request.session.model.Config;
import tomcat.request.session.model.Session;
import tomcat.request.session.model.SessionContext;
import tomcat.request.session.model.SessionMetadata;
import tomcat.request.session.model.SingleSignOnEntry;
import tomcat.request.session.util.ConfigUtil;
import tomcat.request.session.util.SerializationUtil;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.EnumSet;
import java.util.Properties;
import java.util.Set;
/** author: Ranjith Manickam @ 12 Jul' 2018 */
@ -34,10 +32,11 @@ public class SessionManager extends ManagerBase implements Lifecycle {
private static final Logger LOGGER = LoggerFactory.getLogger(SessionManager.class);
private Integer ssoTimeout;
private DataCache dataCache;
private SerializationUtil serializer;
private ThreadLocal<SessionContext> sessionContext = new ThreadLocal<>();
private Set<SessionPolicy> sessionPolicy = EnumSet.of(SessionPolicy.DEFAULT);
private final ThreadLocal<SessionContext> sessionContext = new ThreadLocal<>();
private final Set<SessionPolicy> sessionPolicy = EnumSet.of(SessionPolicy.DEFAULT);
public boolean getSaveOnChange() {
return this.sessionPolicy.contains(SessionPolicy.SAVE_ON_CHANGE);
@ -211,15 +210,16 @@ public class SessionManager extends ManagerBase implements Lifecycle {
/** To initialize the session manager. */
private void initialize() {
try {
Properties properties = getApplicationProperties();
this.dataCache = new DataCacheFactory(properties, getSessionTimeout(null)).getDataCache();
Config config = ConfigUtil.getConfig();
this.ssoTimeout = config.getRedisSSOTimeout();
this.dataCache = new DataCacheFactory(config, getSessionTimeout(null)).getDataCache();
this.serializer = new SerializationUtil();
Context context = getContextIns();
ClassLoader loader = (context != null && context.getLoader() != null) ? context.getLoader().getClassLoader() : null;
this.serializer.setClassLoader(loader);
setSessionPersistentPolicies(properties);
setSessionPersistentPolicies(config);
} catch (Exception ex) {
LOGGER.error("Error occurred while initializing the session manager..", ex);
throw ex;
@ -326,8 +326,8 @@ public class SessionManager extends ManagerBase implements Lifecycle {
}
/** To set session persistent policies */
private void setSessionPersistentPolicies(Properties properties) {
String sessionPolicies = properties.getProperty(SessionConstants.SESSION_PERSISTENT_POLICIES);
private void setSessionPersistentPolicies(Config config) {
String sessionPolicies = config.getSessionPersistentPolicies();
if (sessionPolicies == null || sessionPolicies.isEmpty()) {
return;
}
@ -339,30 +339,46 @@ public class SessionManager extends ManagerBase implements Lifecycle {
}
}
/** To get redis data cache properties. */
private Properties getApplicationProperties() {
Properties properties = new Properties();
/** To set single-sign-on entry to cache. */
void setSingleSignOnEntry(String ssoId, SingleSignOnEntry entry) {
if (entry == null) {
return;
}
try {
String filePath = System.getProperty(SessionConstants.CATALINA_BASE).concat(File.separator)
.concat(SessionConstants.CONF).concat(File.separator)
.concat(DataCacheConstants.APPLICATION_PROPERTIES_FILE);
InputStream resourceStream = null;
try {
resourceStream = (!filePath.isEmpty() && new File(filePath).exists()) ? new FileInputStream(filePath) : null;
if (resourceStream == null) {
ClassLoader loader = Thread.currentThread().getContextClassLoader();
resourceStream = loader.getResourceAsStream(DataCacheConstants.APPLICATION_PROPERTIES_FILE);
}
properties.load(resourceStream);
} finally {
if (resourceStream != null) {
resourceStream.close();
}
byte[] data = this.serializer.serializeSingleSignOnEntry(entry);
this.dataCache.set(ssoId, data);
if (this.ssoTimeout > 0) {
this.dataCache.expire(ssoId, this.ssoTimeout);
}
} catch (IOException ex) {
LOGGER.error("Error while retrieving application properties", ex);
LOGGER.error("Error occurred while serializing the single-sign-on entry..", ex);
}
return properties;
}
/** To get single-sign-on entry from cache. */
SingleSignOnEntry getSingleSignOnEntry(String ssoId) {
byte[] data = this.dataCache.get(ssoId);
if (data == null) {
return null;
}
SingleSignOnEntry entry = new SingleSignOnEntry();
try {
this.serializer.deserializeSingleSignOnEntry(data, entry);
} catch (IOException | ClassNotFoundException ex) {
LOGGER.error("Error occurred while de-serializing the single-sign-on entry..", ex);
return null;
}
return entry;
}
/** To check single-sign-on entry exists from cache. */
Boolean singleSignOnEntryExists(String ssoId) {
return this.dataCache.exists(ssoId);
}
/** To delete single-sign-on entry from cache. */
void deleteSingleSignOnEntry(String ssoId) {
this.dataCache.delete(ssoId);
}
}

View File

@ -0,0 +1,263 @@
package tomcat.request.session.redis;
import org.apache.catalina.Container;
import org.apache.catalina.Context;
import org.apache.catalina.Engine;
import org.apache.catalina.LifecycleException;
import org.apache.catalina.Manager;
import org.apache.catalina.Realm;
import org.apache.catalina.Session;
import org.apache.catalina.authenticator.Constants;
import org.apache.catalina.authenticator.SingleSignOn;
import org.apache.catalina.authenticator.SingleSignOnSessionKey;
import org.apache.catalina.connector.Request;
import org.apache.catalina.connector.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tomcat.request.session.exception.BackendException;
import tomcat.request.session.model.SingleSignOnEntry;
import javax.servlet.ServletException;
import javax.servlet.http.Cookie;
import java.io.IOException;
import java.security.Principal;
import java.util.Set;
/** author: Ranjith Manickam @ 20 Mar' 2020 */
public class SingleSignOnValve extends SingleSignOn {
private static final Logger LOGGER = LoggerFactory.getLogger(SingleSignOnValve.class);
private Engine engine;
private SessionManager manager;
/** {@inheritDoc} */
@Override
protected synchronized void startInternal() throws LifecycleException {
Container c;
for (c = this.getContainer(); c != null && !(c instanceof Engine); c = c.getParent()) {
}
if (c instanceof Engine) {
this.engine = (Engine) c;
}
super.startInternal();
}
/** {@inheritDoc} */
@Override
public void invoke(Request request, Response response) throws BackendException {
try {
this.setSessionManager(request.getContext().getManager());
request.removeNote("org.apache.catalina.request.SSOID");
LOGGER.debug("singleSignOn.debug.invoke, requestURI: {}", request.getRequestURI());
if (request.getUserPrincipal() == null) {
LOGGER.debug("singleSignOn.debug.cookieCheck");
Cookie cookie = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie value : cookies) {
if (Constants.SINGLE_SIGN_ON_COOKIE.equals(value.getName())) {
cookie = value;
break;
}
}
}
if (cookie == null) {
LOGGER.debug("singleSignOn.debug.cookieNotFound");
} else {
LOGGER.debug("singleSignOn.debug.principalCheck, ssoId: {}", cookie.getValue());
SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(cookie.getValue());
if (entry == null) {
LOGGER.debug("singleSignOn.debug.principalNotFound, ssoId: {}", cookie.getValue());
cookie.setValue("REMOVE");
cookie.setMaxAge(0);
cookie.setPath("/");
String domain = this.getCookieDomain();
if (domain != null) {
cookie.setDomain(domain);
}
cookie.setSecure(request.isSecure());
if (request.getServletContext().getSessionCookieConfig().isHttpOnly() || request.getContext().getUseHttpOnly()) {
cookie.setHttpOnly(true);
}
response.addCookie(cookie);
} else {
LOGGER.debug("singleSignOn.debug.principalFound, principal: {}, authType: {}", (entry.getPrincipal() != null ? entry.getPrincipal().getName() : ""), entry.getAuthType());
request.setNote("org.apache.catalina.request.SSOID", cookie.getValue());
if (!this.getRequireReauthentication()) {
request.setAuthType(entry.getAuthType());
request.setUserPrincipal(entry.getPrincipal());
}
}
}
} else {
LOGGER.debug("singleSignOn.debug.hasPrincipal, principal: {}", request.getUserPrincipal().getName());
}
this.getNext().invoke(request, response);
} catch (IOException | ServletException | RuntimeException ex) {
LOGGER.error("Error processing request", ex);
throw new BackendException();
}
}
/** {@inheritDoc} */
@Override
public void sessionDestroyed(String ssoId, Session session) {
if (this.getState().isAvailable()) {
if ((session.getMaxInactiveInterval() <= 0 ||
session.getIdleTimeInternal() < (long) (session.getMaxInactiveInterval() * 1000))
&& session.getManager().getContext().getState().isAvailable()) {
LOGGER.debug("singleSignOn.debug.sessionLogout, session: {}", session);
this.removeSession(ssoId, session);
if (this.manager.singleSignOnEntryExists(ssoId)) {
this.deregister(ssoId);
}
return;
}
LOGGER.debug("singleSignOn.debug.sessionTimeout, ssoId: {}, session: {}", ssoId, session);
this.removeSession(ssoId, session);
}
}
/** {@inheritDoc} */
@Override
protected boolean associate(String ssoId, Session session) {
SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(ssoId);
if (entry == null) {
LOGGER.debug("singleSignOn.debug.associateFail, ssoId: {}, session: {}", ssoId, session);
return false;
}
LOGGER.debug("singleSignOn.debug.associate, ssoId: {}, session: {}", ssoId, session);
entry.addSession(ssoId, session);
this.manager.setSingleSignOnEntry(ssoId, entry);
return true;
}
/** {@inheritDoc} */
@Override
protected void deregister(String ssoId) {
SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(ssoId);
this.manager.deleteSingleSignOnEntry(ssoId);
if (entry == null) {
LOGGER.debug("singleSignOn.debug.deregisterFail, ssoId: {}", ssoId);
return;
}
Set<SingleSignOnSessionKey> ssoKeys = entry.findSessions();
if (ssoKeys.isEmpty()) {
LOGGER.debug("singleSignOn.debug.deregisterNone, ssoId: {}", ssoId);
}
for (SingleSignOnSessionKey ssoKey : ssoKeys) {
this.expire(ssoKey);
LOGGER.debug("singleSignOn.debug.deregister, ssoKey: {}, ssoId: {}", ssoKey, ssoId);
}
}
/** {@inheritDoc} */
@Override
protected boolean reauthenticate(String ssoId, Realm realm, Request request) {
if (ssoId == null || realm == null) {
return false;
}
boolean reAuthenticated = false;
SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(ssoId);
if (entry != null && entry.getCanReauthenticate()) {
String username = entry.getUsername();
if (username != null) {
Principal reAuthPrincipal = realm.authenticate(username, entry.getPassword());
if (reAuthPrincipal != null) {
reAuthenticated = true;
request.setAuthType(entry.getAuthType());
request.setUserPrincipal(reAuthPrincipal);
}
}
}
return reAuthenticated;
}
/** {@inheritDoc} */
@Override
protected void register(String ssoId, Principal principal, String authType, String username, String password) {
LOGGER.debug("singleSignOn.debug.register, ssoId: {}, principal: {}, authType: {}", ssoId, (principal != null ? principal.getName() : ""), authType);
SingleSignOnEntry entry = new SingleSignOnEntry(principal, authType, username, password);
this.manager.setSingleSignOnEntry(ssoId, entry);
}
/** {@inheritDoc} */
@Override
protected boolean update(String ssoId, Principal principal, String authType, String username, String password) {
SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(ssoId);
if (entry == null || !entry.getCanReauthenticate()) {
return false;
}
LOGGER.debug("singleSignOn.debug.update, ssoId: {}, authType: {}", ssoId, authType);
entry.updateCredentials(principal, authType, username, password);
this.manager.setSingleSignOnEntry(ssoId, entry);
return true;
}
/** {@inheritDoc} */
@Override
protected void removeSession(String ssoId, Session session) {
LOGGER.debug("singleSignOn.debug.removeSession, ssoId: {}, session: {}", ssoId, session);
SingleSignOnEntry entry = this.manager.getSingleSignOnEntry(ssoId);
if (entry != null) {
entry.removeSession(session);
if (entry.findSessions().size() == 0) {
this.deregister(ssoId);
}
}
}
/** To set session manager. */
void setSessionManager(Manager manager) {
if (manager != null) {
this.manager = (SessionManager) manager;
}
}
/** To expire session. */
private void expire(SingleSignOnSessionKey key) {
if (this.engine == null) {
LOGGER.warn("singleSignOn.sessionExpire.engineNull, key: {}", key);
} else {
Container host = this.engine.findChild(key.getHostName());
if (host == null) {
LOGGER.warn("singleSignOn.sessionExpire.hostNotFound, key: {}", key);
} else {
Context context = (Context) host.findChild(key.getContextName());
if (context == null) {
LOGGER.warn("singleSignOn.sessionExpire.contextNotFound, key: {}", key);
} else {
Session session;
try {
session = this.manager.findSession(key.getSessionId());
} catch (IOException ex) {
LOGGER.warn("singleSignOn.sessionExpire.managerError, key: {}, exception: {}", key, ex);
return;
}
if (session == null) {
LOGGER.warn("singleSignOn.sessionExpire.sessionNotFound, key: {}", key);
} else {
session.expire();
}
}
}
}
}
}

View File

@ -0,0 +1,154 @@
package tomcat.request.session.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import tomcat.request.session.annotation.Property;
import tomcat.request.session.model.Config;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Field;
import java.util.Properties;
/** author: Ranjith Manickam @ 5 Feb' 2020 */
public class ConfigUtil {
private static final Logger LOGGER = LoggerFactory.getLogger(ConfigUtil.class);
private static final String CONF = "conf";
private static final String CATALINA_BASE = "catalina.base";
/** To get application config. */
public static Config getConfig() {
Properties properties = getApplicationProperties();
Config config = new Config();
for (Field field : Config.class.getDeclaredFields()) {
Property property = field.getAnnotation(Property.class);
if (property == null) {
continue;
}
String propertyName = property.name();
Property.PropertyType propertyType = property.type();
if (propertyName.isEmpty()) {
continue;
}
String value = properties.getProperty(propertyName);
if (isSystemProperty(value)) {
value = getSystemProperty(value);
}
if (value == null || value.isEmpty()) {
value = property.defaultValue();
if (value.isEmpty()) {
continue;
}
}
field.setAccessible(true);
try {
switch (propertyType) {
case BOOLEAN:
field.set(config, Boolean.parseBoolean(value));
break;
case INTEGER:
field.set(config, Integer.parseInt(value));
break;
case LONG:
field.set(config, Long.parseLong(value));
break;
case STRING:
default:
field.set(config, value);
break;
}
} catch (Exception ex) {
LOGGER.error("Error while initializing application properties", ex);
}
}
return config;
}
/** To get redis data cache properties. */
private static Properties getApplicationProperties() {
Properties properties = new Properties();
try {
String filePath = System.getProperty(CATALINA_BASE)
.concat(File.separator)
.concat(CONF).concat(File.separator)
.concat(Config.APPLICATION_PROPERTIES_FILE);
InputStream resourceStream = null;
try {
resourceStream = (!filePath.isEmpty() && new File(filePath).exists()) ? new FileInputStream(filePath) : null;
if (resourceStream == null) {
LOGGER.info("Initializing tomcat redis session manager with default properties");
ClassLoader loader = Thread.currentThread().getContextClassLoader();
resourceStream = loader.getResourceAsStream(Config.APPLICATION_PROPERTIES_FILE);
}
properties.load(resourceStream);
} finally {
if (resourceStream != null) {
resourceStream.close();
}
}
} catch (IOException ex) {
LOGGER.error("Error while retrieving application properties", ex);
}
return properties;
}
/**
* To get property with the specified key from system property.
*
* @param key - search key.
* @return - Returns the system property value.
*/
private static String getSystemProperty(String key) {
int fromIndex = 0;
while (true) {
int beginIndex = key.indexOf("${", fromIndex);
int endIndex = key.indexOf("}", fromIndex);
if (beginIndex < 0 || endIndex < 0) {
break;
}
String expression = key.substring(beginIndex + 2, endIndex);
String value = System.getProperty(expression);
if (value == null || value.isEmpty()) {
fromIndex = endIndex + 1;
continue;
}
key = key.replace(String.format("${%s}", expression), value);
}
return key;
}
/**
* To check if the value is from system property.
*
* @param key - search key.
* @return - Returns true if the key start with '${' and ends with '}'.
*/
private static boolean isSystemProperty(String key) {
if (key == null || key.isEmpty()) {
return false;
}
int beginIndex = key.indexOf("${");
int endIndex = key.indexOf("}");
return beginIndex >= 0 && endIndex >= 0 && beginIndex < endIndex;
}
}

View File

@ -1,6 +1,9 @@
package tomcat.request.session;
package tomcat.request.session.util;
import org.apache.catalina.util.CustomObjectInputStream;
import tomcat.request.session.model.Session;
import tomcat.request.session.model.SessionMetadata;
import tomcat.request.session.model.SingleSignOnEntry;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
@ -74,4 +77,25 @@ public class SerializationUtil {
session.readObjectData(ois);
}
}
/** To serialize single-sign-on entry. */
public byte[] serializeSingleSignOnEntry(SingleSignOnEntry entry) throws IOException {
byte[] serialized;
try (ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectOutputStream oos = new ObjectOutputStream(new BufferedOutputStream(bos))) {
entry.writeObjectData(oos);
oos.flush();
serialized = bos.toByteArray();
}
return serialized;
}
/** To de-serialize single-sign-on entry. */
public void deserializeSingleSignOnEntry(byte[] data, SingleSignOnEntry entry)
throws IOException, ClassNotFoundException {
try (BufferedInputStream bis = new BufferedInputStream(new ByteArrayInputStream(data));
ObjectInputStream ois = new CustomObjectInputStream(bis, this.loader)) {
entry.readObjectData(ois);
}
}
}

View File

@ -0,0 +1,33 @@
<!DOCTYPE html>
<html>
<head>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
</head>
<body>
<div id="download_summary"></div>
<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.10.2/jquery.min.js"></script>
<script>
$( document ).ready(function() {
$.ajax({
type: "GET",
dataType: "jsonp",
url: "https://api.github.com/repos/ran-jit/tomcat-cluster-redis-session-manager/releases",
success: function(data){
let content = "<table border='1px' style='width: 80%;margin-left: 10%;margin-right: 10%;line-height: 1.5;'><tr><th style='width: 30%;'>tag</th><th>asset name & download count</th></tr>";
for(let i=0; i< data.data.length; i++) {
const tag = data.data[i];
for(var j=0; j< tag.assets.length; j++) {
const asset = tag.assets[j];
content = content + "<tr><td style='width: 10%;'>"+ tag.tag_name + "</td>";
content = content + "<td>" + asset.name + "<div style='text-align:right;'>" + asset.download_count + "</div></td></tr>";
}
}
content = content + "</table>";
document.getElementById("download_summary").innerHTML=content;
}
});
});
</script>
</body>
</html>

View File

@ -1,5 +1,7 @@
#-- Redis data-cache configuration
# - ${ENV_VARIABLE_NAME}
#- redis hosts. ex: 127.0.0.1:6379, 127.0.0.2:6379, 127.0.0.2:6380, ....
redis.hosts=127.0.0.1:6379
@ -31,4 +33,7 @@ lb.sticky-session.enabled=false
# policies - DEFAULT, SAVE_ON_CHANGE, ALWAYS_SAVE_AFTER_REQUEST
# 1. SAVE_ON_CHANGE: every time session.setAttribute() or session.removeAttribute() is called the session will be saved.
# 2. ALWAYS_SAVE_AFTER_REQUEST: force saving after every request, regardless of whether or not the manager has detected changes to the session.
session.persistent.policies=DEFAULT
session.persistent.policies=DEFAULT
#- single-sign-on session timeout. (default value: 0 ms (-no expiry))
redis.sso.timeout=0