Spring Security maxSession doesn't work
Asked Answered
G

3

7

I want to prevent login when user exceed maxSession count. For example every user can login once. And then if logged user try another login system should disable login for him.

.sessionManagement()
.maximumSessions(1).expiredUrl("/login?expire").maxSessionsPreventsLogin(true)
.sessionRegistry(sessionRegistry());


@Bean
public static ServletListenerRegistrationBean httpSessionEventPublisher() {
    return new ServletListenerRegistrationBean(new HttpSessionEventPublisher());
}
Gaitskell answered 18/6, 2016 at 1:55 Comment(0)
E
10

NOTE: This is tested on Spring MVC and 4.3.9.RELEASE, I haven't used Spring Boot yet.

I found a solution, let me share how it worked with me.

1) I configured HttpSecurity with SessionManagement as following:

@Override
protected void configure(HttpSecurity http) throws Exception {
  http
    .authorizeRequests()
      .antMatchers("/resources/**").permitAll()
      .antMatchers("/login**").permitAll()        // 1
      .antMatchers(...)
      .anyRequest().authenticated()
      .and()
    .formLogin()
      .loginPage("/login")
      .permitAll()
      .and()
    .logout()
      .deleteCookies("JSESSIONID")
      .permitAll()
      .and()
    .sessionManagement()                          // 2
      .maximumSessions(1)                         // 3
        .maxSessionsPreventsLogin(false)          // 4
        .expiredUrl("/login?expired")             // 5
        .sessionRegistry(getSessionRegistry())    // 6
    ;           
}

With the help of the doc Spring Doc > HttpSecurity > sessionManagement()

Example Configuration

The following configuration demonstrates how to enforce that only a single instance of a user is authenticated at a time. If a user authenticates with the username "user" without logging out and an attempt to authenticate with "user" is made the first session will be forcibly terminated and sent to the "/login?expired" URL.

@Configuration  
@EnableWebSecurity  
public class SessionManagementSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(HttpSecurity http) throws Exception {
            http.authorizeRequests().anyRequest().hasRole("USER").and().formLogin()
                            .permitAll().and().sessionManagement().maximumSessions(1)
                            .expiredUrl("/login?expired");
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            auth.inMemoryAuthentication().withUser("user").password("password").roles("USER");
    }  }   

When using SessionManagementConfigurer.maximumSessions(int), do not forget to configure HttpSessionEventPublisher for the application to ensure that expired sessions are cleaned up. In a web.xml this can be configured using the following:

<listener>
      <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
 </listener>

Alternatively, AbstractSecurityWebApplicationInitializer.enableHttpSessionEventPublisher() could return true.

We could know why we need sessionManagement(), maximumSessions(1), and of course expiredUrl("/login?expired").

  • So why the need of antMatchers("/login**").permitAll()? So that you could have permission to be redirect to /login?expired, otherwise you will be redirect to /login because anyRequest().authenticated(), with current HttpSecurity configuration permitAll() is applied to /login and /login?logout.

2) If you actually need to access current logged in user or expireNow() specific session for specific user like me, you might need getSessionRegistry(), but without it maximumSessions(1) works fine.

So again with the help of the doc:

When using SessionManagementConfigurer.maximumSessions(int), do not forget to configure HttpSessionEventPublisher for the application to ensure that expired sessions are cleaned up. In a web.xml this can be configured using the following:

<listener>
      <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
 </listener>

Alternatively, AbstractSecurityWebApplicationInitializer.enableHttpSessionEventPublisher() could return true.

So I should change my override enableHttpSessionEventPublisher() in my SecurityWebInitializer.java class:

public class SecurityWebInitializer extends AbstractSecurityWebApplicationInitializer {
    @Override
    protected boolean enableHttpSessionEventPublisher() {
        return true;
    }
}

3) Now just one last thing that I found it was my issue: As I am new in Spring framework, I learned to do custom UserDetails but with a little not good implementation, but I might do better later, I created an Entity that acts as both an Entity and a UserDetails:

    @Entity
    @Component("user")
    public class User implements UserDetails, Serializable {
        private static final long serialVersionUID = 1L;

        // ...

        @Override
        public boolean equals(Object obj) {
            if (obj instanceof User) {
              return username.equals( ((User) obj).getUsername() );
            }
            return false;
        }

        @Override
        public int hashCode() {
            return username != null ? username.hashCode() : 0;
        }
    }

I found some one recommended years ago in the forum here, that you should implement both hashCode() equals() methods, and if you take look at the source code of the default implementation for UserDetails User.java you will find that it has both methods implemented, I did it and it worked like a charm.

So that's it, hope this helpful.

You might want to read this link too: Spring - Expiring all Sessions of a User

Eden answered 21/8, 2017 at 19:40 Comment(0)
H
6

I had the same problem and it was originated in my UserDetails implementation:

ConcurrentSessionControlAuthenticationStrategy Line 93:

final List<SessionInformation> sessions = sessionRegistry.getAllSessions(
        authentication.getPrincipal(), false);

SessionRegistryImpl Line 64:

final Set<String> sessionsUsedByPrincipal = principals.get(principal);

if (sessionsUsedByPrincipal == null) {
    return Collections.emptyList();
}

In the session registry searches inside the "principals" list for a UserDetails object. So you need to override equals and hashcode in your UserDetails implementation, otherwise, it will view them as separate objects and thus always return an emptyList.

Example:

public class ApplicationUser implements UserDetails {

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (!(o instanceof ApplicationUser)) return false;
        ApplicationUser that = (ApplicationUser) o;
        return username.equals(that.username) &&
                email.equals(that.email) &&
                password.equals(that.password);
    }

    @Override
    public int hashCode() {
        return Objects.hash(username, email, password);
    }

}
Hwang answered 23/7, 2020 at 16:24 Comment(1)
good solution that worked for me. Thanks! The "hashCode" requirement should be more clear in many example and docs :(Eucken
R
2

I used a custom AuthenticationFilter in Spring security 4.2.3 and this is my solution (according to documentation)

Also, I had similar problem as @Chris Avraam explained, and I had to rewrite my equals() and hashCode()

    @Bean
    public SessionRegistry sessionRegistry() {
        return new SessionRegistryImpl();
    }

    @Bean
    public CompositeSessionAuthenticationStrategy compositeSessionAuthenticationStrategy() {
        ArrayList<SessionAuthenticationStrategy> sessionAuthenticationStrategies =
                Lists.newArrayList(new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry()),
                new RegisterSessionAuthenticationStrategy(sessionRegistry()));
        return new CompositeSessionAuthenticationStrategy(sessionAuthenticationStrategies);
    }

    @Bean
    public ApplicationAuthenticationFilter applicationAuthenticationFilter() throws IOException {
        ApplicationAuthenticationFilter applicationAuthenticationFilter = new ApplicationAuthenticationFilter();
        ...
        applicationAuthenticationFilter.setSessionAuthenticationStrategy(compositeSessionAuthenticationStrategy());
        return applicationAuthenticationFilter;
    }

    @Bean
    public ServletListenerRegistrationBean<HttpSessionEventPublisher> httpSessionEventPublisher() {
        return new ServletListenerRegistrationBean<HttpSessionEventPublisher>(new HttpSessionEventPublisher());
    }

    @Override
    public void configure(HttpSecurity http) throws Exception {
        ...
        http.sessionManagement().maximumSessions(1).sessionRegistry(sessionRegistry())
                .and().sessionAuthenticationStrategy(compositeSessionAuthenticationStrategy());
    }

Rollerskate answered 19/1, 2021 at 21:55 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.