From e670a1903a5127ab533e11a850a69b2e71465f8d Mon Sep 17 00:00:00 2001 From: Gary Roybal Date: Mon, 16 Oct 2023 18:43:48 -0700 Subject: [PATCH] feat: add new 'uPortal-session' submodule which adds support for session clustering/replication/failover with Redis --- docs/CHANGES.md | 4 + gradle.properties | 1 + settings.gradle | 1 + .../portal/portlet/registry/SubscribeKey.java | 7 +- .../portal/url/RequireValidSessionFilter.java | 4 +- uPortal-session/README.md | 69 +++++++++ uPortal-session/build.gradle | 13 ++ .../session/PortalSessionConstants.java | 29 ++++ .../session/redis/RedisSessionConfig.java | 127 +++++++++++++++++ .../redis/RedisSessionInitializer.java | 54 +++++++ .../SpringSessionRedisEnabledCondition.java | 43 ++++++ .../redis/RedisSessionInitializerTest.java | 133 ++++++++++++++++++ uPortal-webapp/build.gradle | 1 + .../portal/PortalWebAppInitializer.java | 41 ++++++ .../contexts/applicationContext.xml | 1 + .../properties/contexts/sessionContext.xml | 31 ++++ .../src/main/webapp/WEB-INF/web.xml | 12 -- 17 files changed, 556 insertions(+), 15 deletions(-) create mode 100644 uPortal-session/README.md create mode 100644 uPortal-session/build.gradle create mode 100644 uPortal-session/src/main/java/org/apereo/portal/session/PortalSessionConstants.java create mode 100644 uPortal-session/src/main/java/org/apereo/portal/session/redis/RedisSessionConfig.java create mode 100644 uPortal-session/src/main/java/org/apereo/portal/session/redis/RedisSessionInitializer.java create mode 100644 uPortal-session/src/main/java/org/apereo/portal/session/redis/SpringSessionRedisEnabledCondition.java create mode 100644 uPortal-session/src/test/java/org/apereo/portal/session/redis/RedisSessionInitializerTest.java create mode 100644 uPortal-webapp/src/main/java/org/apereo/portal/PortalWebAppInitializer.java create mode 100644 uPortal-webapp/src/main/resources/properties/contexts/sessionContext.xml diff --git a/docs/CHANGES.md b/docs/CHANGES.md index 4dfe4c79146..49413db565a 100644 --- a/docs/CHANGES.md +++ b/docs/CHANGES.md @@ -1,5 +1,9 @@ ## Next Release +## v5.15.2 + +- new external storage option (Redis) for sessions; see `README.md` under new uPortal-session submodule + ## v5.15.0 - new cache, org.apereo.portal.i18n.RDBMLocaleStore.userLocales in `ehcache.xml`/`ehcache-no-jgroups.xml` diff --git a/gradle.properties b/gradle.properties index cb6fa4b49b0..e865c16a9c4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -97,6 +97,7 @@ plutoVersion=2.1.0-M3 resourceServerVersion=1.3.1 slf4jVersion=1.7.36 springVersion=4.3.30.RELEASE +springSessionVersion=1.3.5.RELEASE spockVersion=2.1-groovy-3.0 springfoxSwaggerVersion=2.9.2 springLdapVersion=2.3.4.RELEASE diff --git a/settings.gradle b/settings.gradle index 279a84a7d95..64ae3b90527 100644 --- a/settings.gradle +++ b/settings.gradle @@ -10,6 +10,7 @@ include 'uPortal-persondir' include 'uPortal-portlets' include 'uPortal-rendering' include 'uPortal-rdbm' +include 'uPortal-session' include 'uPortal-spring' include 'uPortal-tenants' include 'uPortal-tools' diff --git a/uPortal-content/uPortal-content-portlet/src/main/java/org/apereo/portal/portlet/registry/SubscribeKey.java b/uPortal-content/uPortal-content-portlet/src/main/java/org/apereo/portal/portlet/registry/SubscribeKey.java index 74816ec1608..8790a7560f9 100644 --- a/uPortal-content/uPortal-content-portlet/src/main/java/org/apereo/portal/portlet/registry/SubscribeKey.java +++ b/uPortal-content/uPortal-content-portlet/src/main/java/org/apereo/portal/portlet/registry/SubscribeKey.java @@ -14,7 +14,12 @@ */ package org.apereo.portal.portlet.registry; -final class SubscribeKey { +import java.io.Serializable; + +final class SubscribeKey implements Serializable { + + private static final long serialVersionUID = 1L; + private final int userId; private final String layoutNodeId; diff --git a/uPortal-security/uPortal-security-mvc/src/main/java/org/apereo/portal/url/RequireValidSessionFilter.java b/uPortal-security/uPortal-security-mvc/src/main/java/org/apereo/portal/url/RequireValidSessionFilter.java index c611f4fb0a8..30ad3a386aa 100644 --- a/uPortal-security/uPortal-security-mvc/src/main/java/org/apereo/portal/url/RequireValidSessionFilter.java +++ b/uPortal-security/uPortal-security-mvc/src/main/java/org/apereo/portal/url/RequireValidSessionFilter.java @@ -40,8 +40,8 @@ protected boolean shouldNotFilter(HttpServletRequest request) { // (1) You have a valid session (original method) final HttpSession session = request.getSession(false); - if (session != null && !session.isNew()) { - // Session exists and is not new, don't bother filtering + if (session != null) { + // Session exists, don't bother filtering log.debug("User {} has a session: {}", request.getRemoteUser(), session.getId()); log.debug("Max inactive interval: {}", session.getMaxInactiveInterval()); if (log.isDebugEnabled()) { diff --git a/uPortal-session/README.md b/uPortal-session/README.md new file mode 100644 index 00000000000..c2c054c7742 --- /dev/null +++ b/uPortal-session/README.md @@ -0,0 +1,69 @@ +# uPortal Session + +The purpose of this submodule is to allow Spring Session to be optionally used +in order to provide support for web session clustering, replication, and +failover. + +Spring Session provides the capability to store web sessions external to the +Servlet container (Tomcat). While Spring Session supports several storage +options, uPortal Session currently only supports using Redis. + +## Enabling + +The use of Spring Session is optional. By default, it is disabled. In order +to enable it, use the following environment variable or system property with +the value of 'redis': + +- environment variable: ORG_APEREO_PORTAL_SESSION_STORETYPE +- system property: org.apereo.portal.session.storetype + +Note that an application property was not used because at the time of servlet +context initialization, the application properties are not available for use. + +## Redis Connection Config + +### Mode + +There are three modes supported for connecting to Redis: +- cluster +- sentinel +- standalone + +The 'org.apero.portal.session.redis.mode' property should be set to one of these values. +If the property is not found, then the value of 'standalone' will be used by +default. + +#### Cluster + +When using cluster mode, the following properties should be used: + +- org.apereo.portal.session.redis.cluster.nodes +- org.apereo.portal.session.redis.cluster.maxredirects + +#### Sentinel + +When using sentinel mode, the following properties should be used: + +- org.apereo.portal.session.redis.sentinel.master +- org.apereo.portal.session.redis.sentinel.nodes + +#### Standalone + +When using standalone mode, the following default values will be used: + +- host: 127.0.0.1 +- port: 6379 + +These can be overwritten by using the following properties: + +- org.apereo.portal.session.redis.host +- org.apereo.portal.session.redis.port + +#### Additional Config + +The following properties can optionally be used to additionally configure the +Redis connection: + +- org.apereo.portal.session.redis.timeout +- org.apereo.portal.session.redis.password +- org.apereo.portal.session.redis.database diff --git a/uPortal-session/build.gradle b/uPortal-session/build.gradle new file mode 100644 index 00000000000..76a94cf7820 --- /dev/null +++ b/uPortal-session/build.gradle @@ -0,0 +1,13 @@ +description = "Apereo uPortal Session" + +dependencies { + implementation "org.springframework:spring-web:${springVersion}" + implementation "org.springframework.session:spring-session-data-redis:${springSessionVersion}" + + compileOnly "${servletApiDependency}" + + testImplementation "${servletApiDependency}" + + testRuntimeOnly "org.slf4j:jcl-over-slf4j:${slf4jVersion}" + testRuntimeOnly "org.slf4j:slf4j-api:${slf4jVersion}" +} diff --git a/uPortal-session/src/main/java/org/apereo/portal/session/PortalSessionConstants.java b/uPortal-session/src/main/java/org/apereo/portal/session/PortalSessionConstants.java new file mode 100644 index 00000000000..5c0575fb243 --- /dev/null +++ b/uPortal-session/src/main/java/org/apereo/portal/session/PortalSessionConstants.java @@ -0,0 +1,29 @@ +/** + * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. Apereo + * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the License at the + * following location: + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apereo.portal.session; + +public class PortalSessionConstants { + + private PortalSessionConstants() {} + + public static final String REDIS_STORE_TYPE = "redis"; + public static final String REDIS_STANDALONE_MODE = "standalone"; + public static final String REDIS_SENTINEL_MODE = "sentinel"; + public static final String REDIS_CLUSTER_MODE = "cluster"; + public static final String SESSION_STORE_TYPE_ENV_PROPERTY_NAME = + "ORG_APEREO_PORTAL_SESSION_STORETYPE"; + public static final String SESSION_STORE_TYPE_SYSTEM_PROPERTY_NAME = + "org.apereo.portal.session.storetype"; +} diff --git a/uPortal-session/src/main/java/org/apereo/portal/session/redis/RedisSessionConfig.java b/uPortal-session/src/main/java/org/apereo/portal/session/redis/RedisSessionConfig.java new file mode 100644 index 00000000000..4c5ee141905 --- /dev/null +++ b/uPortal-session/src/main/java/org/apereo/portal/session/redis/RedisSessionConfig.java @@ -0,0 +1,127 @@ +/** + * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. Apereo + * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the License at the + * following location: + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apereo.portal.session.redis; + +import static org.apereo.portal.session.PortalSessionConstants.REDIS_CLUSTER_MODE; +import static org.apereo.portal.session.PortalSessionConstants.REDIS_SENTINEL_MODE; +import static org.apereo.portal.session.PortalSessionConstants.REDIS_STANDALONE_MODE; + +import java.util.Arrays; +import java.util.List; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.connection.RedisClusterConfiguration; +import org.springframework.data.redis.connection.RedisConnectionFactory; +import org.springframework.data.redis.connection.RedisSentinelConfiguration; +import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; +import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; + +@Configuration +@Conditional(SpringSessionRedisEnabledCondition.class) +@EnableRedisHttpSession +public class RedisSessionConfig { + + @Value("${org.apereo.portal.session.redis.mode:standalone}") + private String redisMode; + + @Value("${org.apereo.portal.session.redis.host:#{null}}") + private String redisHost; + + @Value("${org.apereo.portal.session.redis.port:#{null}}") + private Integer redisPort; + + @Value("${org.apereo.portal.session.redis.cluster.nodes:#{null}}") + private String redisClusterNodes; + + @Value("${org.apereo.portal.session.redis.cluster.maxredirects:#{null}}") + private Integer redisClusterMaxRedirects; + + @Value("${org.apereo.portal.session.redis.sentinel.master:#{null}}") + private String redisSentinelMaster; + + @Value("${org.apereo.portal.session.redis.sentinel.nodes:#{null}}") + private String redisSentinelNodes; + + @Value("${org.apereo.portal.session.redis.database:#{null}}") + private Integer redisDatabase; + + @Value("${org.apereo.portal.session.redis.password:#{null}}") + private String redisPassword; + + @Value("${org.apereo.portal.session.redis.timeout:#{null}}") + private Integer redisTimeout; + + @Bean + public RedisConnectionFactory redisConnectionFactory() { + JedisConnectionFactory result; + if (REDIS_CLUSTER_MODE.equalsIgnoreCase(this.redisMode)) { + result = this.createBaseClusterConnectionFactory(); + } else if (REDIS_SENTINEL_MODE.equalsIgnoreCase(this.redisMode)) { + result = this.createBaseSentinelConnectionFactory(); + } else if (REDIS_STANDALONE_MODE.equalsIgnoreCase(this.redisMode)) { + result = this.createBaseStandaloneConnectionFactory(); + } else { + throw new IllegalArgumentException( + "Invalid value for org.apereo.portal.session.redis.mode: " + this.redisMode); + } + this.setAdditionalProperties(result); + return result; + } + + private JedisConnectionFactory createBaseClusterConnectionFactory() { + List nodesList = Arrays.asList(this.redisClusterNodes.split(",")); + RedisClusterConfiguration clusterConfig = new RedisClusterConfiguration(nodesList); + if (this.redisClusterMaxRedirects != null) { + clusterConfig.setMaxRedirects(this.redisClusterMaxRedirects); + } + return new JedisConnectionFactory(clusterConfig); + } + + private JedisConnectionFactory createBaseSentinelConnectionFactory() { + RedisSentinelConfiguration sentinelConfig = new RedisSentinelConfiguration(); + sentinelConfig.master(this.redisSentinelMaster); + String[] nodesArray = this.redisSentinelNodes.split(","); + for (String node : nodesArray) { + String[] hostAndPort = node.split(":"); + sentinelConfig.sentinel(hostAndPort[0], Integer.parseInt(hostAndPort[1])); + } + return new JedisConnectionFactory(sentinelConfig); + } + + private JedisConnectionFactory createBaseStandaloneConnectionFactory() { + final JedisConnectionFactory result = new JedisConnectionFactory(); + if (this.redisHost != null) { + result.setHostName(this.redisHost); + } + if (this.redisPort != null) { + result.setPort(this.redisPort); + } + return result; + } + + private void setAdditionalProperties(JedisConnectionFactory factory) { + if (this.redisDatabase != null) { + factory.setDatabase(this.redisDatabase); + } + if (this.redisPassword != null) { + factory.setPassword(this.redisPassword); + } + if (this.redisTimeout != null) { + factory.setTimeout(this.redisTimeout); + } + } +} diff --git a/uPortal-session/src/main/java/org/apereo/portal/session/redis/RedisSessionInitializer.java b/uPortal-session/src/main/java/org/apereo/portal/session/redis/RedisSessionInitializer.java new file mode 100644 index 00000000000..cbd9ecc2f13 --- /dev/null +++ b/uPortal-session/src/main/java/org/apereo/portal/session/redis/RedisSessionInitializer.java @@ -0,0 +1,54 @@ +/** + * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. Apereo + * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the License at the + * following location: + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apereo.portal.session.redis; + +import static org.apereo.portal.session.PortalSessionConstants.REDIS_STORE_TYPE; +import static org.apereo.portal.session.PortalSessionConstants.SESSION_STORE_TYPE_ENV_PROPERTY_NAME; +import static org.apereo.portal.session.PortalSessionConstants.SESSION_STORE_TYPE_SYSTEM_PROPERTY_NAME; + +import org.springframework.session.web.context.AbstractHttpSessionApplicationInitializer; + +/** + * This class is needed to enable Spring Session Redis support in uPortal. It registers the filter + * that is needed by Spring Session to manage the session with Redis. It also ensures that the + * filter is only registered if the session store-type is configured for Redis. The filter could + * have instead been added to web.xml, but that would not have allowed for the feature to be + * enabled/disabled via configuration. Note that the application properties are not available during + * initialization, and therefore we instead check for an environment variable or system property. + */ +public class RedisSessionInitializer extends AbstractHttpSessionApplicationInitializer { + + public RedisSessionInitializer() { + // MUST pass null here to avoid having Spring Session create a root WebApplicationContext + // that does not work with the current uPortal setup. + super((Class[]) null); + } + + @Override + public void onStartup(javax.servlet.ServletContext servletContext) + throws javax.servlet.ServletException { + if (REDIS_STORE_TYPE.equals(this.getStoreTypeConfiguredValue())) { + super.onStartup(servletContext); + } + } + + private String getStoreTypeConfiguredValue() { + String result = System.getProperty(SESSION_STORE_TYPE_SYSTEM_PROPERTY_NAME); + if (result == null) { + result = System.getenv(SESSION_STORE_TYPE_ENV_PROPERTY_NAME); + } + return result; + } +} diff --git a/uPortal-session/src/main/java/org/apereo/portal/session/redis/SpringSessionRedisEnabledCondition.java b/uPortal-session/src/main/java/org/apereo/portal/session/redis/SpringSessionRedisEnabledCondition.java new file mode 100644 index 00000000000..152ff50ba30 --- /dev/null +++ b/uPortal-session/src/main/java/org/apereo/portal/session/redis/SpringSessionRedisEnabledCondition.java @@ -0,0 +1,43 @@ +/** + * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. Apereo + * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the License at the + * following location: + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apereo.portal.session.redis; + +import static org.apereo.portal.session.PortalSessionConstants.REDIS_STORE_TYPE; +import static org.apereo.portal.session.PortalSessionConstants.SESSION_STORE_TYPE_ENV_PROPERTY_NAME; +import static org.apereo.portal.session.PortalSessionConstants.SESSION_STORE_TYPE_SYSTEM_PROPERTY_NAME; + +import org.springframework.context.annotation.Condition; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; + +public class SpringSessionRedisEnabledCondition implements Condition { + + @Override + public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { + return REDIS_STORE_TYPE.equals(this.getSessionStoreTypeValue(context)); + } + + private String getSessionStoreTypeValue(ConditionContext context) { + String result = + context.getEnvironment() + .getProperty(SESSION_STORE_TYPE_SYSTEM_PROPERTY_NAME, String.class, null); + if (result == null) { + result = + context.getEnvironment() + .getProperty(SESSION_STORE_TYPE_ENV_PROPERTY_NAME, String.class, null); + } + return result; + } +} diff --git a/uPortal-session/src/test/java/org/apereo/portal/session/redis/RedisSessionInitializerTest.java b/uPortal-session/src/test/java/org/apereo/portal/session/redis/RedisSessionInitializerTest.java new file mode 100644 index 00000000000..1d274e16934 --- /dev/null +++ b/uPortal-session/src/test/java/org/apereo/portal/session/redis/RedisSessionInitializerTest.java @@ -0,0 +1,133 @@ +/** + * Licensed to Apereo under one or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information regarding copyright ownership. Apereo + * licenses this file to you under the Apache License, Version 2.0 (the "License"); you may not use + * this file except in compliance with the License. You may obtain a copy of the License at the + * following location: + * + *

http://www.apache.org/licenses/LICENSE-2.0 + * + *

Unless required by applicable law or agreed to in writing, software distributed under the + * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apereo.portal.session.redis; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.lang.reflect.Field; +import java.util.Map; +import javax.servlet.Filter; +import javax.servlet.FilterRegistration.Dynamic; +import javax.servlet.ServletContext; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +public class RedisSessionInitializerTest { + + private RedisSessionInitializer redisSessionInitializer; + + @Mock private ServletContext servletContext; + @Mock private Dynamic FilterRegistration; + + @Before + public void setUp() { + MockitoAnnotations.openMocks(this); + when(this.servletContext.addFilter(anyString(), any(Filter.class))) + .thenReturn(this.FilterRegistration); + this.redisSessionInitializer = new RedisSessionInitializer(); + } + + @Test + public void testOnStartupAddsFilterWhenStoreTypeSystemPropertySetToRedis() throws Exception { + try { + System.setProperty("org.apereo.portal.session.storetype", "redis"); + this.redisSessionInitializer.onStartup(this.servletContext); + + verify(this.servletContext, times(1)).addFilter(anyString(), any(Filter.class)); + } finally { + System.clearProperty("org.apereo.portal.session.storetype"); + } + } + + @Test + public void + testOnStartupDoesNotAddFilterWhenStoreTypeEnvironmentVariableAndSystemPropertyNotFound() + throws Exception { + this.redisSessionInitializer.onStartup(this.servletContext); + verify(this.servletContext, never()).addFilter(anyString(), any(Filter.class)); + } + + @Test + public void testOnStartupDoesNotAddFilterWhenStoreTypeSystemPropertyValueIsNotRedis() + throws Exception { + try { + System.setProperty("org.apereo.portal.session.storetype", "rdbms"); + this.redisSessionInitializer.onStartup(this.servletContext); + verify(this.servletContext, never()).addFilter(anyString(), any(Filter.class)); + } finally { + System.clearProperty("org.apereo.portal.session.storetype"); + } + } + + @Test + public void testOnStartupAddsFilterWhenStoreTypeEnvironmentVariableSetToRedis() + throws Exception { + try { + this.setEnvironmentVariableValue("ORG_APEREO_PORTAL_SESSION_STORETYPE", "redis"); + this.redisSessionInitializer.onStartup(this.servletContext); + + verify(this.servletContext, times(1)).addFilter(anyString(), any(Filter.class)); + } finally { + this.setEnvironmentVariableValue("ORG_APEREO_PORTAL_SESSION_STORETYPE", ""); + } + } + + @Test + public void testOnStartupDoesNotAddFilterWhenStoreTypeEnvironmentVariableValueIsNotRedis() + throws Exception { + try { + this.setEnvironmentVariableValue("ORG_APEREO_PORTAL_SESSION_STORETYPE", "foo"); + this.redisSessionInitializer.onStartup(this.servletContext); + verify(this.servletContext, never()).addFilter(anyString(), any(Filter.class)); + } finally { + this.setEnvironmentVariableValue("ORG_APEREO_PORTAL_SESSION_STORETYPE", ""); + } + } + + @Test + public void testOnStartupHasSystemPropertyTakePresidenceOverEnvironmentVariable() + throws Exception { + try { + this.setEnvironmentVariableValue("ORG_APEREO_PORTAL_SESSION_STORETYPE", "redis"); + System.setProperty("org.apereo.portal.session.storetype", "foo"); + this.redisSessionInitializer.onStartup(this.servletContext); + verify(this.servletContext, never()).addFilter(anyString(), any(Filter.class)); + } finally { + this.setEnvironmentVariableValue("ORG_APEREO_PORTAL_SESSION_STORETYPE", ""); + System.clearProperty("org.apereo.portal.session.storetype"); + } + } + + @SuppressWarnings({"unchecked"}) + private void setEnvironmentVariableValue(String key, String value) { + try { + Map env = System.getenv(); + Class cl = env.getClass(); + Field field = cl.getDeclaredField("m"); + field.setAccessible(true); + Map writableEnv = (Map) field.get(env); + writableEnv.put(key, value); + } catch (Exception e) { + throw new IllegalStateException("Failed to set environment variable", e); + } + } +} diff --git a/uPortal-webapp/build.gradle b/uPortal-webapp/build.gradle index 7529684ed0a..8f39085856c 100644 --- a/uPortal-webapp/build.gradle +++ b/uPortal-webapp/build.gradle @@ -39,6 +39,7 @@ dependencies { api project(':uPortal-security:uPortal-security-authn') api project(':uPortal-security:uPortal-security-xslt') api project(':uPortal-security:uPortal-security-filters') + api project(':uPortal-session') api project(':uPortal-soffit:uPortal-soffit-connector') api project(':uPortal-utils:uPortal-utils-jmx') api project(':uPortal-utils:uPortal-utils-url') diff --git a/uPortal-webapp/src/main/java/org/apereo/portal/PortalWebAppInitializer.java b/uPortal-webapp/src/main/java/org/apereo/portal/PortalWebAppInitializer.java new file mode 100644 index 00000000000..5053c795d90 --- /dev/null +++ b/uPortal-webapp/src/main/java/org/apereo/portal/PortalWebAppInitializer.java @@ -0,0 +1,41 @@ +package org.apereo.portal; + +import org.springframework.web.context.AbstractContextLoaderInitializer; +import org.springframework.web.context.WebApplicationContext; +import org.springframework.web.context.support.XmlWebApplicationContext; + +/** + * This class does the following: 1. creates the root application context using the specified config + * locations 2. initializes the context loader + * + *

This replaces the the following, which were previously defined in web.xml file. + * + *

{@code
+ * 
+ *     contextConfigLocation
+ *     classpath:/properties/contexts/*.xml,classpath:/properties/contextOverrides/*.xml
+ * 
+ *
+ * 
+ * 
+ *     org.springframework.web.context.ContextLoaderListener
+ * 
+ *
+ * }
+ * + * This new approach allows us to dynamically update the servlet context programatically with + * Spring, which was needed in order support Spring Session handling as a feature that could be + * enabled/disabled with configuration. + */ +public class PortalWebAppInitializer extends AbstractContextLoaderInitializer { + + @Override + protected WebApplicationContext createRootApplicationContext() { + XmlWebApplicationContext context = new XmlWebApplicationContext(); + context.setConfigLocation( + "classpath:/properties/contexts/*.xml,classpath:/properties/contextOverrides/*.xml"); + return context; + } +} diff --git a/uPortal-webapp/src/main/resources/properties/contexts/applicationContext.xml b/uPortal-webapp/src/main/resources/properties/contexts/applicationContext.xml index 1e418b66b06..a90eb9f6064 100644 --- a/uPortal-webapp/src/main/resources/properties/contexts/applicationContext.xml +++ b/uPortal-webapp/src/main/resources/properties/contexts/applicationContext.xml @@ -53,6 +53,7 @@ + diff --git a/uPortal-webapp/src/main/resources/properties/contexts/sessionContext.xml b/uPortal-webapp/src/main/resources/properties/contexts/sessionContext.xml new file mode 100644 index 00000000000..f6bab467de2 --- /dev/null +++ b/uPortal-webapp/src/main/resources/properties/contexts/sessionContext.xml @@ -0,0 +1,31 @@ + + + + + + + + diff --git a/uPortal-webapp/src/main/webapp/WEB-INF/web.xml b/uPortal-webapp/src/main/webapp/WEB-INF/web.xml index e27e2c7b4ef..d613e3a66e6 100644 --- a/uPortal-webapp/src/main/webapp/WEB-INF/web.xml +++ b/uPortal-webapp/src/main/webapp/WEB-INF/web.xml @@ -28,11 +28,6 @@ uPortal - - contextConfigLocation - classpath:/properties/contexts/*.xml,classpath:/properties/contextOverrides/*.xml - - ch.qos.logback.classic.selector.servlet.ContextDetachingSCL @@ -45,13 +40,6 @@ org.apereo.portal.utils.PortalApplicationContextLocator - - - org.springframework.web.context.ContextLoaderListener - -