Securing Your Spring Boot Application with Spring Security
Security is a paramount concern for any application, and when you’re developing a Spring Boot application, you have a powerful tool at your disposal: Spring Security. Spring Security is a robust and highly customizable framework that allows you to control access to your application, manage authentication, and protect your resources. In this blog, we’ll delve into the world of Spring Security, exploring its concepts, components, and providing a hands-on implementation example.
What is Spring Security?
Spring Security is an extension of the Spring Framework that provides comprehensive security features for Java applications. It is used to secure web applications and web services, including REST APIs. Spring Security’s core purpose is to handle authentication and authorization. With Spring Security, you can easily manage user authentication, password storage, access control, and more.
Key Concepts of Spring Security
Before diving into the implementation, let’s understand some key concepts of Spring Security:
Authentication: Authentication is the process of verifying the identity of a user. Spring Security supports various authentication mechanisms, such as form-based, basic authentication, OAuth, and more.
Authorization: Authorization is the process of determining whether an authenticated user has permission to access a specific resource or perform a specific action. Spring Security allows you to define access rules using expressions or configuration.
User Details Service: Spring Security typically relies on a User Details Service to load user details (e.g., username and password) from a data store (usually a database).
Principal and Authorities: In Spring Security, the authenticated user is represented as a principal with associated Authorities (roles or permissions). You can control access to resources based on these authorities.
Security Filter Chain: Spring Security is implemented as a chain of filters that intercept incoming requests and perform various security-related tasks. These filters handle tasks like authentication, authorization, session management, and more.
Now that we have a basic understanding of Spring Security, let’s implement it in a Spring Boot application.
Create a Spring Boot Project
You can use the Spring Initializer to create a new Spring Boot project with the necessary dependencies. Include “Spring Web” and “Spring Security.”
The main class of the Spring Boot application has the @SpringBootApplication annotation, which marks it as the starting point of the application. When you run this class, it starts the Spring Boot application.
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
When run, the console logs print the default password that was randomly generated as a part of the default security configuration. However, we must implement our own credentials based on our needs. So, let’s look at how we can secure our application. The overall flow of the architecture looks like this:
Entity Classes
To implement our own username and password, we need two entity classes, a user class to store user information and have roles associated with them through many to many relationships. Role class defines roles for users. Product class to check our credentials by implementing two endpoints, one all users can access, and the other only admin can access.
@Data
@Entity
@AllArgsConstructor
@NoArgsConstructor
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private String email;
private String password;
@ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
@JoinTable(name = “users_roles”, joinColumns = {
@JoinColumn(name = “user_id”, referencedColumnName = “id”) }, inverseJoinColumns = {
@JoinColumn(name = “role_id”, referencedColumnName = “id”) })
private List<Role> roles;
public User(String name, String email, String password, List<Role> roles) {
this.name = name;
this.email = email;
this.password = password;
this.roles = roles;
}
}
************
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String roleName;
public Role(String roleName){
this.roleName = roleName;
}
}
****************
@Entity
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Product {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String type;
private Long quantity;
}
Repository Interface
User Repository, Role Repository and Product Repository provides us basic crud operations for the User, Role and Product entity, and it allows us to interact with the role, user and product data in the database.
public interface UserRepository extends JpaRepository<User, Long> {
}
***************
public interface RoleRepository extends JpaRepository<Role, Long> {
}
****************
public interface ProductRepository extends JpaRepository<Product, Long> {
Product findByType(String type);
}
Product Service Class
Product service implementation of the product service interface provides methods for retrieving and adding products, interacting with the Product repository.
public interface ProductService {
public Product getProduct(Long id);
public User addProduct(Product product);
}
*********
@Service
@RequiredArgsConstructor
public class ProductServiceImpl implements ProductService {
private final ProductRepository productRepository;
@Override
public Product getProduct(String type) {
return productRepository.findByType(type);
}
@Override
public Product addProduct(Product product) {
return productRepository.save(product);
}
}
Controller Class
I implemented a user controller with two endpoints for getting and adding products to test our application.
- The @GetMapping(“/{type}”) endpoint retrieves a product by their type.
- The @PostMapping(“/add”) endpoint adds a new product.
@RestController
@RequiredArgsConstructor
@RequestMapping(“/products”)
public class UserController {
private final ProductService productService;
@GetMapping(“/{type}”)
public ResponseEntity<Product> getProduct(@PathVariable String type){
return new ResponseEntity<Product>(productService.getProduct(type), HttpStatus.OK);
}
@PostMapping(value = “/add”)
public ResponseEntity<Product> addProduct(@RequestBody Product product){
return new ResponseEntity<Product>(productService.addProduct(product), HttpStatus.OK);
}
}
User Details Service
The CustomUserDetailsService class implements the UserDetailsService interface. It loads user details from the database and converts them into CustomUserDetails. This service is essential for retrieving user information, including roles, for authentication and authorization. The CustomUserDetails class implements the UserDetails interface and represents custom user details. It is used for authenticating and authorizing users in Spring Security.
@Data
public class CustomUserDetails implements UserDetails {
private String email;
private String password;
private List<Role> roles;
public CustomUserDetails(User user) {
this.email = user.getEmail();
this.password = user.getPassword();
this.roles = user.getRoles();
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
String[] userRoles = getRoles().stream().map((role) -> role.getRoleName()).toArray(String[]::new);
Collection<GrantedAuthority> authorities = AuthorityUtils.createAuthorityList(userRoles);
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return email;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
*************
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
private UserRepository userRepository;
@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email);
if (user == null) {
throw new UsernameNotFoundException(“User not found”);
}
// Extract roles and convert them to GrantedAuthority
Collection<GrantedAuthority> authorities = new HashSet<>();
for (Role role : user.getRoles()) {
authorities.add(new SimpleGrantedAuthority(“ROLE_” + role.getRoleName()));
}
// Create CustomUserDetails with username, password, and authorities
CustomUserDetails userDetails = new CustomUserDetails(new User(user.getEmail(),
user.getPassword(), user.getRoles())
);
return userDetails;
}
}
Security Filter
With @EnableWebSecurity the Applicationsecurity class is responsible for configuring Spring Security for the application. It defines the security filter chain, authentication manager, and authentication provider.
It configures HTTP security rules, allowing public access to certain endpoints and restricting access to others. It uses BCryptPasswordEncoder for password encoding and hashing. It sets up a custom UserDetailsService (CustomUserDetailsService) to retrieve user details and roles. It creates an AuthenticationProvider to verify user credentials.
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class ApplicationSecurity {
private final CustomUserDetailsService customUserDetailsService;
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception{
return config.getAuthenticationManager();
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
httpSecurity
.csrf((csrf) -> csrf.disable())
.authorizeHttpRequests((authorizeHttpRequests) ->
authorizeHttpRequests
.requestMatchers(HttpMethod.POST,”/products/add”).hasAuthority(“Admin”)
.anyRequest().authenticated()
)
.httpBasic(withDefaults());
return httpSecurity.build();
}
@Bean
public AuthenticationProvider authenticationProvider(){
DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
authenticationProvider.setUserDetailsService(customUserDetailsService);
authenticationProvider.setPasswordEncoder(passwordEncoder());
return authenticationProvider;
}
}
In summary, we’ve covered the main classes and their roles in securing the application. Spring Security is a crucial aspect of building secure web applications, and understanding its components is essential for creating robust and protected systems.