Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Implementing Authentication, Login Flow and User Management in Spring Security 5.7.5 GA

Tech 1

Core Authentication Components

Spring Security's authentication subsystem relies on standardized interfaces to decouple different workflow stages:

  • Authentication: Stores the identity and granted permissions of the requesting party
  • AuthenticationManager: Orchestrates the full authentication verification workflow
  • UserDetails: Defines the standard structure of user identity data
  • UserDetailsService: Implements user data retrieval from arbitrary data sources
  • PasswordEncoder: Handles password hashing and verification to avoid plaintext storage
  • RememberMeService: Manages persistent login sessions across browser restarts

Login Success Handling

Authentication success workflows are implemented via implementations of the AuthenticationSuccessHandler interface, with three core built-in implementations:

  1. SimpleUrlAuthenticationSuccessHandler: Base implementation for redirect-based post-login navigation
  2. SaveRequestAwareAuthenticationSuccessHandler: Extends the above with request caching to redirect users back to the originally requested protected resource after login
  3. ForwardAuthenticationSuccessHandler: Implements server-side forward navigation after login All implementations execute logic via the onAuthenticationSuccess interface method.

defaultSuccessUrl Configuration

This setting uses SaveRequestAwareAuthenticationSuccessHandler by default, with the following behavior:

  • If a user accesses a protected resource without authentication, they are redirected back to that resource after successful login
  • If the user directly accesses the login page first, they are redirected to the configured path after login
  • Passing true as the second parameter switches the navigation method from client-side redirect to server-side forward
  • Default navigation uses client-side redirect
@Configuration
public class AppSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/custom-login.html")
                .loginProcessingUrl("/perform-login")
                .defaultSuccessUrl("/dashboard")
                .failureUrl("/custom-login.html?error")
                .usernameParameter("userAccount")
                .passwordParameter("userPwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }
}

successForwardUrl Configuration

This setting uses ForwardAuthenticationSuccessHandler exclusively:

  • Always uses server-side forward to the configured path regardless of the user's original request
  • Preserves request attributes across the navigation, making it suitable for passing login context data
@Configuration
public class AppSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/custom-login.html")
                .loginProcessingUrl("/perform-login")
                .successForwardUrl("/dashboard")
                .failureUrl("/custom-login.html?error")
                .usernameParameter("userAccount")
                .passwordParameter("userPwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }
}

Custom successHandler Implementation

Redirect with Dynamic Target Parameter

You can configure a custom SaveRequestAwareAuthenticationSuccessHandler to support dynamic redirect targets passed via login request parameters:

@Configuration
public class AppSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/custom-login.html")
                .loginProcessingUrl("/perform-login")
                .successHandler(cachedRequestSuccessHandler())
                .failureUrl("/custom-login.html?error")
                .usernameParameter("userAccount")
                .passwordParameter("userPwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }

    SaveRequestAwareAuthenticationSuccessHandler cachedRequestSuccessHandler() {
        SaveRequestAwareAuthenticationSuccessHandler handler = new SaveRequestAwareAuthenticationSuccessHandler();
        handler.setDefaultTargetUrl("/dashboard");
        handler.setTargetUrlParameter("redirectTo");
        return handler;
    }
}

Add the redirectTo parameter to your login form action to specify a custom post-login path, e.g. /perform-login?redirectTo=/hello.

Fixed Forward Path

Configure a custom ForwardAuthenticationSuccessHandler for fixed server-side forward navigasion:

@Configuration
public class AppSecurityConfig {
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/custom-login.html")
                .loginProcessingUrl("/perform-login")
                .successHandler(forwardAuthSuccessHandler())
                .failureUrl("/custom-login.html?error")
                .usernameParameter("userAccount")
                .passwordParameter("userPwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }

    ForwardAuthenticationSuccessHandler forwardAuthSuccessHandler() {
        return new ForwardAuthenticationSuccessHandler("/welcome");
    }
}

Separate Frontend/Backend Architecture

For REST APIs that return JSON instead of page navigation, implement a custom AuthenticationSuccessHandler:

  1. Standalone implementation class:
public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> respData = new HashMap<>();
        respData.put("code", 200);
        respData.put("message", "Login successful");
        ObjectMapper objectMapper = new ObjectMapper();
        String jsonResp = objectMapper.writeValueAsString(respData);
        response.getWriter().write(jsonResp);
    }
}

Register the handler in your security configuration:

.successHandler(new CustomAuthSuccessHandler())
  1. Anonymous inner class implementation (for simple use cases):
.successHandler(new AuthenticationSuccessHandler() {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> respData = new HashMap<>();
        respData.put("code", 200);
        respData.put("message", "Login successful");
        ObjectMapper objectMapper = new ObjectMapper();
        String jsonResp = objectMapper.writeValueAsString(respData);
        response.getWriter().write(jsonResp);
    }
})

Login Failure Handling

Login failure workflows are implemented via implementations of the AuthenticationFailureHandler interface, with five core built-in implementations:

  1. SimpleUrlAuthenticationFailureHandler: Base implementation for redirect-based failure navigation, with ExceptionMappingAuthenticationFailureHandler subclass that maps different authentication exceptions to different redirect paths
  2. ForwardAuthenticationFailureHandler: Implements server-side forward navigation on login failure
  3. AuthenticationEntryPointFailureHandler: Triggers authentication entry point workflows for unauthenticated requests
  4. DelegatingAuthenticationFailureHandler: Delegates failure handling to multiple registered handlers based on exception type All implementations execute logic via the onAuthenticationFailure interface method.

failureUrl Configuration

This setting uses SimpleUrlAuthenticationFailureHandler by default, using client-side redirect to the configured path, with error information passed via URL query parameters.

@Configuration
public class AppSecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/custom-login.html")
                .loginProcessingUrl("/perform-login")
                .defaultSuccessUrl("/dashboard")
                .failureUrl("/custom-login.html?loginFailed")
                .usernameParameter("userAccount")
                .passwordParameter("userPwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }
}

failureForwardUrl Configuration

This setting uses ForwardAuthenticationFailureHandler, using server-side forward to the configured path, preserving authentication exception attributes in the request for server-side rendering of error messages.

@Configuration
public class AppSecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/custom-login.html")
                .loginProcessingUrl("/perform-login")
                .defaultSuccessUrl("/dashboard")
                .failureForwardUrl("/custom-login.html")
                .usernameParameter("userAccount")
                .passwordParameter("userPwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }
}

Custom failureHandler Implemantation

Redirect Mode

@Configuration
public class AppSecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/custom-login.html")
                .loginProcessingUrl("/perform-login")
                .defaultSuccessUrl("/dashboard")
                .failureHandler(redirectFailureHandler())
                .usernameParameter("userAccount")
                .passwordParameter("userPwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }

    SimpleUrlAuthenticationFailureHandler redirectFailureHandler() {
        SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler("/custom-login.html?error");
        handler.setUseForward(false);
        return handler;
    }
}

Forward Mode

@Configuration
public class AppSecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .loginPage("/custom-login.html")
                .loginProcessingUrl("/perform-login")
                .defaultSuccessUrl("/dashboard")
                .failureHandler(forwardFailureHandler())
                .usernameParameter("userAccount")
                .passwordParameter("userPwd")
                .permitAll()
                .and()
                .csrf().disable()
                .build();
    }

    SimpleUrlAuthenticationFailureHandler forwardFailureHandler() {
        SimpleUrlAuthenticationFailureHandler handler = new SimpleUrlAuthenticationFailureHandler("/custom-login.html");
        handler.setUseForward(true);
        return handler;
    }
}

Separate Frontend/Backend Architecture

  1. Standalone implementation class:
public class CustomAuthFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> respData = new HashMap<>();
        respData.put("code", 401);
        respData.put("message", "Login failed: " + exception.getMessage());
        ObjectMapper objectMapper = new ObjectMapper();
        String jsonResp = objectMapper.writeValueAsString(respData);
        response.getWriter().write(jsonResp);
    }
}

Register the handler:

.failureHandler(new CustomAuthFailureHandler())
  1. Anonymous inner class implementation:
.failureHandler(new AuthenticationFailureHandler() {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("application/json;charset=UTF-8");
        Map<String, Object> respData = new HashMap<>();
        respData.put("code", 401);
        respData.put("message", "Login failed: " + exception.getMessage());
        ObjectMapper objectMapper = new ObjectMapper();
        String jsonResp = objectMapper.writeValueAsString(respData);
        response.getWriter().write(jsonResp);
    }
})

Logout Handling

logoutSuccessUrl Configuration

Basic logout configuration with a single logout endpoint and fixed post-logout navigation:

@Configuration
public class AppSecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .mvcMatchers("/custom-login.html").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .logout()
                .logoutUrl("/sign-out")
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessUrl("/custom-login.html?loggedOut")
                .and()
                .csrf().disable().build();
    }
}

Supported parameters:

  • logoutUrl: Specifies the logout request path, defaults to /logout with GET method allowed
  • invalidateHttpSession: Toggles session invalidation on logout, defaults to true
  • clearAuthentication: Toggles clearing of authenticated credentials on logout, defaults to true

logoutRequestMatcher Configuration

Use this setting to configure multiple logout endpoints with different HTTP methods:

.logoutRequestMatcher(new OrRequestMatcher(
    new AntPathRequestMatcher("/sign-out-get", "GET"),
    new AntPathRequestMatcher("/sign-out-post", "POST")
))

Custom logoutSuccessHandler Implementation

For separate frontend/backend architectures, return JSON responses on logout instead of page navigation:

@Configuration
public class AppSecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/sign-out-get", "GET"),
                        new AntPathRequestMatcher("/sign-out-post", "POST")
                ))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .logoutSuccessHandler(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.setContentType("application/json;charset=UTF-8");
                        Map<String, Object> respData = new HashMap<>();
                        respData.put("code", 200);
                        respData.put("message", "Logout successful");
                        ObjectMapper objectMapper = new ObjectMapper();
                        String jsonResp = objectMapper.writeValueAsString(respData);
                        response.getWriter().write(jsonResp);
                    }
                })
                .and()
                .csrf().disable().build();
    }
}

defaultLogoutSuccessHandlerFor Configuration

Use this setting to map different logout endpoints to different response behaviors:

@Configuration
public class AppSecurityConfig {
    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        return httpSecurity.authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .formLogin()
                .and()
                .logout()
                .logoutRequestMatcher(new OrRequestMatcher(
                        new AntPathRequestMatcher("/sign-out-get", "GET"),
                        new AntPathRequestMatcher("/sign-out-post", "POST")
                ))
                .invalidateHttpSession(true)
                .clearAuthentication(true)
                .defaultLogoutSuccessHandlerFor(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.setContentType("application/json;charset=UTF-8");
                        Map<String, Object> respData = new HashMap<>();
                        respData.put("code", 200);
                        respData.put("message", "GET logout successful");
                        ObjectMapper objectMapper = new ObjectMapper();
                        String jsonResp = objectMapper.writeValueAsString(respData);
                        response.getWriter().write(jsonResp);
                    }
                }, new AntPathRequestMatcher("/sign-out-get", "GET"))
                .defaultLogoutSuccessHandlerFor(new LogoutSuccessHandler() {
                    @Override
                    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                        response.setContentType("application/json;charset=UTF-8");
                        Map<String, Object> respData = new HashMap<>();
                        respData.put("code", 200);
                        respData.put("message", "POST logout successful");
                        ObjectMapper objectMapper = new ObjectMapper();
                        String jsonResp = objectMapper.writeValueAsString(respData);
                        response.getWriter().write(jsonResp);
                    }
                }, new AntPathRequestMatcher("/sign-out-post", "POST"))
                .and()
                .csrf().disable().build();
    }
}

Retrieve Authenticated User Data

SecurityContextHolder

For single-threaded environments, fetch the authentication object directly from the security context:

@GetMapping("/current-user")
public String getCurrentUser() {
    Authentication auth = SecurityContextHolder.getContext().getAuthentication();
    return auth.toString();
}

The security context is bound to the current thread by default, and will be automatically cleared at the end of the request lifecycle.

Controller Method Injection

Inject the authentication or principal object directly into controller handler methods:

@GetMapping("/auth-data")
public String getAuthenticationData(Authentication authentication) {
    return authentication.toString();
}

@GetMapping("/principal-data")
public String getPrincipalData(Principal principal) {
    return principal.toString();
}

User Definition and Datasource Configuration

In-Memory User Datasource

InMemoryUserDetailsManager provides an in-memory user store for development and testing scenarios:

  1. Configure via AuthenticationManagerBuilder:
@Autowired
public void configureInMemoryUsers(AuthenticationManagerBuilder authBuilder) throws Exception {
    InMemoryUserDetailsManager userManager = new InMemoryUserDetailsManager();
    UserDetails adminUser = User.withUsername("appAdmin")
            .roles("ADMIN")
            .password("{noop}SecurePass123")
            .build();
    userManager.createUser(adminUser);
    authBuilder.userDetailsService(userManager);
}
  1. Expose as a Spring bean:
@Bean
public UserDetailsService inMemoryUserService() {
    InMemoryUserDetailsManager userManager = new InMemoryUserDetailsManager();
    UserDetails adminUser = User.withUsername("appAdmin")
            .roles("ADMIN")
            .password("{noop}SecurePass123")
            .build();
    userManager.createUser(adminUser);
    return userManager;
}

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.