Securing messages with Spring Session and Redis

To summarize, so far we have seen how to broadcast STOMP messages to StockJS clients, how to stack messages in an external multi-protocol broker, and how to interact with this broker (RabbitMQ) in the Spring ecosystem.

Getting ready

This recipe is about implementing dedicated queues, no longer topics (broadcast), so that users can receive real-time updates related to the specific content they are viewing. It is also a demonstration of how SockJS clients can send data to their private queues.

For private queues, we had to secure messages and queue accesses. We have broken down our stateless rule of thumb for the API to make use of Spring Session. This extends the authentication performed by cloudstreetmarket-api and reuses the Spring Security context within cloudstreetmarket-websocket.

How to do it…

Apache HTTP proxy configuration

Because the v8.2.x branch introduced the new cloudstreetmarket-websocket web app, the Apache HTTP proxy configuration needs to be updated to fully support our WebSocket implementation. Our VirtualHost definition is now:

<VirtualHost cloudstreetmarket.com:80>
  ProxyPass        /portal http://localhost:8080/portal
  ProxyPassReverse /portal http://localhost:8080/portal
  ProxyPass        /api  	http://localhost:8080/api
  ProxyPassReverse /api  	http://localhost:8080/api
  ProxyPass        /ws  	http://localhost:8080/ws
  ProxyPassReverse /ws  	http://localhost:8080/ws
  RewriteEngine on
  RewriteCond %{HTTP:UPGRADE} ^WebSocket$ [NC]
  RewriteCond %{HTTP:CONNECTION} ^Upgrade$ [NC]
  RewriteRule .* ws://localhost:8080%{REQUEST_URI} [P]
  RedirectMatch ^/$ /portal/index
</VirtualHost>

Redis server installation

  1. If you are on a Linux-based machine, download the latest stable version (3+) at http://redis.io/download. The format of the archive to download is tar.gz. Follow the instructions on the page to install it (unpackage it, uncompress it, and build it with the make command).

    Once installed, for a quick start, run Redis with:

    $ src/redis-server
  2. If you are on a Windows-based machine, we recommend this repository: https://github.com/ServiceStack/redis-windows. Follow the instructions on the README.md page. Running Microsoft's native port of Redis allows you to run Redis without any other third-party installations.

    To quickly start Redis server, run the following command:

    $ redis-server.exe redis.windows.conf
  3. When Redis is running, you should be able to see the following welcome screen:
    Redis server installation
  4. Update your Tomcat configuration in Eclipse to use the local Tomcat installation. To do so, double-click on your current server (the Servers tab):
    Redis server installation
  5. This should open the configuration panel as follows:
    Redis server installation

    Make sure the Use Tomcat installation radio button is checked.

    Tip

    If the panel is greyed out, right-click on your current server again, then click Add, Remove... Remove the three deployed web apps from your server and right click on the server once more, then click Publish.

  6. Now, download the following jars:
    • jedis-2.5.2.jar: A small Redis Java client library
    • commons-pool2-2.2.jar: The Apache common object pooling library

    You can download them respectively from http://central.maven.org/maven2/redis/clients/jedis/2.5.2/jedis-2.5.2.jar and http://central.maven.org/maven2/org/apache/commons/commons-pool2/2.2/commons-pool2-2.2.jar

    You can also find these jars in the chapter_8/libs directory.

  7. In the chapter_8/libs directory, you will also find the tomcat-redis-session-manager-2.0-tomcat-8.jar archive. Copy the three jars tomcat-redis-session-manager-2.0-tomcat-8.jar, commons-pool2-2.2.jar, and jedis-2.5.2.jar into the lib directory of your local Tomcat installation that Eclipse is referring to. This should be C: omcat8lib or /home/usr/{system.username}/tomcat8/lib if our instructions have been followed in Chapter 1, Setup Routine for an Enterprise Spring Application.
  8. Now in your workspace, open the context.xml file of your Server project.
    Redis server installation
  9. Add the following Valve configuration:
    <Valve asyncSupported="true" className="edu.zipcloud.catalina.session.RedisSessionHandlerValve"/>
    <Manager className="edu.zipcloud.catalina.session.RedisSessionManager"
          host="localhost" 
          port="6379" 
          database="0" 
          maxInactiveInterval="60"/>

MySQL server installation

While creating the new cloudstreetmarket-websocket web app, we have also changed the database engine from HSQLDB to MySQL. Doing so has allowed us to share the database between the api and websocket modules.

  1. The first step for this section is to download and install the MySQL community server from http://dev.mysql.com/downloads/mysql.. Download the generally available release suited to your system. If you are using MS Windows, we recommend installing the installer.
  2. You can follow the installation instructions provided by the MySQL team at http://dev.mysql.com/doc/refman/5.7/en/installing.html.

    We are now going to define a common configuration for schema users and a database name.

  3. Create a root user with the password of your choice.
  4. Create a technical user (with the administrator role) that the application will use. This user needs to be called csm_tech and needs to have the password csmDB1$55:
    MySQL server installation
  5. Start the MySQL Client (the command line tool), as follows:
    • On MS Windows, start the program mysql.exe in the MySQL servers installation directory:MySQL Server 5.6inmysql.exe
    • On Linux or Mac OS, invoke the mysql command from the terminal

    On both platforms, the first step is then to provide the root password chosen earlier.

  6. Create a csm database either with the MySQL workbench or with MySQL client:
    mysql> CREATE DATABASE csm; 
    
  7. Select the csm database as the current database:
    mysql> USE csm;
    
  8. From Eclipse, start the local Tomcat server. Once it has started, you can shut it down again; this step was only to get Hibernate to generate the schema.
  9. We need then to insert the data manually. To do so, execute the following import commands one after the other:
    mysql> csm < <home-directory>cloudstreetmarket-parentcloudstreetmarket-coresrcmain
    esourcesMETA-INFdbcurrency_exchange.sql;
    mysql> csm < <home-directory>cloudstreetmarket-parentcloudstreetmarket-coresrcmain
    esourcesMETA-INFdbinit.sql;
    mysql> csm < <home-directory>cloudstreetmarket-parentcloudstreetmarket-coresrcmain
    esourcesMETA-INFdbstocks.sql;
    mysql> csm < <home-directory>cloudstreetmarket-parentcloudstreetmarket-coresrcmain
    esourcesMETA-INFdbindices.sql;
    

Application-level changes

  1. In cloudstreetmarket-api and cloudstreetmarket-websocket, the following filter has been added to the web.xml files. This filter has to be positioned before the Spring Security chain definition:
    <filter>
      <filter-name>springSessionRepositoryFilter</filter-name>
      <filter-class>
      org.springframework.web.filter.DelegatingFilterProxy
      </filter-class>
      <async-supported>true</async-supported>
    </filter>
    <filter-mapping>
      <filter-name>springSessionRepositoryFilter</filter-name>
      <url-pattern>/*</url-pattern>
    </filter-mapping>
  2. A couple of Maven dependencies have also been added to cloudstreetmarket-api:
    <!-- Spring Session -->
    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session</artifactId>
      <version>1.0.2.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-pool2</artifactId>
      <version>2.2</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.session</groupId>
      <artifactId>spring-session-data-redis</artifactId>
      <version>1.0.2.RELEASE</version>
    </dependency>
    <!-- Spring Security -->
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-messaging</artifactId>
      <version>4.0.2.RELEASE</version>
    </dependency>
      <dependency>
        <groupId>commons-io</groupId>
        <artifactId>commons-io</artifactId>
        <version>2.4</version>
      </dependency>
  3. In cloudstreetmarket-api again, security-config.xml has been updated to reflect the following changes in the Spring Security filter chain:
    <security:http create-session="ifRequired" 
      authentication-manager-ref="authenticationManager" entry-point-ref="authenticationEntryPoint">
     <security:custom-filter ref="basicAuthenticationFilter" after="BASIC_AUTH_FILTER" />
       <security:csrf disabled="true"/>
     <security:intercept-url pattern="/oauth2/**" access="permitAll"/>
     <security:intercept-url pattern="/basic.html" access="hasRole('ROLE_BASIC')"/>
       <security:intercept-url pattern="/**" access="permitAll"/>
     <security:session-management session-authentication-strategy-ref="sas"/>
    </security:http>
    <bean id="sas" class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" />
  4. Also, this same security-config.xml file, as well as the security-config.xml file in cloudstreetmarket-websocket now define three extra beans:
    <bean class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory" p:port="6379"/>
    <bean class="org.springframework.session.data.redis.config.annotation.web.http.RedisHttpSessionConfiguration"/>
    <bean class="edu.zipcloud.cloudstreetmarket.core.util.RootPath CookieHttpSessionStrategy"/>
  5. Care was taken with cloudstreetmarket-webapp not to create sessions. We wanted sessions to be created only in the cloudstreetmarket-api. We have achieved this with adding the following configuration to the web.xml file in cloudstreetmarket-webapp:
    <session-config>
        <session-timeout>1</session-timeout>
        <cookie-config>
            <max-age>0</max-age>
        </cookie-config>
    </session-config>
  6. Regarding Spring Security, cloudstreetmarket-websocket has the following configuration:
    <bean id="securityContextPersistenceFilter" class="org.springframework.security.web.context.SecurityContextPersistenceFilter"/>
    <security:http create-session="never" 
    authentication-manager-ref="authenticationManager" entry-point-ref="authenticationEntryPoint">
      <security:custom-filter ref="securityContextPersistenceFilter" before="FORM_LOGIN_FILTER" />
      <security:csrf disabled="true"/>
      <security:intercept-url pattern="/channels/private/**" access="hasRole('OAUTH2')"/>
      <security:headers>
          <security:frame-options policy="SAMEORIGIN" />
      </security:headers>
    </security:http>
    <security:global-method-security secured-annotations="enabled" pre-post-annotations="enabled" authentication-manager-ref="authenticationManager"/>
  7. Two configuration-beans in cloudstreetmarket-websocket complete the XML configuration:

    The WebSocketConfig bean in edu.zipcloud.cloudstreetmarket.ws.config is defined as follows:

    @EnableScheduling
    @EnableAsync
    @EnableRabbit
    @Configuration
    @EnableWebSocketMessageBroker
    public class WebSocketConfig extends   AbstractSessionWebSocketMessageBrokerConfigurer<Expiring Session> {
      @Override
      protected void configureStompEndpoints(StompEndpointRegistry registry) {
              registry.addEndpoint("/channels/users/broadcast")
              .setAllowedOrigins(protocol.concat(realmName))
              .withSockJS()
              .setClientLibraryUrl( Constants.SOCKJS_CLIENT_LIB);
        
            registry.addEndpoint("/channels/private")
              .setAllowedOrigins(protocol.concat(realmName))
              .withSockJS()
              .setClientLibraryUrl( Constants.SOCKJS_CLIENT_LIB);
      }
    
      @Override
      public void configureMessageBroker(final MessageBrokerRegistry registry) {
            registry.enableStompBrokerRelay("/topic", "/queue");
            registry.setApplicationDestinationPrefixes("/app");
      }
       
      @Override
      public void configureClientInboundChannel(ChannelRegistration registration) {
            registration.taskExecutor() corePoolSize(Runtime.getRuntime().availableProcessors() *4);
      }
    
      @Override
      //Increase number of threads for slow clients
      public void configureClientOutboundChannel( 
         ChannelRegistration registration) {
            registration.taskExecutor().corePoolSize( Runtime.getRuntime().availableProcessors() *4);
      }
      @Override
      public void configureWebSocketTransport(  
        WebSocketTransportRegistration registration) {
            registration.setSendTimeLimit(15*1000) 
              .setSendBufferSizeLimit(512*1024); 
      }
    }

    The WebSocketSecurityConfig bean in edu.zipcloud.cloudstreetmarket.ws.config is defined as follows:

    @Configuration
    public class WebSocketSecurityConfig extends AbstractSecurityWebSocketMessageBrokerConfigurer {
        @Override
        protected void configureInbound( MessageSecurityMetadataSourceRegistry messages) {
        messages.simpMessageDestMatchers("/topic/actions", "/queue/*", "/app/queue/*").permitAll();
        }
        @Override
        protected boolean sameOriginDisabled() {
        return true;
        }
    }
  8. The ActivityFeedWSController class has been copied over to cloudstreetmarket-websocket to broadcast user activities. It still doesn't require any specific role or authentication:
    @RestController
    public class ActivityFeedWSController extends CloudstreetWebSocketWCI{
    
        @MessageMapping("/channels/users/broadcast")
        @SendTo("/topic/actions")
        public UserActivityDTO handle(UserActivityDTO message) throws Exception {
            return message;
        }
        
        @RequestMapping(value="/channels/users/broadcast/info", produces={"application/json"})
        @ResponseBody
        public String info(HttpServletRequest request) {
          return "v0";
        }
    }
  9. One extra controller sends messages (which are up-to-date stocks values) into private queues:
    @RestController
    public class StockProductWSController extends CloudstreetWebSocketWCI<StockProduct>{
    
      @Autowired
      private StockProductServiceOffline stockProductService;
    
      @MessageMapping("/queue/CSM_QUEUE_{queueId}")
      @SendTo("/queue/CSM_QUEUE_{queueId}")
      @PreAuthorize("hasRole('OAUTH2')")
      public List<StockProduct> sendContent(@Payload List<String> tickers, @DestinationVariable("queueId") String queueId) throws Exception {
          String username = extractUserFromQueueId(queueId);
          if(!getPrincipal().getUsername().equals(username)){
            throw new IllegalAccessError("/queue/CSM_QUEUE_"+queueId);
          }
          return stockProductService.gather(username,      tickers.toArray(new String[tickers.size()]));
      }
        
      @RequestMapping(value=PRIVATE_STOCKS_ENDPOINT+"/info", produces={"application/xml", "application/json"})
      @ResponseBody
      @PreAuthorize("hasRole('OAUTH2')")
      public String info(HttpServletRequest request) {
          return "v0";
      }
    
      private static String extractUserFromQueueId(String token){
            Pattern p = Pattern.compile("_[0-9]+$");
            Matcher m = p.matcher(token);
            String sessionNumber = m.find() ? m.group() : "";
            return token.replaceAll(sessionNumber, "");
    	}
    }
  10. On the client side, new WebSockets are initiated from the stock-search screens (stocks result lists). Especially in stock_search.js and stock_search_by_market.js, the following block has been added in order to regularly request data updates for the set of results that is displayed to the authenticated user:
    if(httpAuth.isUserAuthenticated()){
      window.socket = new SockJS('/ws/channels/private');
      window.stompClient = Stomp.over($scope.socket);
      var queueId = httpAuth.generatedQueueId();
    
      window.socket.onclose = function() {
        window.stompClient.disconnect();
      };
      window.stompClient.connect({}, function(frame) {
        var intervalPromise = $interval(function() {
          window.stompClient.send( '/app/queue/CSM_QUEUE_'+queueId, {}, JSON.stringify($scope.tickers)); 
        }, 5000);
        
        $scope.$on(
            "$destroy",
            function( event ) {
              $interval.cancel(intervalPromise);
              window.stompClient.disconnect();
            }
        );
    
      window.stompClient.subscribe('/queue/CSM_QUEUE_'+queueId, function(message){
        var freshStocks = JSON.parse(message.body);
        $scope.stocks.forEach(function(existingStock) {
          //Here we update the currently displayed stocks
        });
         
        $scope.$apply();
        dynStockSearchService.fadeOutAnim(); //CSS animation   
          //(green/red backgrounds…)
         });
        });
    };

The httpAuth.generatedQueueId()function generates a random queue name based on the authenticated username (see http_authorized.js for more details).

RabbitMQ configuration

  1. Open the RabbitMQ WebConsole, select the Admin tab, then select the Policy menu (also accessible from the http://localhost:15672/#/policies URL).
  2. Add the following policy:
    RabbitMQ configuration

This policy (named PRIVATE) applies to all auto-generated queues matching the pattern CSM_QUEUE_*, with an auto-expiration of 24 hours.

The results

  1. Let's have a look ... before starting the Tomcat Server, ensure that:
    • MySQL is running with the loaded data
    • The Redis server is running
    • RabbitMQ is running
    • Apache HTTP has been restarted/reloaded
  2. When all these signals are green, start the Tomcat servers.
  3. Log in to the application with your Yahoo! account, register a new user, and navigate to the screen: Prices and markets | Search by markets. If you target a market that is potentially open at your time, you should be able to notice real-time updates on the result list:
    The results

How it works...

The Redis server

Redis is an open source in-memory data-structure store. Day-after-day, it is becoming increasingly popular as a NoSQL database and as a key-value store.

Its ability to store keys with optional expiration times and with very high availability (over its remarkable cluster) makes it a very solid underlying technology for session manager implementations. This is precisely the use we make of it through Spring Session.

Spring session

Spring Session is a relatively new Spring project, but it is meant to grow up and take a substantial space in the Spring ecosystem, especially with the recent Microservices and IoT trends. The project is managed by Rob Winch at Pivotal inc. As introduced previously, Spring Session provides an API to manage users' sessions from different Spring components.

The most interesting and notable feature of Spring Session is its ability to integrate with the container (Apache Tomcat) to supply a custom implementation of HttpSession.

SessionRepositoryFilter

To make use of a custom HttpSession implementation, Spring Session completely replaces the HttpServletRequest with a custom wrapper (SessionRepositoryRequestWrapper). This operation is performed inside SessionRepositoryFilter, which is the servlet filter that needs to be configured in the web.xml to intercept the request flow (before Spring MVC).

To do its job, the SessionRepositoryFilter must have an HttpSession implementation. At some point, we registered the RedisHttpSessionConfiguration bean. This bean defines a couple of other beans, and among them is a sessionRepository, which is a RedisOperationsSessionRepository.

See how the SessionRepositoryFilter is important for bridging across the application all the performed session operations to the actual engine implementation that will execute those operations.

RedisConnectionFactory

A RedisConnectionFactory implementation is necessary in order to produce a suitable connection to Redis. Selecting a RedisConnectionFactory implementation, we have been following the Spring team's choice which appeared to be the JedisConnectionFactory. This RedisConnectionFactory relies on Jedis (a lightweight Redis Java client). https://github.com/xetorthio/jedis.

CookieHttpSessionStrategy

We have registered an HttpSessionStrategy implementation: RootPathCookieHttpSessionStrategy. This class is a customized version (in our codebase) of the Spring CookieHttpSessionStrategy.

Because we wanted to pass the cookie from cloudstreetmarket-api to cloudstreetmarket-websocket, the cookie path (which is a property of a cookie) needed to be set to the root path (and not the servlet context path). Spring Session 1.1+ should offer a configurable path feature.

https://github.com/spring-projects/spring-session/issues/155

For now, our RootPathCookieHttpSessionStrategy (basically CookieHttpSessionStrategy) produces and expects cookies with a SESSION name:

CookieHttpSessionStrategy

Currently, only cloudstreetmarket-api produces such cookies (the two other web apps have been restricted in their cookie generation capabilities so they don't mess up our sessions).

Spring Data Redis and Spring Session Data Redis

Do you remember our good friend Spring Data JPA? Well now, Spring Data Redis follows a similar purpose but for the Redis NoSQL key-value store:

 

"The Spring Data Redis (framework makes it easy to write Spring applications that use the Redis key value store by eliminating the redundant tasks and boiler plate code required for interacting with the store through Spring's excellent infrastructure support."

 
 --Spring Data Redis reference

Spring Session Data Redis is the Spring module that specifically implements Spring Data Redis for the purpose of Spring Session management.

The Redis Session manager for Tomcat

Apache Tomcat natively provides clustering and session-replication features. However, these features rely on load balancers sticky sessions. Sticky sessions have pros and cons for scalability. As cons, we can remember that sessions can be lost when servers go down. Also the stickiness of sessions can induce a slow loading time when we actually need to respond to a surge of traffic.

We have also been using an open source project from James Coleman that allows a Tomcat servers to store non-sticky sessions in Redis immediately on session creation for potential uses by other Tomcat instances. This open source project can be reached at the following address:

https://github.com/jcoleman/tomcat-redis-session-manager

However, this project doesn't officially support Tomcat 8. Thus, another fork went further in the Tomcat Release process and is closer from the Tomcat 8 requirements:

https://github.com/rmohr/tomcat-redis-session-manager

We forked this repository and provided an adaptation for Tomcat 8 in https://github.com/alex-bretet/tomcat-redis-session-manager.

The tomcat-redis-session-manager-2.0-tomcat-8.jar copied to tomcat/lib comes from this repository.

Note

Tomcat 8 is still recent, and time is required for peripheral tools to follow releases. We don't provide tomcat-redis-session-manager-2.0-tomcat-8.jar for production use.

Viewing/flushing sessions in Redis

In the main installation directory for Redis, an executable for a command line tool (Cli) can be found. This executable can be launched from the command:

$ src/redis-cli

Or:

$ redis-cli.exe

This executable gives access to the Redis console. From there, for example, the KEY * command lists all the active sessions:

127.0.0.1:6379> keys *
1) "spring:session:sessions:4fc39ce3-63b3-4e17-b1c4-5e1ed96fb021"
2) "spring:session:expirations:1418772300000"

The FLUSHALL command clears all the active sessions:

redis 127.0.0.1:6379> FLUSHALL
OK

Tip

Discover the Redis client language with their online tutorial accessible at http://try.redis.io.

securityContextPersistenceFilter

We make use of this filter in the cloudstreetmarket-websocket Spring Security filter chain. Its role consists of injecting an external Spring Security context into a SecurityContextHolder from the configured SecurityContextRepository:

<bean id="securityContextPersistenceFilter" class="org.sfw.security.web.context.SecurityContextPersistence Filter">
    <constructor-arg name="repo" ref="httpSessionSecurityContextRepo" />
</bean>
    
<bean id="httpSessionSecurityContextRepo" class='org.sfw.security.web.context.HttpSessionSecurityContext Repository'>operty name='allowSessionCreation' value='false' />
</bean> 

This filter interacts with SecurityContextRepository to persist the context once the filter chain has been completed. Combined with Spring Session, this filter is very useful when you need to reuse an authentication that has been performed in another component (another web app in our case).

After this point, we have also been able to declare a global-method-security element (of the Spring Security namespace) that allows us to make use of @PreAuthorize annotations in @MessageMapping annotated methods (our message handling methods)::

<global-method-security secured-annotations="enabled" pre-post-annotations="enabled" />

AbstractSessionWebSocketMessageBrokerConfigurer

This is a long title. We have used this abstract class to give our WebSocketConfig the ability to:

  • Ensure sessions are kept alive on incoming web socket messages
  • Ensure that WebSocket sessions are destroyed when session terminate

AbstractSecurityWebSocketMessageBrokerConfigurer

In a similar fashion, this abstract class provides authorization capabilities to our WebSocketSecurityConfig bean. Thanks to it, the WebSocketSecurityConfig bean now controls the destinations that are allowed for incoming messages.

There's more…

Spring Session

Once again, we recommend the Spring reference document on Spring Session, which is very well done. Please check it out:

http://docs.spring.io/spring-session/docs/current/reference/html5

Apache HTTP proxy extra configuration

The few lines added to httpd.conf serve the purpose of rewriting the WebSocket scheme to ws during the WebSocket handshake. Not doing this causes SockJS to fall back to its XHR options (one WebSocket emulation).

Spring Data Redis

Also, we recommend that you read more about the Spring Data Redis project (in its reference document):

http://docs.spring.io/spring-data/data-redis/docs/current/reference/html

See also

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset