首页 > 解决方案 > 从 Spring Security 中的另一个控制器手动调用 failureHandler?

问题描述

inb4:总的来说,我对 Spring 很陌生,所以我知道处理 SpringSecurity 是一个陡峭的过程。

tl;dr:我可以从我的自定义 SuccessHandler “抛出异常”到我的 FailureHandler 吗?

目前我正在尝试通过我们的登录页面实现一些行为。如果凭据错误,用户将被重定向回登录,并显示消息错误。如果使用的凭据是正确的,但另一个人已经在使用该帐户,则用户将被重定向到 Login 并显示一条消息错误(“最大会话错误”)。

到目前为止,在会话/并发控制中使用带有 Springs 的 ExceptionMappingAuthenticationFailureHandler 工作正常,显示正确的错误消息,但会话控制没有按我预期的那样工作。这可能是我自己的错误配置,或者只是我并不真正理解文档(英语不是我的母语,所以我完全接受这种可能性)。我希望 SessionControl 的行为是,如果与给定用户的会话处于活动状态,则试图进入的人将被踢回登录。目前它会这样做,但是如果具有会话活动的用户注销并尝试重新登录,它将踢他/她以 MaxSessionsError 登录。AFAIU 这是预期的,因为 SessionManagement.MaximumSessions() 限制了帐户的登录次数,不是用户可以同时拥有的活动会话数。我可能错了,但我仍在调查。我试图设置我的配置正如文档所说,但它没有像我预期的那样工作。

我当前的替代方法是在我确认另一个人当前正在使用帐户时尝试在 SuccessHandler 上抛出异常,以便 FailureHandler 可以将用户重定向到带有正确错误消息的登录页面。我的理解是,如果我重定向到登录页面,会话不会被破坏。我可以尝试自己摧毁它,但由于我缺乏一般经验,我真的更相信 Spring 能够做到这一点,因此我提出了问题。

欢迎任何帮助或耳光。我想学习,到目前为止我在安全方面有点疏忽。下面是 config 和 successHandler 的代码。小提示:由于某种我不知道的原因,这个项目没有使用 web.xml,所以我尝试将大多数配置转换为@Bean符号

配置

/*Other beans and configs*/

@Bean
    ExceptionMappingAuthenticationFailureHandler exceptionMappingAuthenticationFailureHandler(){
        ExceptionMappingAuthenticationFailureHandler ex = new ExceptionMappingAuthenticationFailureHandler();
        String RUTAERROR1 = "/login.zul?login_error=1";
        String RUTAERROR2 = "/login.zul?login_error=2";
        Map<String, String> errores = new HashMap<>();
        errores.put(org.springframework.security.authentication.ProviderNotFoundException.class.getCanonicalName(), RUTAERROR1);
        errores.put(SessionAuthenticationException.class.getCanonicalName(), RUTAERROR2);
        ex.setExceptionMappings(errores);
        return ex;
    }

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
        .antMatchers("/zkau/**").permitAll()
        .antMatchers("/css/**").permitAll()
        .antMatchers("/services/**").permitAll()
        .antMatchers("/images/**").permitAll()
        .antMatchers("/forgotpassword.zul").permitAll()
        .antMatchers("/login.zul**").permitAll()
        .antMatchers("/**").authenticated()
        .and()
        .formLogin()
        .loginProcessingUrl("/j_spring_security_check")
        .successHandler(customSuccessHandler())
        .loginPage("/login.zul")
        .permitAll()
//      .defaultSuccessUrl("/index.zul")
//      .defaultSuccessUrl("/menuAgrupadores.zul").
        .failureHandler(exceptionMappingAuthenticationFailureHandler())
//      .failureUrl("/login.zul?login_error=1")
        .and()
        .logout()
//      .logoutSuccessUrl("/login.zul")
        .logoutUrl("/j_spring_security_logout")
        .deleteCookies("JSESSIONID")
        .invalidateHttpSession(true)
        .logoutSuccessHandler(logoutSuccessHandler())
        .and().csrf().disable();
        
        http.headers()
        .frameOptions().sameOrigin()
        .httpStrictTransportSecurity().disable();
        
        http.sessionManagement().maximumSessions(1).maxSessionsPreventsLogin(true);
    }

成功处理程序

public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {

    @Resource
    private UsuariosRepository usuariosRepository;
    private RedirectStrategy redirect = new DefaultRedirectStrategy();
    
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication auth)
            throws IOException, ServletException {
        Usuarios user = usuariosRepository.findByUsuario(auth.getName());
        if(user.getToken() != null) {
            throw new SessionLimitException("Max sessions limit reached!", new ProviderNotFoundException("Máximo de sesiones excedido.")); //This is what I'm trying to do
        } else {
            user.setToken(request.getSession().getId());
            usuariosRepository.save(user);
            redirect.sendRedirect(request, response, "/menuAgrupadores.zul");
        }
        
    }

SessionLimitException 只是扩展了 AuthenticationException,没有进一步的逻辑。

编辑

正如@Eleftheria Stein-Kousathana 所说,我错过了监听器,但我必须将它添加到 web.xml。就文档而言,这有点明显,但是我的应用程序一开始就没有这个文件,并且所有其他 bean/config 都正常运行。换句话说,HttpSessionEventPublisher 的@Bean 表示法在我的实例中不起作用。如果您在这里遇到同样的问题,我强烈建议您尝试@Bean 和 XML 方式来注册此侦听器。如果你不知道,web.xml 应该放在 WEB-INF 中。

工作代码:

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

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
            .antMatchers("/zkau/**").permitAll()
            .antMatchers("/css/**").permitAll()
            .antMatchers("/services/**").permitAll()
            .antMatchers("/images/**").permitAll()
            .antMatchers("/forgotpassword.zul").permitAll()
            .antMatchers("/login.zul**").permitAll()
            .antMatchers("/**").authenticated()
        .and()
            .formLogin()
            .loginProcessingUrl("/j_spring_security_check")
            .loginPage("/login.zul")
            .permitAll()
            .defaultSuccessUrl("/menuAgrupadores.zul")
            .failureHandler(exceptionMappingAuthenticationFailureHandler())
        .and()
            .logout()
            .logoutUrl("/j_spring_security_logout")
            .deleteCookies("JSESSIONID")
            .invalidateHttpSession(true)
            .logoutSuccessUrl("/login.zul")
        .and()
            .csrf()
            .disable();
        
        http.headers()
            .frameOptions()
            .sameOrigin()
            .httpStrictTransportSecurity()
            .disable();
        
        http.sessionManagement()
            .maximumSessions(1)
            .maxSessionsPreventsLogin(true)
            .sessionRegistry(sessionRegistry()); //Added this for good measure

HttpSessionEventPublisher 的 xml 表示法

    <listener>
        <listener-class>
            org.springframework.security.web.session.HttpSessionEventPublisher
        </listener-class>
    </listener>

标签: javaspringspring-security

解决方案


我建议坚持第一种方法。

你唯一缺少的是一个HttpSessionEventPublisher.

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

添加后,它将解决注销问题,并允许新用户登录。

如果没有这个,HttpSessionDestroyedEvent则不会在注销时发布到应用程序上下文,并且不会清除过期的会话。
这就是不允许新用户登录的原因。

请注意HttpSecurity.sessionManagementJavadoc中的说明:

在使用 SessionManagementConfigurer.maximumSessions(int) 时,不要忘记为应用程序配置 HttpSessionEventPublisher 以确保清理过期的会话。


推荐阅读