A Comprehensive Guide to Implementing Spring Security 6

24 / Sep / 2024 by Shubham Asthana 0 comments

Introduction

Spring Security 6 is a powerful framework that provides authentication, authorization, and protection against common attacks, such as CSRF, session fixation, and clickjacking. As of Spring Boot 3, Spring Security 6 is now the default security version integrated within the application. This guide walks you through the process of implementing Spring Security 6 in a Spring Boot 3 application, explains the role of each component, and highlights best practices to ensure your application is secure and efficient.

Prerequisites

Before starting, ensure the following prerequisites are met:

  • Java 17: This is the baseline version for Spring Boot 3 and Spring Security 6.
  • Spring Boot 3.3.1: In this guide, we will use version 3.3.1. However, you can use the latest available version.

Read More: Step By Step Guide : Sending Emails in Spring Boot

Why Spring Security?

Spring Security is an essential framework in modern applications. It handles critical tasks such as user authentication, access control, and protecting against attacks. By default, Spring Security comes with a sensible configuration that ensures basic security features like form-based login, session management, and CSRF protection. However, in most cases, you’ll need to customize this configuration to meet your application’s requirements.

Spring Security 6 Key Changes and Deprecations

In Spring Security 6, some major features and classes were deprecated to streamline and modernize the framework:

  • Deprecated: WebSecurityConfigurerAdapter, authorizeRequests, and AntMatcher.
  • Replaced By:
    • SecurityFilterChain for defining security filters.
    • authorizeHttpRequests for configuring access controls.
    • requestMatchers replaces AntMatcher and MvcMatcher

The shift from these deprecated methods encourages a more modular, component-based approach to configuring security in a Spring Boot application.

Adding Spring Security Dependency

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>

This dependency enables the default security configuration and includes everything needed to start securing your application.

Spring Boot Security Auto Configuration Features

Spring Boot automatically configures Spring Security with several key features:

  • Servlet Filter: A SpringSecurityFilterChain bean is created to manage all security filters.
  • Default Login Form: A login form is provided at /login.
  • Generated User Credentials: A default user (user) and password are generated and printed in the console during startup.
  • CSRF Protection: Enabled by default to prevent Cross-Site Request Forgery attacks.
  • Logout Support: A default /logout endpoint is provided.

This automatic configuration can be modified and extended to suit the needs of your application.

Creating Entity

Person Entity:

In Spring Security, UserDetails is a central interface that represents a user’s information for authentication purposes. It serves as a container for essential details required by Spring Security to verify a user’s identity and determine their access rights.

@Entity
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class Person implements UserDetails {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;
    String name;
    String contact;
    String password;
    @Column(unique = true)
    String email;

    @ManyToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinTable(name = "person_role",
            joinColumns = @JoinColumn(name = "person_id"),
            inverseJoinColumns = @JoinColumn(name = "role_id"))
    List<GrantedAuthorityImpl> grantedAuthorities;

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return grantedAuthorities;
    }

    @Override
    public String getPassword() {
        return password;
    }

    @Override
    public String getUsername() {
        return email;
    }
}

Role Entity:

In Spring Security, GrantedAuthority is a fundamental interface that represents an authorization granted to a user. It defines a permission or privilege that a user possesses within the application.

@Entity
@Data
@FieldDefaults(level = AccessLevel.PRIVATE)
public class GrantedAuthorityImpl implements GrantedAuthority {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    Long id;

    String authority;

    @Override
    public String getAuthority() {
        return authority;
    }
}

Creating Repository

Person Repository:

public interface PersonRepo extends JpaRepository<Person, Long> {
    Optional<Person> findByEmail(String email);
}

Note: I’ll use constructor injection throughout this project

  • @RequiredArgsConstructor: Lombok generates a constructor for all final fields.
  • Constructor Injection: Spring Boot uses this generated constructor to inject dependencies.
  • Automatic Injection: Dependencies are provided by Spring at runtime.

Implementing UserDetailsService

UserDetailsService is an interface used to retrieve user-related data. It is a core interface for authentication, specifically designed to load user information such as username, password, and roles (authorities).

Key Functionality

  • Main Purpose: It provides a way to retrieve the user details (like username, password, roles) from a persistent storage (e.g., a database, LDAP, etc.) for authentication.
  • Method to Implement: The UserDetailsService interface has only one method
@Service
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class UserDetailServiceImpl implements UserDetailsService {

    final PersonRepo personRepo;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        return personRepo.findByEmail(username).orElseThrow(() ->
                new UsernameNotFoundException("User not found with email: " + username));
    }
}

Controller Class

@RestController
@RequestMapping("/person")
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class PersonController {

 final PersonService personService;

 @PostMapping
 public String savePerson(@RequestBody @Valid PersonSaveCO personSaveCO) {
   return personService.savePerson(personSaveCO);
 }

 @GetMapping("/{id}")
 public Person getPerson(@PathVariable Long id) {
   return personService.getPerson(id);
 }

 @GetMapping
 @Secured("ROLE_ADMIN")
 public List<Person> getAllPersons() {
   return personService.getAllPersons();
 }

 @PutMapping
 public String updatePerson(@RequestBody @Valid PersonUpdateCO personUpdateCO) {
   return personService.updatePerson(personUpdateCO);
 }

 @DeleteMapping("/{id}")
 public String deletePerson(@PathVariable Long id) {
   return personService.deletePerson(id);
 }
}

Service Class

@Service
@RequiredArgsConstructor
@FieldDefaults(level = AccessLevel.PRIVATE)
public class PersonService {

 final PersonRepo personRepo;
 final ModelMapper modelMapper;
 final PasswordEncoder passwordEncoder;

 public String savePerson(PersonSaveCO personSaveCO) {
   Person person = personRepo.findByEmail(personSaveCO.getEmail()).orElse(null);
   if (person != null) {
    return "Already Exist";
   }

   person = modelMapper.map(personSaveCO, Person.class);
   GrantedAuthorityImpl grantedAuthority = new GrantedAuthorityImpl();
   grantedAuthority.setAuthority(personSaveCO.getRole());
   person.setGrantedAuthorities(Collections.singletonList(grantedAuthority));

 // Encrypt password
   person.setPassword(passwordEncoder.encode(personSaveCO.getPassword()));
   personRepo.save(person);
   return "Person Saved";
 }

 public Person getPerson(Long id) {
    return personRepo.findById(id).orElse(null);
 }

 public List<Person> getAllPersons() {
    return personRepo.findAll();
 }

 public String updatePerson(PersonUpdateCO personUpdateCO) {
    Person person = personRepo.findById(personUpdateCO.getId()).orElse(null);
    if (person == null) {
     return "Person does not exist";
    }

   if (personUpdateCO.getPassword() != null) {
    personUpdateCO.setPassword(passwordEncoder.encode(personUpdateCO.getPassword()));
   }

   modelMapper.map(personUpdateCO, person);
   personRepo.save(person);
   return "Person Updated";
}

 public String deletePerson(Long id) {
    personRepo.deleteById(id);
    return "Person Deleted Successfully";
 }
}

Here in Controller and Service we have used SaveCO classes to request the body,  model mapper to map the object to desired class / destination, you can use the simple operation here to test the Spring security.
Also you can see @Secured(“ROLE_ADMIN”) we will discuss this in next section.

Security Configuration class

  • In previous versions of Spring Security, you had to extend the WebSecurityConfigurerAdapter class to configure security settings. This class has been deprecated and removed in Spring Security 6. Instead, you should now take a more component-based approach and create a bean of type SecurityFilterChain.
  • Instead of using authorizeRequests, which has been deprecated, you should now use authorizeHttpRequests. This method is part of the HttpSecurity configuration and allows you to configure fine-grained request matching for access control.
  • In Spring Security 6, AntMatcher, MvcMatcher, and RegexMatcher have been deprecated and replaced by requestMatchers for path-based access control. This allows you to match requests based on patterns or other criteria without relying on specific matchers.
  • In Spring Security 6, the @EnableWebSecurity annotation is no longer required to configure security. This is because Spring Boot automatically provides default configurations for security. You can directly create a SecurityFilterChain bean to define your custom security configuration, and Spring Boot will apply it without needing @EnableWebSecurity.
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true):
  • It allows you to define access control restrictions directly on your methods using annotations.
  • The specific options used in this case activate support for two main method security annotation types:
    • @Secured: This Spring Security annotation lets you specify required roles for a method. Only users with those roles will be authorized to access the method.
    • @RolesAllowed: This annotation from the JSR-250 standard (Java Security API) serves the same purpose as @Secured. With jsr250Enabled=true, Spring Security recognizes and processes both annotations interchangeably.

Authentication Provider and Authentication Manager

  • The DaoAuthenticationProvider bean provides the implementation for user authentication based on a UserDetailsService. The DaoAuthenticationProvider defines the specific authentication logic and password comparison
  • The AuthenticationManager bean orchestrates the authentication process by delegating tasks to the AuthenticationProvider. The AuthenticationManager acts as the central point for authentication requests, ultimately relying on the AuthenticationProvider to perform the actual work.
@Configuration
@RequiredArgsConstructor
@EnableMethodSecurity(securedEnabled = true, jsr250Enabled = true)
@FieldDefaults(level = AccessLevel.PRIVATE)
public class WebSecurity {

    final UserDetailsService userDetailsService;

    @Bean
    SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .csrf(AbstractHttpConfigurer::disable)
                .authorizeHttpRequests((requests) -> requests
                         .requestMatchers(HttpMethod.POST, "/person").anonymous() 
                         .requestMatchers(HttpMethod.PUT, "/person").permitAll()
                         .requestMatchers(HttpMethod.DELETE, "/person/{id}").hasRole("ADMIN") 
                        .anyRequest().authenticated()
                )
                .exceptionHandling(exceptionHandling ->
                        exceptionHandling.authenticationEntryPoint(new AppAuthenticationEntryPoint())
                                .accessDeniedHandler(new CustomAccessDeniedHandler()
                                )
                )
                .httpBasic(Customizer.withDefaults());
        return http.build();
    }

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Bean
    public AuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider authenticationProvider = new DaoAuthenticationProvider();
        authenticationProvider.setUserDetailsService(userDetailsService);
        authenticationProvider.setPasswordEncoder(passwordEncoder());
        return authenticationProvider;
    }

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }
}

Custom Exception Handling

Spring Security allows customization of how exceptions like unauthorized access or failed authentication are handled.

AccessDeniedHandler:

  • An AccessDeniedException is thrown when a user’s authorities (roles and permissions) don’t match the requirements for a specific resource or operation.
@Component
public class CustomAccessDeniedHandler implements AccessDeniedHandler {

    @Override
    public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json");
        response.getWriter().write("Access Denied");
    }
}

AuthenticationEntryPoint:

  • An AuthenticationException is thrown when a user hasn’t gone through the login process or their authentication attempt was unsuccessful (e.g., invalid credentials).
@Component
public class AppAuthenticationEntryPoint implements AuthenticationEntryPoint {

    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
        response.setStatus(HttpServletResponse.SC_FORBIDDEN);
        response.setContentType("application/json");
        response.getWriter().write("UnAuthorized");
    }
}

Conclusion

Spring Security 6 introduces a more modular and powerful approach to securing your Spring Boot applications. By following this guide, you can implement robust security measures while adhering to industry best practices. We explored the key changes, from configuring SecurityFilterChain to handling authentication and customizing security for REST APIs.

FOUND THIS USEFUL? SHARE IT

Leave a Reply

Your email address will not be published. Required fields are marked *