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