Implementing Authentication, Login Flow and User Management in Spring Security 5.7.5 GA
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 partyAuthenticationManager: Orchestrates the full authentication verification workflowUserDetails: Defines the standard structure of user identity dataUserDetailsService: Implements user data retrieval from arbitrary data sourcesPasswordEncoder: Handles password hashing and verification to avoid plaintext storageRememberMeService: 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:
SimpleUrlAuthenticationSuccessHandler: Base implementation for redirect-based post-login navigationSaveRequestAwareAuthenticationSuccessHandler: Extends the above with request caching to redirect users back to the originally requested protected resource after loginForwardAuthenticationSuccessHandler: Implements server-side forward navigation after login All implementations execute logic via theonAuthenticationSuccessinterface 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
trueas 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:
- 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())
- 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:
SimpleUrlAuthenticationFailureHandler: Base implementation for redirect-based failure navigation, withExceptionMappingAuthenticationFailureHandlersubclass that maps different authentication exceptions to different redirect pathsForwardAuthenticationFailureHandler: Implements server-side forward navigation on login failureAuthenticationEntryPointFailureHandler: Triggers authentication entry point workflows for unauthenticated requestsDelegatingAuthenticationFailureHandler: Delegates failure handling to multiple registered handlers based on exception type All implementations execute logic via theonAuthenticationFailureinterface 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
- 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())
- 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/logoutwith GET method allowedinvalidateHttpSession: Toggles session invalidation on logout, defaults totrueclearAuthentication: Toggles clearing of authenticated credentials on logout, defaults totrue
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:
- 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);
}
- 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;
}