Spring security 使用Spring安全性的并发用户锁定

Spring security 使用Spring安全性的并发用户锁定,spring-security,spring-boot,Spring Security,Spring Boot,我以为我用这个解决了这个问题。然而,我发现一个新的用例导致了问题,所以请原谅重复。我有一个使用SpringSecurity4.0.2的SpringBoot应用程序。我不允许同时登录,并且可以确认您不能在不同的浏览器中登录两次。我看到浏览器显然共享会话,因为我可以从同一浏览器的两个不同选项卡登录(Chrome、Safari、Firefox、IE11测试) 我看到的是,当我尝试第三次登录浏览器选项卡时,身份验证失败。这是预期和期望的行为,但不可接受的是用户帐户被锁定,需要重新启动服务器。我检查了数据

我以为我用这个解决了这个问题。然而,我发现一个新的用例导致了问题,所以请原谅重复。我有一个使用SpringSecurity4.0.2的SpringBoot应用程序。我不允许同时登录,并且可以确认您不能在不同的浏览器中登录两次。我看到浏览器显然共享会话,因为我可以从同一浏览器的两个不同选项卡登录(Chrome、Safari、Firefox、IE11测试)

我看到的是,当我尝试第三次登录浏览器选项卡时,身份验证失败。这是预期和期望的行为,但不可接受的是用户帐户被锁定,需要重新启动服务器。我检查了数据库记录,该用户帐户仍然启用了所有功能

我设置断点并逐步通过org.springframework.security.core.sessionSessionRegistryImpl和
org.springframework.security.web.authenticationsimplerulthenticationfailurehandler
,我看不到用户被阻止的任何地方。我有我的调试级别:

logging.level.org.springframework:INFO
logging.level.org.springframework.web: INFO
logging.level.org.springframework.security:INFO
logging.level.org.springframework.security.web.authentication:INFO
我看不到任何异常或错误消息被抛出。这是我的主要配置:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private CustomUserDetailsService customUserDetailsService;

    @Autowired
    private SessionRegistry sessionRegistry;

    @Autowired
    ServletContext servletContext;

    @Autowired
    private CustomLogoutHandler logoutHandler;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth)
                    throws Exception {
        auth
                        .userDetailsService(customUserDetailsService)
                        .passwordEncoder(passwordEncoder());
        return;
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                        .addFilterAfter(new CSRFTokenGeneratorFilter(), CsrfFilter.class)
                        .authorizeRequests()
                        .antMatchers("/", ).permitAll().anyRequest().authenticated()

                        .and()

                        .formLogin()
                        .loginPage("/loginPage")
                        .permitAll()
                        .loginProcessingUrl("/login")
                        .defaultSuccessUrl("/?tab=success").failureUrl("/")
                        .and()
                        .logout().addLogoutHandler(logoutHandler).logoutRequestMatcher( new AntPathRequestMatcher("/logout"))
                        .deleteCookies("JSESSIONID")
                        .invalidateHttpSession(true).permitAll().and().csrf()

                        .and()  // Instructs Spring Security to use the Servlet 3.1 changeSessionId method for protecting against session fixation attacks,
                                        // also we set maximum session to 1
                        .sessionManagement().sessionAuthenticationStrategy(
                        concurrentSessionControlAuthenticationStrategy()).sessionFixation().changeSessionId().maximumSessions(1)
                        .maxSessionsPreventsLogin( true).expiredUrl("/login?expired" ).sessionRegistry(sessionRegistry )
                        .and()
                        .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED)
                        .invalidSessionUrl("/")
                        .and().authorizeRequests().anyRequest().authenticated();

        // Here we protect site from:
        // 1. X-Content-Type-Options
        http.headers().contentTypeOptions();
        // 2. Web Browser XSS Protection
        http.headers().xssProtection();

        http.headers().cacheControl();
        http.headers().httpStrictTransportSecurity();
        // 3. X-Frame-Options
        http.headers().frameOptions();

        // Inject servlet context and set http only (for increased security you should always customize http only to true)
        servletContext.getSessionCookieConfig().setHttpOnly(true);
    }

    // CORS protection
    @Bean
    public WebMvcConfigurer corsConfigurer() {
        return new WebMvcConfigurerAdapter() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping( "/**" ).allowedOrigins( "*" )
                                .allowedHeaders( "Access-Control-Allow-Origin", "*" )
                                .allowedHeaders( "Access-Control-Allow-Headers", "x-requested-with" )
                                .allowedMethods( "GET", "POST", "PUT", "DELETE" )
                                .maxAge( 3600 );
            }
        };
    }


    @Bean
    public PasswordEncoder passwordEncoder() {
        // default strength = 10
        return new BCryptPasswordEncoder();
    }

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


    @Bean
    public ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy() {

        ConcurrentSessionControlAuthenticationStrategy strategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry());
        strategy.setExceptionIfMaximumExceeded(true);
        strategy.setMessageSource(messageSource);

        return strategy;
    }
}
我还有一个LogoutHandler来处理额外的会话(我想这就是Quchie所指的?)

@组件
公共类CustomLogoutHandler实现LogoutHandler{
私有静态最终记录器Logger=LoggerFactory.getLogger(CustomLogoutHandler.class);
@自动连线
私人审计服务;
@自动连线
非公开会议登记处会议登记处;
@凌驾
公共无效注销(HttpServletRequest HttpServletRequest,HttpServletResponse HttpServletResponse,
身份验证(身份验证){
if(authentication!=null&&authentication.getDetails()!=null){
试一试{
User=(用户)身份验证。getPrincipal();
auditService.logoutUser(user.getOffice());
httpServletRequest.getSession().invalidate();
httpServletResponse.setStatus(httpServletResponse.SC_OK);
//重定向到登录
httpServletResponse.sendRedirect(url.ROOT);
List userSessions=sessionRegistry.getAllSessions(user,true);
for(会话信息会话:userSessions){
removeSessionInformation(session.getSessionId());
}
List principals=sessionRegistry.getAllPrincipals();
if(principals.contains(用户)){
抛出新异常(“用户未从会话中删除!”);
}
调试(“用户注销”);
}捕获(例外e){
e、 printStackTrace();
e=零;
}
}
}
} 
有人能帮我找出如何防止用户帐户在失败的并发登录尝试中被锁定吗?或者至少让它有一个时间,让它解锁有一定的时间量

编辑:
我无法理解,如果它只是存储在会话中,为什么它会锁定更长的时间(天)。数据库记录从未设置为错误状态,但在我重新启动Tomcat之前,用户帐户被锁定。

SessionRegistryImpl存储登录用户的会话。用户注销后,基本上需要将其清除。如果您不这样做,则任何用户都不能使用相同的id登录。我的建议是实现一个自定义LogoutHandler,以删除SessionRegistry中的会话。我在这里有类似的解释:谢谢你,Quchie,我用LogoutController更新了我的问题。我查看了您链接到的示例,但在任何地方都找不到springSecurityService。这是仅Groovy实现吗?SessionRegistryImpl存储登录用户的会话。用户注销后,基本上需要将其清除。如果您不这样做,则任何用户都不能使用相同的id登录。我的建议是实现一个自定义LogoutHandler,以删除SessionRegistry中的会话。我在这里有类似的解释:谢谢你,Quchie,我用LogoutController更新了我的问题。我查看了您链接到的示例,但在任何地方都找不到springSecurityService。这是一个仅使用Groovy的实现吗?
@Component
public class CustomLogoutHandler implements LogoutHandler {

    private static final Logger LOGGER = LoggerFactory.getLogger( CustomLogoutHandler.class );

    @Autowired
    private AuditService auditService;

    @Autowired
    private SessionRegistry sessionRegistry;

    @Override
    public void logout(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                       Authentication authentication) {

        if (authentication != null && authentication.getDetails() != null) {
            try {

                User user = (User)authentication.getPrincipal();

                auditService.logoutUser(user.getOffice());
                httpServletRequest.getSession().invalidate();


                httpServletResponse.setStatus(HttpServletResponse.SC_OK);
                //redirect to login
                httpServletResponse.sendRedirect(Urls.ROOT);

                List<SessionInformation> userSessions =  sessionRegistry.getAllSessions(user, true);

                for (SessionInformation session: userSessions) {
                  sessionRegistry.removeSessionInformation(session.getSessionId());

                }

                List<Object> principals = sessionRegistry.getAllPrincipals();

                if (principals.contains(user)) {
                    throw new Exception("User not removed from session!");
                }
                LOGGER.debug( "user logged out" );

            } catch (Exception e) {
                e.printStackTrace();
                e = null;
            }
        }

    }
}