Introduction
Handling security exceptions is a critical aspect of implementing robust security in any application. Spring Security provides mechanisms to handle various security-related exceptions, such as authentication failures, access denials, and more. This tutorial will guide you through the process of handling security exceptions in a Spring Boot application.
Prerequisites
Before starting, ensure you have the following installed:
- JDK 8 or later
- Maven or Gradle
- An IDE like IntelliJ IDEA or Eclipse
- Basic knowledge of Spring Boot and Spring Security
Step 1: Set Up Your Spring Boot Project
First, create a new Spring Boot project. You can use Spring Initializr (https://start.spring.io/) to generate the project. Select the following dependencies:
- Spring Web
- Spring Security
Download the project and import it into your IDE.
Step 2: Basic Spring Security Configuration
Start by creating a basic Spring Security configuration. Create a configuration class in src/main/java/com/example/demo/config/SecurityConfig.java
:
package com.example.demo.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Step 3: Create a UserDetailsService
To manage user authentication, create a custom UserDetailsService
. This service will load user-specific data. Create CustomUserDetailsService
in src/main/java/com/example/demo/service/CustomUserDetailsService.java
:
package com.example.demo.service;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Collections;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
// For demonstration purposes, we are using hardcoded users.
// In a real application, you would fetch user details from a database.
if ("user".equals(username)) {
return User.withUsername("user")
.password("$2a$10$DowJonesIndex12345")
.authorities("ROLE_USER")
.build();
} else if ("admin".equals(username)) {
return User.withUsername("admin")
.password("$2a$10$AdminPassword67890")
.authorities("ROLE_ADMIN")
.build();
} else {
throw new UsernameNotFoundException("User not found");
}
}
}
Update the security configuration to use this UserDetailsService
:
package com.example.demo.config;
import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.permitAll()
.and()
.logout()
.permitAll();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Step 4: Handle Authentication Exceptions
Custom Authentication Failure Handler
To handle authentication failures, you can create a custom authentication failure handler. This handler will be invoked when authentication fails. Create CustomAuthenticationFailureHandler
in src/main/java/com/example/demo/security/CustomAuthenticationFailureHandler.java
:
package com.example.demo.security;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CustomAuthenticationFailureHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
response.sendRedirect("/login?error=true");
}
}
Update the security configuration to use this custom handler:
package com.example.demo.config;
import com.example.demo.security.CustomAuthenticationFailureHandler;
import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureHandler(new CustomAuthenticationFailureHandler())
.permitAll()
.and()
.logout()
.permitAll();
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Step 5: Handle Access Denied Exceptions
Custom Access Denied Handler
To handle access denied exceptions, you can create a custom access denied handler. This handler will be invoked when a user tries to access a resource they are not authorized to access. Create CustomAccessDeniedHandler
in src/main/java/com/example/demo/security/CustomAccessDeniedHandler.java
:
package com.example.demo.security;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {
@Override
public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
response.sendRedirect("/access-denied");
}
}
Update the security configuration to use this custom handler:
package com.example.demo.config;
import com.example.demo.security.CustomAccessDeniedHandler;
import com.example.demo.security.CustomAuthenticationFailureHandler;
import com.example.demo.service.CustomUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
private CustomUserDetailsService userDetailsService;
@Autowired
private CustomAccessDeniedHandler accessDeniedHandler;
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/public/**").permitAll()
.anyRequest().authenticated()
.and()
.formLogin()
.loginPage("/login")
.failureHandler(new CustomAuthenticationFailureHandler())
.permitAll()
.and()
.logout()
.permitAll()
.and()
.exceptionHandling()
.accessDeniedHandler(accessDeniedHandler);
}
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Step 6: Create Error Pages
Create error pages to be displayed when exceptions occur.
Access Denied Page
Create access-denied.html
in src/main/resources/templates/
:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Access Denied</title>
</head>
<body>
<h1>Access Denied</h1>
<p>You do not have permission to access this page.</p>
<a href="/">Go to Home</a>
</body>
</html>
Login Page with Error Handling
Update login.html
to display error messages:
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<title>Login</title>
</head>
<body>
<h1>Login</h1>
<form th:action="@{/login}" method="post">
<div>
<label for="username">Username:</label>
<input type="text" id="username" name="username"/>
</div>
<div>
<label for="password">Password:</label>
<input type="password" id="password" name="password"/>
</div>
<div>
<button type="submit">Login</button>
</div>
</form>
<div th:if="${param.error}">
<p>Invalid username or password.</p>
</div>
</body>
</html>
Step 7: Test the Application
Run your Spring Boot application and test the following scenarios:
- Attempt to log in with incorrect credentials to trigger the authentication failure handler.
- Log in with a valid user and attempt to access a protected resource with insufficient permissions to trigger the access denied handler.
Conclusion
In this tutorial, you have learned how to handle security exceptions in Spring Security. You explored how to create custom handlers for authentication failures and access denied exceptions, providing a more user-friendly experience and better control over security-related events in your application. By implementing these handlers, you can ensure that your application handles security exceptions gracefully and securely.