Spring Security 4 退出后再登录,页面停留在登录页,Url却多了一个Logout参数

时间:2021-12-25 17:19:45

spring security 4 的logout问题
今天在集成spring-boot 和 spring-security的时候,出现了如下的怪象:
配置好了基本的登录和身份验证(自定义了一个简单的UserDetailsService),springboot的配置如下:

       http.authorizeRequests().anyRequest().authenticated()
                    .and()
                    .formLogin()
                    .loginPage("/signin").permitAll()
                    .failureUrl("/signin?error=1").permitAll()
                    .defaultSuccessUrl("/index")
                    .loginProcessingUrl("/login")
                    .and()
                    .logout().logoutRequestMatcher(AntPathRequestMatcher("/logout", "POST"))
                    .deleteCookies("JSESSIONID")
                    .invalidateHttpSession(true)
                    .and()
                    .httpBasic();

首次登录没有问题,即:登录后直接进入了Java-Based配置好的index页面。然后点击执行退出,因为启用了CSRF策略,所有登出分为”signout -> logout”两步。登出也OK了。但是,此时再次登录,页面没有进入index,而是停留在登录页面。仔细观察URL,发现浏览器地址栏的URL变成了http://localhost:8080/signin?logout。经过调查,发现是LogoutSuccessHandler出了问题。

默认情况下,spring security 4启用了SimpleUrlLogoutSuccessHandler来处理登出的逻辑,关键代码如下:

public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
    super.handle(request, response, authentication);
}

superAbstractAuthenticationTargetUrlRequestHandler,其handle方法的代码为:

    protected void handle(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws IOException, ServletException {
         String targetUrl = determineTargetUrl(request, response);
        if (response.isCommitted()) {
            logger.debug("Response has already been committed. Unable to redirect to "
                    + targetUrl);
            return;
        }
        redirectStrategy.sendRedirect(request, response, targetUrl);
    }

其中,最关键的就是

String targetUrl = determineTargetUrl(request, response);

通过detemineTargetUrl方法得到的targetUrl就是http://localhost:8080/signin?logout,所以程序最后跳转到了这个路径。那么问题来了,determineTargetUrl方法怎么会生成带有signin/logout的URL呢?

追根溯源,最后来到了这里:
spring-security 4 内部使用AbstractAuthenticationFilterConfigurerLogoutConfigurer进行配置,而LogoutConfigurer在内部设置了SimpleUrlLogoutSuccessHandler的defaultTargetUrl参数,具体代码如下:
AbstractAuthenticationFilterConfigurer的相关代码:

private void updateAuthenticationDefaults() {
        if (loginProcessingUrl == null) {
            loginProcessingUrl(loginPage);
        }
        if (failureHandler == null) {
            failureUrl(loginPage + "?error");
        }
        final LogoutConfigurer<B> logoutConfigurer = getBuilder().getConfigurer(
                LogoutConfigurer.class);
        if (logoutConfigurer != null && !logoutConfigurer.isCustomLogoutSuccess()) {
             logoutConfigurer.logoutSuccessUrl(loginPage + "?logout");
        }
}

注意上面第11行,spring security 4 默认将登录页面的url拼上一个?logout参数作为logout成功后的跳转URL,正式这段代码,导致了本文之初所述的问题

LogoutConfigurer的相关代码:

    /** * The URL to redirect to after logout has occurred. The default is "/login?logout". * This is a shortcut for invoking {@link #logoutSuccessHandler(LogoutSuccessHandler)} * with a {@link SimpleUrlLogoutSuccessHandler}. * * @param logoutSuccessUrl the URL to redirect to after logout occurred * @return the {@link LogoutConfigurer} for further customization */
    public LogoutConfigurer<H> logoutSuccessUrl(String logoutSuccessUrl) {
        this.customLogoutSuccess = true;
        this.logoutSuccessUrl = logoutSuccessUrl;
        return this;
    }

    /** * Gets the {@link LogoutSuccessHandler} if not null, otherwise creates a new * {@link SimpleUrlLogoutSuccessHandler} using the {@link #logoutSuccessUrl(String)}. * * @return the {@link LogoutSuccessHandler} to use */
    private LogoutSuccessHandler getLogoutSuccessHandler() {
        LogoutSuccessHandler handler = this.logoutSuccessHandler;
        if (handler == null) {
            handler = createDefaultSuccessHandler();
        }
        return handler;
    }
    private LogoutSuccessHandler createDefaultSuccessHandler() {
        SimpleUrlLogoutSuccessHandler urlLogoutHandler = new SimpleUrlLogoutSuccessHandler();
        urlLogoutHandler.setDefaultTargetUrl(logoutSuccessUrl);
        if(defaultLogoutSuccessHandlerMappings.isEmpty()) {
            return urlLogoutHandler;
        }
        DelegatingLogoutSuccessHandler successHandler = new DelegatingLogoutSuccessHandler(defaultLogoutSuccessHandlerMappings);
        successHandler.setDefaultLogoutSuccessHandler(urlLogoutHandler);
        return successHandler;
    }