Spring boot session management – why are there two instances of sessionRegistry?

I am trying to implement a ‘force logout’ functionality in my Spring Boot app (for example, after an admin disables a user account).

I followed the steps as specified in various tutorials to get access to the session registry, in order to expire the session of a user (baeldung step 6 and verbose version on myyurt; also related SO).

However, after registering the SessionRegistryImpl as a @Bean, when using the debugger, I see there are 2 different instances in the dependency injection mechanism:

  • one sessionRegistry instance is used by Spring Security at login and logout and holds the principals and sessions as expected. The screenshot below was taken after a login – I had a breakpoint in the registerNewSession() method. Notice the id and the maps with already logged in users.

  • the other sessionRegistry instance is only given to my own SessionManager class which requires SessionRegistry as a dependency and calls getAllPrincipals(). Notice the id is different and the maps are empty (I called getAllPrincipals() after I logged in multiple times and took the first screenshot)

Class where I register the sessionRegistry bean (I removed the unnecessary code and left only my custom filters, in case it might have something to do with Springs auto-configurations):

@EnableWebSecurity
class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public static HttpSessionEventPublisher httpSessionEventPublisher() {
        return new HttpSessionEventPublisher();
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        http
            .addFilterBefore(myFirstFilter, UsernamePasswordAuthenticationFilter.class)
            .addFilterAfter(mySecondFilter, FilterSecurityInterceptor.class)
            .formLogin() // skipping details
            .and()
            .x509() // skipping details
            .and()
            .logout()
                .invalidateHttpSession(true)
                .permitAll()
            .and()
            .authorizeRequests() // skipping details
            .and()
            .sessionManagement()
                .invalidSessionUrl("/login")
                .enableSessionUrlRewriting(false)
                .maximumSessions(-1)
                    .maxSessionsPreventsLogin(false)
                    .sessionRegistry(sessionRegistry())
                    .expiredUrl("/login?expire");
    }    
}

Class where I consumer the sessionRegistry dependency:

@Component
class DefaultSessionManager implements SessionManager {    
    private final SessionRegistry sessionRegistry;

    @Autowired public DefaultSessionManager(SessionRegistry sessionRegistry) {
        this.sessionRegistry = sessionRegistry;
    }

    public void expireUserSessions(String username) {
        for (Object principal : sessionRegistry.getAllPrincipals()) {
            // do stuff, but won't enter because the list is empty
        }
    }
}

If it helps, I looked up the beans setup with the Actuator /beans endpoint, and this is what it returns

        {
            "bean": "defaultSessionManager",
            "aliases":
            [
            ],
            "scope": "singleton",
            "type": "com.foo.bar.DefaultSessionManager",
            "resource": // file path
            "dependencies":
            [
                "sessionRegistry"
            ]
        },

        {
            "bean": "httpSessionEventPublisher",
            "aliases":
            [
            ],
            "scope": "singleton",
            "type": "org.springframework.security.web.session.HttpSessionEventPublisher",
            "resource": "class path resource [com/foo/bar/SecurityConfig.class]",
            "dependencies":
            [
            ]
        },
        {
            "bean": "sessionRegistry",
            "aliases":
            [
            ],
            "scope": "singleton",
            "type": "org.springframework.security.core.session.SessionRegistryImpl",
            "resource": "class path resource [com/foo/bar/SecurityConfig.class]",
            "dependencies":
            [
            ]
        },

How can there be two different instances in the DI system if all are declared as Singleton?
Do you have any tips on what might be wrong?

I’m using spring-boot-starter-parent 1.5.2.RELEASE, which uses Spring Security 4.2.2.RELEASE.

The problem are the static keywords on your @Bean annotated methods. In your configuration where you call

.sessionRegistry(sessionRegistry())

it directly calls the static method instead of going through the Spring proxy which would get the bean from the application context. That means, for your security configuration you create a new instance instead of getting the bean from the application context. With non static methods, direct method calls within the same calls are intercepted by Spring, which then checks if the bean already exists in the application context, and if yes the bean will be returned.