Handling Authentication Failures in Spring Security with Custom Error Mapping

When implementing form-based authentication in Spring Security, it's essential to provide meaningful feedback to users upon login failure. By default, Spring Security stores the AuthenticationException in the request scope before forwarding to the configured failure URL. This enables custom error handling logic in the controller.

Security Configuration Overview

The following configuration disables CSRF (for simplicity), permits public access to authentication endpoints, and delegates credential validation to a custom UserDetailsService using BCrypt hashing:

package cn.young.greenhome.config;

import cn.young.greenhome.module.auth.UserDetailsServiceImpl;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl accountService;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .headers().frameOptions().disable()
            .and()
            .authorizeRequests()
                .requestMatchers("/login", "/logout", "/getVerifyCode", "/validateVerifyCode").permitAll()
                .anyRequest().authenticated()
            .and()
            .formLogin()
                .loginPage("/login")
                .successForwardUrl("/success")
                .failureForwardUrl("/login-error")
            .and()
            .logout()
                .logoutUrl("/logout")
                .invalidateHttpSession(true)
            .and()
            .csrf().disable();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(accountService)
            .passwordEncoder(new BCryptPasswordEncoder());
    }

    @Override
    public void configure(WebSecurity web) throws Exception {
        web.ignoring().requestMatchers("/static/**");
    }
}

How Failure Forwarding Works

Spring Security uses ForwardAuthenticationFailureHandler internally when failureForwardUrl() is set. This handler stores the exception under the key WebAttributes.AUTHENTICATION_EXCEPTION before dispatching the request:

public class ForwardAuthenticationFailureHandler implements AuthenticationFailureHandler {
    private final String forwardUrl;

    public ForwardAuthenticationFailureHandler(String forwardUrl) {
        Assert.isTrue(UrlUtils.isValidRedirectUrl(forwardUrl),
            () -> "'" + forwardUrl + "' is not a valid forward URL");
        this.forwardUrl = forwardUrl;
    }

    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        request.setAttribute(WebAttributes.AUTHENTICATION_EXCEPTION, exception);
        request.getRequestDispatcher(forwardUrl).forward(request, response);
    }
}

Controller-Level Exception Interpretation

A dedicated endpoint processes the forwarded request and maps specific AuthenticationException subtypes to user-friendly messages:

@Controller
public class AuthController {

    @GetMapping("/login-error")
    public String handleLoginError(HttpServletRequest req, Model model) {
        AuthenticationException ex = (AuthenticationException) 
            req.getAttribute(WebAttributes.AUTHENTICATION_EXCEPTION);

        String localizedMessage = switch (ex != null ? ex.getClass().getSimpleName() : "Unknown") {
            case "UsernameNotFoundException", "BadCredentialsException" ->
                "Invalid username or password.";
            case "DisabledException" ->
                "Your account has been deactivated.";
            case "LockedException" ->
                "Your account is temporarily locked.";
            case "AccountExpiredException" ->
                "Your account has expired.";
            case "CredentialsExpiredException" ->
                "Your credentials are no longer valid.";
            default ->
                "Authentication failed. Please try again.";
        };

        model.addAttribute("errorMessage", localizedMessage);
        return "login";
    }
}

This approach decouples security concerns from presentation logic while enabling precise, localized error messaging without exposing internal exception details.

Tags: spring-security spring-boot authentication-exception form-login webattributes

Posted on Sun, 14 Jun 2026 17:21:42 +0000 by peranha