{"id":14688,"date":"2016-04-08T10:07:10","date_gmt":"2016-04-08T08:07:10","guid":{"rendered":"https:\/\/blog.trifork.com\/?p=14688"},"modified":"2016-04-08T10:07:10","modified_gmt":"2016-04-08T08:07:10","slug":"spring-session-concurrent-session-control","status":"publish","type":"post","link":"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/","title":{"rendered":"Using Spring Session for concurrent session control in a clustered environment"},"content":{"rendered":"<p><span style=\"font-weight: 400\">For a long time, Spring Security has provided support to limit the number of sessions a single user can have concurrently. This prevents users from being logged in from many different devices at the same time, for example to ensure that they won\u2019t share their credentials to a paid site with their friends and family. <\/span><\/p>\n<p><span style=\"font-weight: 400\">My former colleague Quinten Krijger has <a href=\"https:\/\/blog.trifork.com\/2014\/02\/28\/session-timeout-and-concurrent-session-control-with-spring-security-and-spring-mvc\/\" target=\"_blank\" rel=\"noopener\">blogged about this feature before<\/a>.\u00a0<\/span><span style=\"font-weight: 400\">Note the last paragraph, which explains how this support is limited to single-node applications.<\/span><\/p>\n<p><span style=\"font-weight: 400\">Although running on a single node may suffice for many applications, there are plenty applications running in a clustered environment that should be able to benefit from concurrent session control as well. As hinted in the aforementioned blog, this requires both implementing a custom SessionRegistry as well as ensuring that expiring a session is propagated to all nodes in the cluster.<\/span><\/p>\n<p><span style=\"font-weight: 400\">This is exactly what I\u2019ve done recently using Spring Session, a framework that allows you to take control over managing sessions using a shared external registry like Redis. In this post I\u2019d like to walk you through the code, which can be found here: <\/span><a href=\"https:\/\/github.com\/jkuipers\/spring-session-concurrent-session-control\" target=\"_blank\" rel=\"noopener\"><span style=\"font-weight: 400\">https:\/\/github.com\/jkuipers\/spring-session-concurrent-session-control<\/span><\/a><\/p>\n<h4>UPDATE:<\/h4>\n<p>Based on the code I wrote for this blog I&#8217;ve opened a <a href=\"https:\/\/github.com\/spring-projects\/spring-session\/pull\/473\" target=\"_blank\" rel=\"noopener\">pull request for Spring Session<\/a>. That request is scheduled for inclusion in Spring Session 1.3, but the code\u00a0works just fine with the upcoming 1.2 release and removes the limitation of not providing\u00a0an expiry notification after exceeding the maximum number of sessions.<\/p>\n<p><!--more--><\/p>\n<h2>Spring Session primer<\/h2>\n<p><span style=\"font-weight: 400\">Spring Session is a relatively new addition to the Spring portfolio. It is a framework that basically allows you to let your application take control over session management, rather than leaving this up to the servlet container, where session information is typically stored in some external repository like a Redis server. <\/span><\/p>\n<p><span style=\"font-weight: 400\">There are several advantages in doing this: sessions are no longer limited to the traditional 1-session-per-browser approach but can be expanded to individual browser tabs, sessions IDs can be exchanged through other means than only cookies, thus adding session support to e.g. web sockets or through custom request headers, sessions can be clustered, it\u2019s easy to get an overview of the sessions that exist for a given user, etc.<\/span><\/p>\n<p><span style=\"font-weight: 400\">To integrate with the Servlet API, Spring Session provides a filter which wraps your HttpServletRequests and overrides the getSession methods. This means that it becomes trivial to transparently add Spring Session support to a Servlet-based web application, like a Spring-MVC app.<\/span><\/p>\n<p><span style=\"font-weight: 400\">The fact that Spring Session (since version 1.1) allows you to query for sessions stored in an external repository by username seems to make it a good fit for solving the problem of customizing Spring Security to support concurrent session control in a distributed environment. <\/span><\/p>\n<p><span style=\"font-weight: 400\">There\u2019s actually already an <a href=\"https:\/\/github.com\/spring-projects\/spring-session\/issues\/65\" target=\"_blank\" rel=\"noopener\">open issue for this<\/a>,\u00a0but I went ahead and implemented a proof of concept already which I discuss in the next section.<\/span><\/p>\n<h2>The implementation<\/h2>\n<h3>Custom SessionRegistry<\/h3>\n<p><span style=\"font-weight: 400\">The default SessionRegistry implementation from Spring Security tracks all sessions in a local map, which is updated when \u201csession created\u201d and \u201csession destroyed\u201d events are received. This doesn\u2019t work for a clustered environment, as these events aren\u2019t propagated across nodes in a cluster. One approach would be to try to propagate these events, but that would mean that the SessionRegistry on every node actually duplicates the information that\u2019s already managed by Spring Session and could potentially get out of sync.<\/span><\/p>\n<p><span style=\"font-weight: 400\">Therefore, I chose to implement a SessionRegistry that simply delegates finding all sessions for a user to Spring Session directly: that means that no internal administration is necessary.<\/span><\/p>\n<p><a href=\"https:\/\/github.com\/jkuipers\/spring-session-concurrent-session-control\/blob\/master\/src\/main\/java\/nl\/trifork\/security\/SpringSessionBackedSessionRegistry.java\" target=\"_blank\" rel=\"noopener\">My implementation<\/a>\u00a0<span style=\"font-weight: 400\">relies on Spring Session\u2019s FindByIndexNameSessionRepository to retrieve sessions for a given principal. Currently the in-memory MapSessionRepository is the only SessionRepository which doesn\u2019t implement that interface, and that implementation isn\u2019t intended for use in clustered environments anyway, so this should work with all relevant supported repositories. The upcoming 1.2 version of Spring Session will support relational databases and MongoDB in addition to Redis that\u2019s already supported in 1.1.<\/span><\/p>\n<p><span style=\"font-weight: 400\">The SessionRegistry interface defines several methods that are used for internal session book keeping: we don\u2019t need a meaningful implementation of those methods, since we\u2019re not going to track session creation, usage and deletion ourselves but leave this completely to Spring Session. <\/span><span style=\"font-weight: 400\"><br \/>\n<\/span><span style=\"font-weight: 400\">Also, there\u2019s a method to query for all principals that have sessions that cannot be implemented based on the functionality currently provided by Spring Session. Fortunately Spring Security doesn\u2019t actually call this method in any of its code, so we can easily just throw an UnsupportedOperationException here. As I understand it, there are plans to split up the SessionRegistry interface in future version of Spring Security to avoid the need to provide such a method implementation altogether. <\/span><\/p>\n<p><span style=\"font-weight: 400\">The API also expects that you include support for including expired sessions when querying for a principal\u2019s sessions, but again that\u2019s never used. Nevertheless I\u2019ve taken this into account in my implementation, but currently Spring Session will never return expired sessions when you query it. Should that ever change, then my code will at least be prepared \ud83d\ude09<\/span><\/p>\n<p>One thing that is a bit tricky is that Spring Security passes in a principal as an Object, while Spring Session expects a String to represent the current user. I\u2019ve added a method to derive this String from a given principal, based on Spring Security\u2019s AbstractAuthenticationToken#getName() method. This should work for the majority of applications, and would be easy to adapt to custom implementations.<\/p>\n<h3>Custom SessionInformation<\/h3>\n<p><span style=\"font-weight: 400\">Out of the box, Spring Security expects that the SessionRegistry tracks sessions itself in the form of SessionInformation objects. When a user logs in but has already reached the configured maximum number of sessions, then by default Spring Security expires the oldest session by calling an expireNow method on the corresponding SessionInformation.<br \/>\n<\/span><span style=\"font-weight: 400\">That poses a problem for my implementation, in which the SessionRegistry doesn\u2019t track sessions itself but leaves the session management completely to Spring Session: the SessionInformations in that implementation only contain derived state, so updating that state doesn\u2019t actually expire a session at all. <\/span><\/p>\n<p><span style=\"font-weight: 400\">Therefore, I\u2019ve extended the default SessionInformation so that its expireNow method can be overridden to ensure that an invocation of that method is propagated to Spring Session: see\u00a0<\/span><a href=\"https:\/\/github.com\/jkuipers\/spring-session-concurrent-session-control\/blob\/master\/src\/main\/java\/nl\/trifork\/security\/SpringSessionBackedSessionInformation.java\" target=\"_blank\" rel=\"noopener\"><span style=\"font-weight: 400\">https:\/\/github.com\/jkuipers\/spring-session-concurrent-session-control\/blob\/master\/src\/main\/java\/nl\/trifork\/security\/SpringSessionBackedSessionInformation.java<\/span><\/a><\/p>\n<p>The only limitation here compared to the default provided by Spring Security is that a session which is expired in this way will simply be deleted, while the default implementation only marks it as expired: the latter approach allows showing a warning to the user that his session was expired because too many sessions were created.<br \/>\n<span style=\"font-weight: 400\">My implementation doesn\u2019t support such a notice, since Spring Session doesn\u2019t allow manual expiration of a session nor retrieving expired sessions.<\/span><\/p>\n<h2>Testing the implementation<\/h2>\n<p><span style=\"font-weight: 400\">The demo app is a Spring Boot app, so you can run it from your IDE or package it as a jar and run it from the command line. It relies on having a Redis server running on the default port: if your setup is not like that, you\u2019ll have to provide the proper configuration in the application.properties file.<br \/>\n<\/span><span style=\"font-weight: 400\">The application configures a single user called \u2018user\u2019 with a password \u2018secret\u2019. After logging in with this user you\u2019ll see an overview of all sessions and the timestamp of their last usage, with the current session highlighted. <\/span><\/p>\n<p><span style=\"font-weight: 400\">By default the application runs on port 8080. You can run a second instance by providing an alternative port:<\/span><\/p>\n<pre><span style=\"font-weight: 400\">java -jar spring-session-concurrent-session-control-1.0.jar<\/span><span style=\"font-weight: 400\"> --server.port=9000<\/span><\/pre>\n<p><span style=\"font-weight: 400\">Now you can log in from two different browsers (or use private browser mode from a single browser) to the two running applications and see your two different sessions. When you refresh the page you\u2019ll see the updated timestamps. <\/span><\/p>\n<p><span style=\"font-weight: 400\">If you now log in a third time, the session with the oldest timestamp will be deleted, thus causing you to be logged out in that browser. This should now work across application instances, since a single Spring Session-backed session repository is used. <\/span><\/p>\n<p><span style=\"font-weight: 400\">Alternatively, in App.java, you can update the code to read<\/span><\/p>\n<pre><span style=\"font-weight: 400\">.maxSessionsPreventsLogin(<\/span><b>true<\/b><span style=\"font-weight: 400\">);<\/span><\/pre>\n<p><span style=\"font-weight: 400\">This will cause a third login to simply fail with an error, rather than deleting another session.<\/span><\/p>\n<h2>Conclusion<\/h2>\n<p><span style=\"font-weight: 400\">Adding support for concurrent session control to Spring Security through Spring Session is actually fairly straightforward. Apart from losing the option to warn a user that a session has been expired because the maximum has been reached, all expected functionality is there with only two classes and very little additional configuration. <\/span><\/p>\n<p><span style=\"font-weight: 400\">Apart from concurrent session management, using Spring Session\u00a0opens up a slew of additional options: users can see what sessions they have running and manually expire them, for example. Reasons enough to start giving Spring Session a try!<br \/>\nIn the coming release it will support additional backends next to Redis, like MongoDB or a relational database, so if running Redis is not an option for you then that will no longer be a show stopper.<\/span><\/p>\n","protected":false},"excerpt":{"rendered":"<p>For a long time, Spring Security has provided support to limit the number of sessions a single user can have concurrently. This prevents users from being logged in from many different devices at the same time, for example to ensure that they won\u2019t share their credentials to a paid site with their friends and family. [&hellip;]<\/p>\n","protected":false},"author":62,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_acf_changed":false,"content-type":"","footnotes":""},"categories":[10,94],"tags":[70,228],"class_list":["post-14688","post","type-post","status-publish","format-standard","hentry","category-development","category-spring","tag-spring","tag-spring-security"],"acf":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v24.4 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Using Spring Session for concurrent session control in a clustered environment - Trifork Blog<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Using Spring Session for concurrent session control in a clustered environment - Trifork Blog\" \/>\n<meta property=\"og:description\" content=\"For a long time, Spring Security has provided support to limit the number of sessions a single user can have concurrently. This prevents users from being logged in from many different devices at the same time, for example to ensure that they won\u2019t share their credentials to a paid site with their friends and family. [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/\" \/>\n<meta property=\"og:site_name\" content=\"Trifork Blog\" \/>\n<meta property=\"article:published_time\" content=\"2016-04-08T08:07:10+00:00\" \/>\n<meta name=\"author\" content=\"Joris Kuipers\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"Joris Kuipers\" \/>\n\t<meta name=\"twitter:label2\" content=\"Est. reading time\" \/>\n\t<meta name=\"twitter:data2\" content=\"8 minutes\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/\",\"url\":\"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/\",\"name\":\"Using Spring Session for concurrent session control in a clustered environment - Trifork Blog\",\"isPartOf\":{\"@id\":\"https:\/\/trifork.nl\/blog\/#website\"},\"datePublished\":\"2016-04-08T08:07:10+00:00\",\"author\":{\"@id\":\"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b\"},\"breadcrumb\":{\"@id\":\"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/trifork.nl\/blog\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Using Spring Session for concurrent session control in a clustered environment\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/trifork.nl\/blog\/#website\",\"url\":\"https:\/\/trifork.nl\/blog\/\",\"name\":\"Trifork Blog\",\"description\":\"Keep updated on the technical solutions Trifork is working on!\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/trifork.nl\/blog\/?s={search_term_string}\"},\"query-input\":{\"@type\":\"PropertyValueSpecification\",\"valueRequired\":true,\"valueName\":\"search_term_string\"}}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b\",\"name\":\"Joris Kuipers\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/trifork.nl\/blog\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g\",\"caption\":\"Joris Kuipers\"},\"sameAs\":[\"http:\/\/www.trifork.nl\"],\"url\":\"https:\/\/trifork.nl\/blog\/author\/jorisk\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Using Spring Session for concurrent session control in a clustered environment - Trifork Blog","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/","og_locale":"en_US","og_type":"article","og_title":"Using Spring Session for concurrent session control in a clustered environment - Trifork Blog","og_description":"For a long time, Spring Security has provided support to limit the number of sessions a single user can have concurrently. This prevents users from being logged in from many different devices at the same time, for example to ensure that they won\u2019t share their credentials to a paid site with their friends and family. [&hellip;]","og_url":"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/","og_site_name":"Trifork Blog","article_published_time":"2016-04-08T08:07:10+00:00","author":"Joris Kuipers","twitter_card":"summary_large_image","twitter_misc":{"Written by":"Joris Kuipers","Est. reading time":"8 minutes"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/","url":"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/","name":"Using Spring Session for concurrent session control in a clustered environment - Trifork Blog","isPartOf":{"@id":"https:\/\/trifork.nl\/blog\/#website"},"datePublished":"2016-04-08T08:07:10+00:00","author":{"@id":"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b"},"breadcrumb":{"@id":"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/trifork.nl\/blog\/spring-session-concurrent-session-control\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/trifork.nl\/blog\/"},{"@type":"ListItem","position":2,"name":"Using Spring Session for concurrent session control in a clustered environment"}]},{"@type":"WebSite","@id":"https:\/\/trifork.nl\/blog\/#website","url":"https:\/\/trifork.nl\/blog\/","name":"Trifork Blog","description":"Keep updated on the technical solutions Trifork is working on!","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/trifork.nl\/blog\/?s={search_term_string}"},"query-input":{"@type":"PropertyValueSpecification","valueRequired":true,"valueName":"search_term_string"}}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/trifork.nl\/blog\/#\/schema\/person\/265bd41e503f7176742258a927de598b","name":"Joris Kuipers","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/trifork.nl\/blog\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/9ab8da0d60582bad84342d4602d23dbd?s=96&d=mm&r=g","caption":"Joris Kuipers"},"sameAs":["http:\/\/www.trifork.nl"],"url":"https:\/\/trifork.nl\/blog\/author\/jorisk\/"}]}},"_links":{"self":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/posts\/14688","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/users\/62"}],"replies":[{"embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/comments?post=14688"}],"version-history":[{"count":0,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/posts\/14688\/revisions"}],"wp:attachment":[{"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/media?parent=14688"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/categories?post=14688"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/trifork.nl\/blog\/wp-json\/wp\/v2\/tags?post=14688"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}