Spring Boot 会话管理——为啥会有两个 sessionRegistry 实例?

Posted

技术标签:

【中文标题】Spring Boot 会话管理——为啥会有两个 sessionRegistry 实例?【英文标题】:Spring boot session management - why are there two instances of sessionRegistry?Spring Boot 会话管理——为什么会有两个 sessionRegistry 实例? 【发布时间】:2018-02-03 17:15:12 【问题描述】:

我正在尝试在我的 Spring Boot 应用程序中实现“强制注销”功能(例如,在管理员禁用用户帐户后)。

我按照各种教程中指定的步骤来访问会话注册表,以使用户的会话到期(baeldung step 6 和 verbose version on myyurt;还有related SO)。

但是,在将 SessionRegistryImpl 注册为@Bean 后,在使用调试器时,我看到依赖注入机制中有 2 个不同的实例:

Spring Security 在登录和注销时使用一个 sessionRegistry 实例,并按预期保存主体和会话。下面的屏幕截图是在登录后拍摄的——我在 registerNewSession() 方法中有一个断点。注意 id 和已登录用户的地图。

另一个 sessionRegistry 实例只提供给我自己的 SessionManager 类,它需要 SessionRegistry 作为依赖项并调用 getAllPrincipals()。请注意 id 不同并且地图为空(我在多次登录并拍摄第一个屏幕截图后调用了 getAllPrincipals())

我注册 sessionRegistry bean 的类(我删除了不必要的代码,只留下了我的自定义过滤器,以防它可能与 Springs 自动配置有关):

@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");
        

我使用 sessionRegistry 依赖项的类:

@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
        
    

如果有帮助,我使用 Actuator /beans 端点查找了 bean 设置,这就是它返回的内容

        
            "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":
            [
            ]
        ,

如果都声明为 Singleton,DI 系统中怎么会有两个不同的实例? 您对可能出现的问题有任何提示吗?

我正在使用 spring-boot-starter-parent 1.5.2.RELEASE,它使用 Spring Security 4.2.2.RELEASE。

【问题讨论】:

不知道有没有关系,但是@Bean注解的方法不需要static。由于 Spring 在配置类中使用代理等,这可能是一个问题,但我不确定。也许尝试删除静态关键字。 我在春天遇到过这样的问题。你能显示引用 SessionRegistries 的对象吗?就我而言,应用程序中有上下文。 实际上我认为静态关键字是问题所在,因为在您调用 .sessionRegistry(sessionRegistry()) 的配置中,它会直接调用静态方法,而不是通过 Spring 代理从应用程序中获取 bean语境。这意味着,对于您的安全配置,您需要创建一个新实例,而不是从应用程序上下文中获取 bean。 谢谢@dunni,就是这样。我删除了静态字段,它现在可以工作了……我没想到。如果您愿意,请将其发布为答案,我会接受。干杯! 【参考方案1】:

问题在于 @Bean 注释方法上的静态关键字。在您调用的配置中

.sessionRegistry(sessionRegistry())

它直接调用静态方法,而不是通过 Spring 代理从应用程序上下文中获取 bean。这意味着,对于您的安全配置,您将创建一个新实例,而不是从应用程序上下文中获取 bean。对于非静态方法,相同调用中的直接方法调用会被 Spring 拦截,然后检查 bean 是否已存在于应用程序上下文中,如果存在则返回 bean。

【讨论】:

谢谢,docs 在这方面不是很有帮助。但我会牢记未来。那里发生了很多魔法。 一般的经验法则(不是官方的,只是从我的角度来看):静态和 Spring 通常不能很好地结合在一起。

以上是关于Spring Boot 会话管理——为啥会有两个 sessionRegistry 实例?的主要内容,如果未能解决你的问题,请参考以下文章

通过 spring-boot 进行 Spring WS UsernameToken 身份验证 + 会话管理

如何使用 Spring Security 管理 Spring Boot 中的会话?

对其他 Spring Boot 应用程序的集中会话管理

如何通过管理器路径名在 Spring Boot 中禁用 Tomcat 会话持久性?

spring boot为啥引入两个mapping会报错

为啥spring boot总是在两个小时内关闭