Spring Security通过并发登录尝试来锁定用户

我是安全新手,遇到了导致用户帐户被锁定的问题,只有应用程序重新启动才能修复它。

我有一个弹簧启动(1.3.0.BUILD-SNAPSHOT)与弹簧安全(4.0.2.RELEASE)应用程序,我试图控制并发会话策略,以便用户只能有一个登录。 它正确检测来自其他浏览器的后续登录尝试并阻止它。 但是,我注意到一些我似乎无法追查的奇怪行为:

  • 用户可以在同一浏览器中对两个选项卡进行身份validation。 我无法使用三个标签登录,但有两个标签。 退出一个似乎注销两者。 我看到cookie的值是一样的,所以我猜他们正在共享一个会话:

表1 JSESSIONID: DA7C3EF29D55297183AF5A9BEBEF191F &941135CEBFA92C3912ADDC1DE41CFE9A

表2 JSESSIONID: DA7C3EF29D55297183AF5A9BEBEF191F &48C17A19B2560EAB8EC3FDF51B179AAE

第二次登录尝试会显示以下日志消息,这些消息似乎表示第二次登录尝试(我通过Spring-Security源步进来validation:

osswaiFilterSecurityInterceptor : Secure object: FilterInvocation: URL: /loginPage; Attributes: [permitAll] osswaiFilterSecurityInterceptor : Previously Authenticated: org.springframework.security.authentication.UsernamePasswordAuthenticationToken@754041c8: Principal: User [username=xxx@xxx.xx, password= ]; Credentials: [PROTECTED]; Authenticated: true; Details: org.springframework.security.web.authentication.WebAuthenticationDetails@43458: RemoteIpAddress: 0:0:0:0:0:0:0:1; SessionId: 4708D404F64EE758662B2B308F36FFAC; Granted Authorities: Owner ossaccess.vote.AffirmativeBased : Voter: org.springframework.security.web.access.expression.WebExpressionVoter@17527bbe, returned: 1 osswaiFilterSecurityInterceptor : Authorization successful osswaiFilterSecurityInterceptor : RunAsManager did not change Authentication object ossecurity.web.FilterChainProxy : /loginPage reached end of additional filter chain; proceeding with original chain org.apache.velocity : ResourceManager : unable to find resource 'loginPage.vm' in any resource loader. osswaExceptionTranslationFilter : Chain processed normally sswcSecurityContextPersistenceFilter : SecurityContextHolder now cleared, as request processing completed 
  • 当我使用两个选项卡登录然后注销时,用户帐户将被锁定并需要重新启动服务器。 控制台中没有错误,数据库中的用户记录未更改。

这是我的安全配置:

 @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private CustomUserDetailsService customUserDetailsService; @Autowired private SessionRegistry sessionRegistry; @Autowired ServletContext servletContext; @Autowired private CustomLogoutHandler logoutHandler; @Autowired private MessageSource messageSource; /** * Sets security configurations for the authentication manager */ @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth .userDetailsService(customUserDetailsService) .passwordEncoder(passwordEncoder()); return; } protected void configure(HttpSecurity http) throws Exception { http .formLogin() .loginPage("/loginPage") .permitAll() .loginProcessingUrl("/login") .defaultSuccessUrl("/?tab=success") .and() .logout().addLogoutHandler(logoutHandler).logoutRequestMatcher( new AntPathRequestMatcher("/logout")) .deleteCookies("JSESSIONID") .invalidateHttpSession(true).permitAll().and().csrf() .and() .sessionManagement().sessionAuthenticationStrategy( concurrentSessionControlAuthenticationStrategy).sessionFixation().changeSessionId().maximumSessions(1) .maxSessionsPreventsLogin( true).expiredUrl("/login?expired" ).sessionRegistry(sessionRegistry ) .and() .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) .invalidSessionUrl("/") .and().authorizeRequests().anyRequest().authenticated(); http.headers().contentTypeOptions(); http.headers().xssProtection(); http.headers().cacheControl(); http.headers().httpStrictTransportSecurity(); http.headers().frameOptions(); servletContext.getSessionCookieConfig().setHttpOnly(true); } @Bean public ConcurrentSessionControlAuthenticationStrategy concurrentSessionControlAuthenticationStrategy() { ConcurrentSessionControlAuthenticationStrategy strategy = new ConcurrentSessionControlAuthenticationStrategy(sessionRegistry()); strategy.setExceptionIfMaximumExceeded(true); strategy.setMessageSource(messageSource); return strategy; } // Work around https://jira.spring.io/browse/SEC-2855 @Bean public SessionRegistry sessionRegistry() { SessionRegistry sessionRegistry = new SessionRegistryImpl(); return sessionRegistry; } } 

我还有以下方法来处理检查用户:

 @Entity @Table(name = "USERS") @JsonIdentityInfo(generator = ObjectIdGenerators.PropertyGenerator.class, property = "username") public class User implements UserDetails { ... @Override public int hashCode() { final int prime = 31; int result = 1; result = prime * result + ((username == null) ? 0 : username.hashCode()); return result; } @Override public boolean equals(Object obj) { if (this == obj) return true; if (obj == null) return false; if (getClass() != obj.getClass()) return false; User other = (User) obj; if (username == null) { if (other.username != null) return false; } else if (!username.equals(other.username)) return false; return true; } } 

如何防止帐户锁定,或者至少如何以编程方式解锁?

编辑1/5/16我在WebSecurityConfig中添加了以下内容:

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

并删除:

 servletContext.addListener(httpSessionEventPublisher()) 

但是当我在同一个浏览器上登录两次时,我仍然会看到这种行为 – 注销会锁定帐户,直到我重新启动。

事实certificate,SessionRegistryImpl没有从会话中删除用户。 第一个选项卡注销从未实际调用过服务器,因此第二个调用会删除一个sessionid,而在主体中保留一个。

我不得不做出一些改变:

 @Component public class CustomLogoutHandler implements LogoutHandler { @Autowired private SessionRegistry sessionRegistry; @Override public void logout(HttpServletRequest httpServletRequest, httpServletResponse httpServletResponse, Authentication authentication) { ... httpServletRequest.getSession().invalidate(); httpServletResponse.setStatus(HttpServletResponse.SC_OK); //redirect to login httpServletResponse.sendRedirect("/"); List userSessions = sessionRegistry.getAllSessions(user, true); for (SessionInformation session: userSessions) { sessionRegistry.removeSessionInformation(session.getSessionId()); } } @Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public SessionRegistry sessionRegistry() { if (sessionRegistry == null) { sessionRegistry = new SessionRegistryImpl(); } return sessionRegistry; } @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; } }