2

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. registerNewSession() happens at login

  • 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) getAllPrincipals() query from my own class

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.

Andrei Epure
  • 1,746
  • 1
  • 21
  • 30
  • I don't know if it's related, but the methods which are annotated with `@Bean`, don't need to be `static`. Since Spring works with proxies etc. in configuration classes, this could be a probem, but i'm not sure. Maybe try with removing the static keywords. – dunni Aug 25 '17 at 15:45
  • I met problems like this in Spring. Can you show objects, which refer to SessionRegistries? In my case there are was to context in application. – egorlitvinenko Aug 25 '17 at 15:46
  • Actually i think the static keywords are the problem, because in your configuration where you call `.sessionRegistry(sessionRegistry())` it then 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. – dunni Aug 25 '17 at 15:47
  • thanks, @dunni, that was it. I removed the static field and it works now... I haven't thought of that. If you want, please post this as an answer and I'll accept it. Cheers! – Andrei Epure Aug 25 '17 at 15:51

1 Answers1

1

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.

dunni
  • 43,386
  • 10
  • 104
  • 99
  • Thanks, the [docs](https://stackoverflow.com/a/14943106/2261315) aren't very helpful on this one. But I will keep in mind for the future. A lot of magic happening there. – Andrei Epure Aug 25 '17 at 16:03
  • 2
    A general rule of thumb (nothing official, just from my side): Static and Spring usually don't play nice together. – dunni Aug 25 '17 at 16:09