Unveiling the Power of Spring Security 6.2 in Your Spring Boot Project

Unveiling the Power of Spring Security 6.2 in Your Spring Boot Project

[GitHub] Project Source Code for Reference

Navigating the latest enhancements in Spring Security opens a door to strengthening your Spring Boot applications. This guide is your compass as we seamlessly integrate these updates into your Spring Boot project with the Spring Security module.

What's Fresh in Spring Security 6.2:

Spring Security 6.2 brings exciting changes, redefining how security is woven into Spring Boot applications. Here's what's in store:

  1. Welcome RequestMatchers, Farewell AntMatchers: Embrace versatility with RequestMatchers, and bid adieu to rigid matching patterns. These offer a dynamic way to match requests, allowing for greater flexibility in security configurations.
  2. It's time to move on from WebSecurityConfigurerAdapter. Embrace newer, more efficient configuration methods as we bid farewell to this class.
  3. Streamlining with DSL Lambdas: Say hello to cleaner, more concise security configurations. DSL Lambdas replace older methods, offering enhanced flexibility and readability in your code.
  4. Meet the SecurityFilterChain: Say hello to clarity and customization with the SecurityFilterChain. This revolutionary approach simplifies the configuration of security filters, enhancing customization and clarity.

In the following sections, we'll dive into each of these changes, uncovering their significance and practical implications.

Securing your web application can feel like navigating a maze. However, by the end of this guide, you'll be equipped to seamlessly integrate Spring Security into any Spring Boot 3 web application.

Remember, whether your web application is small or large, configuring security is a one-time task, with room for modifications as your application evolves.

Chapter 0: Introduction

Embark on a journey to secure your web application developed with the latest version of Spring Boot. We'll explore integrating Spring Security's newest updates into your project, coupled with the power of PostgreSQL database integration via Spring Data JPA.

This article is split into two main parts, each offering valuable insights into fortifying your Spring Boot application with the latest in Spring Security.

Part One: Crafting an Employee Service with CRUD Operations

In this foundational phase, our focus is on crafting an efficient employee service that encompasses essential CRUD (Create, Read, Update, Delete) operations. Through this, we establish the bedrock for our system, paving the way for subsequent enhancements and refinements.

Part Two: Strengthening Endpoint Security with Spring Security

The essence of our discourse lies in fortifying our endpoints with the robust security measures offered by Spring Security.

We'll delve into securing our application, ensuring that sensitive endpoints are accessible only to authorized users. This crucial step not only enhances the integrity and confidentiality of our employee service but also augments its overall reliability and trustworthiness.

Chapter 1: Building a Simple Employee Service

While our primary focus remains on securing the application, we'll provide a basic overview of building a CRUD application for better context.

To kickstart our project, we've included the following dependencies:

  • Spring Web: Empowers the development of web applications with Spring, facilitating the creation of RESTful services.
  • PostgreSQL Driver: Establishes a connection between your application and a PostgreSQL database for efficient data storage.
  • Spring Data JPA: Simplifies data access and manipulation by offering JPA repositories.
  • Lombok: Drastically reduces boilerplate code by automatically generating essential methods like getters, setters, and constructors.
  • Spring Boot DevTools: Offers swift application restarts, live reload capabilities, and configuration options, ensuring a seamless development experience.

Employee.java

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Employee {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long employeeId;
    private String name;
    private String email;
    private String department;
    private String company;

}
        

EmployeeRepository.java

This interface defines a repository for Employee entities, extending Spring Data JPA to facilitate database operations.

public interface EmployeeRepository extends JpaRepository<Employee,Integer>{
}        

EmployeeController.java

This controller manages Employee data, offering endpoints to create, retrieve, update, and delete employees using an EmployeeService.

@RestController
@RequestMapping("/employees")
public class EmployeeController {

    private final EmployeeService service;

    public EmployeeController(EmployeeService service) {
        this.service = service;
    }

    @GetMapping
    public List<Employee> getAllEmployees() {
        return service.findAll();
    }

    @GetMapping("/{id}")
    public ResponseEntity<Employee> getEmployeeById(@PathVariable Integer id) {
        return service.findById(id)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @PostMapping
    public Employee createEmployee(@RequestBody Employee employee) {
        return service.save(employee);
    }


    @PutMapping("/{id}")
    public ResponseEntity<Employee> updateEmployee(@PathVariable Integer id, @RequestBody Employee employeeDetails) {
        return service.updateEmployee(id, employeeDetails)
                .map(ResponseEntity::ok)
                .orElseGet(() -> ResponseEntity.notFound().build());
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<?> deleteEmployee(@PathVariable Integer id) {
        return service.findById(id)
                .map(employee -> {
                    service.deleteById(id);
                    return ResponseEntity.ok().build();
                })
                .orElseGet(() -> ResponseEntity.notFound().build());
    }
}
        

EmployeeService.java

The EmployeeService class interact with the EmployeeRepository to perform CRUD operations on Employee entities, allowing for the creation, retrieval, updating, and deletion of employee records in the database.

@Service
public class EmployeeService {

    private final EmployeeRepository repository;

    public EmployeeService(EmployeeRepository repository) {
        this.repository = repository;
    }

    public List<Employee> findAll() {
        return repository.findAll();
    }

    public Optional<Employee> findById(Integer id) {
        return repository.findById(id);
    }

    public Employee save(Employee employee) {
        return repository.save(employee);
    }

    public void deleteById(Integer id) {
        repository.deleteById(id);
    }

    public Optional<Employee> updateEmployee(Integer id, Employee employeeDetails) {
        return repository.findById(id).map(employee -> {
            employee.setName(employeeDetails.getName());
            employee.setEmail(employeeDetails.getEmail());
            employee.setDepartment(employeeDetails.getDepartment());
            employee.setCompany(employeeDetails.getCompany());
            return Optional.of(repository.save(employee));
        }).orElse(Optional.empty());
    }

}
        

Create schema.sql and data.sql so that application can create populate and create some data on startup

schema.sql

CREATE TABLE employee (
                          employee_id SERIAL PRIMARY KEY,
                          name VARCHAR(255),
                          email VARCHAR(255),
                          department VARCHAR(255),
                          company VARCHAR(255)
);
        

data.sql

INSERT INTO Employee (name, email, department, company) VALUES ('Mukesh Chaubey', '[email protected]', 'IT', 'Test tech ltd');
INSERT INTO Employee (name, email, department, company) VALUES ('Test Record', '[email protected]', 'Marketing', 'Test tech ltd');
        

application.yaml

server:
    port: 8080
spring:
    application:
        name: <application-name>
    datasource:
        url: jdbc:postgresql://localhost:5432/<dbName>
        username: <username>
        password: <password>
    jpa:
        hibernate:
            ddl-auto: update
        properties:
            hibernate:
                dialect: org.hibernate.dialect.PostgreSQLDialect
        show-sql: true
    sql:
        init:
            mode: always        


Chapter 2.0: Protecting our Web Application using Default Spring Security Configuration

Insight: Integrating Spring Security into Your Spring Boot Project Instantly Bolsters Security

When Spring Security joins your Spring Boot project, safety becomes the default setting. This deliberate choice by the Spring creators ensures that every application starts off on a secure footing.

How Does It Work?

The moment Spring Security becomes part of your project, it springs into action, configuring essential security features for you. This means your application receives a foundational layer of security without any additional effort on your part.

To fortify our web application, we'll need to include another dependency: Spring Security.

  • Spring Security: This powerhouse adds authentication and authorization features, elevating the security posture of your application.

For those utilizing Maven/Gradle and Spring Boot for their project, the dependency declaration for Spring Security would be found in the pom.xml/build.gradle file, resembling the snippet below:

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

or

implementation 'org.springframework.boot:spring-boot-starter-security:3.2.3'
        

Our final build.gradle file would be structured as follows to incorporate the specified dependencies:

plugins {
	id 'java'
	id 'org.springframework.boot' version '3.2.5'
	id 'io.spring.dependency-management' version '1.1.4'
}

group = 'com.example'
version = '0.0.1-SNAPSHOT'

java {
	sourceCompatibility = '17'
}

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-security'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'org.postgresql:postgresql'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
	useJUnitPlatform()
}        

Insight:

Default Login Form and Limitations of Default Spring Security Setup

Upon integrating the security dependency and launching your Spring Boot project, a default login form is automatically provided, equipped with fields for a username and password.

When attempting to access any API via the browser, this default login form will be presented. The default username provided by Spring Security is "user," while the password is auto-generated and can be found in the console. However, it's important to note that this username-password combination is not suitable for real-time production scenarios.

Here's what the default login form might look like:


However, relying solely on the default Spring Security setup comes with several limitations:

  1. Secures Everything: By default, it locks down all endpoints, including those you may want to keep open.
  2. Not Flexible Enough: The preset security settings are quite general, which may not align with the specific security requirements of your application.
  3. Easy to Misconfigure: Without careful attention, sticking to default settings could result in security vulnerabilities or tricky bugs.
  4. One-Size-Fits-All Approach: It treats all applications uniformly in terms of security, which may not be suitable for apps with unique security needs.

To gain more precise control over your application's security mechanisms, such as defining custom usernames and passwords and implementing password encryption for enhanced authentication and authorization of API access, you'll need to create a custom security configuration file. Spring Security offers robust flexibility for such customizations.

However, it's important to acknowledge that customizing the security setup can be challenging and may require complex coding efforts. Nevertheless, the payoff in terms of tailored security solutions is worth the investment.

Chapter 2.1: Securing web application with our own custom security configuration

To enable security, we need to create a user class that includes fields such as username and password. This allows us to store user information in the database and authenticate users based on these credentials.

However, there’s an important aspect to note: Spring Security does not automatically recognize this custom user class. Instead, it works with its predefined UserDetails interface.

In simple terms, UserDetails is a special interface in Spring Security designed to handle user information in a way that Spring Security can understand. This means that for Spring Security to work with our custom user class, we need to adapt our class to fit this interface. Essentially, we need to convert our user class into one that implements the UserDetails interface.

@Entity
@Data
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private int id;
    private String username;
    private String password;

}        

Implementing the UserDetails interface provided by spring security:

UserPrinciple.java

public class UserPrinciple implements UserDetails {

    private User user;

    public UserPrinciple(User user) {
        this.user = user;
    }

    @Override
    public Collection<? extends GrantedAuthority> getAuthorities() {
        return Collections.singleton(new SimpleGrantedAuthority("USER"));
    }

    @Override
    public String getPassword() {
        return user.getPassword();
    }

    @Override
    public String getUsername() {
        return user.getUsername();
    }

    @Override
    public boolean isAccountNonExpired() {
        return true;
    }

    @Override
    public boolean isAccountNonLocked() {
        return true;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return true;
    }

    @Override
    public boolean isEnabled() {
        return true;
    }
}

        

UserRepo.java

@Repository
public interface UserRepo extends JpaRepository<User, Integer> {

    public User findByUsername(String username);
}
        

UserService.java

@Service
public class UserService implements UserDetailsService {
    @Autowired
    private UserRepo userRepo;
    private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);

    public User saveUser(User user) {
        user.setPassword(encoder.encode(user.getPassword()));
        return userRepo.save(user);
    }

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        User user = userRepo.findByUsername(username);
        if (user == null) {
            throw new UsernameNotFoundException("Error 404");
        } else {
            return new UserPrincipal(user);
        }
    }
}
        

UserSevice is implementing UserDetailService.

UserDetailsService is an interface provided by Spring Security that is used to retrieve user-related data. It has a single method, loadUserByUsername(String username), which must be implemented to fetch a UserDetails object based on the username. The UserDetails interface itself is a core part of Spring Security, providing essential information (such as username, password, and granted authorities) necessary for security checks.

Let's see how it works:-

User Repository and Password Encoder: So, in this class, we kinda connect to our database to get user info. And to keep those passwords safe, we use this thing called BCryptPasswordEncoder. It sort of mixes up the passwords so they're super hard to figure out or steal.

saveUser Method: Whenever we gotta save info for a new user, we use this method. It's pretty cool because it first scrambles up the user's password using BCryptPasswordEncoder, and then it saves all their stuff to our database. This way, even if someone sneaks into our database, they won't be able to easily crack the passwords.

loadUserByUsername Method: This one's all about finding the right user when someone's trying to log in. It looks for a user by their username. If it finds them, it kinda gets their info ready in a special way needed for checking who they are when they log in. And if it can't find the user, it kinda throws up an error to let us know, which stops strangers from getting in.

So, yeah, the UserService is pretty important for keeping user info safe and making sure only the right person logs in with the right password.

UserController.java

@RestController
@CrossOrigin
public class UserController {

    @Autowired
    private UserService userService;

    @PostMapping("/register")
    public ResponseEntity<String> userRegister(@RequestBody User user) {
        if (userService.saveUser(user) != null) {
            return new ResponseEntity<>("User Registered Successfully", HttpStatus.OK);
        } else {
            return new ResponseEntity<>("Oops! User not registered", HttpStatus.OK);
        }
    }
}
        

Creating separate controller class which a single post api ,using which new user can registered themselves.

And the most important class i.e. SecurityConfig.java where we define all our latest security-related beans.

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    @Autowired
    private UserDetailsService userDetailsService;

    @Bean
    public DaoAuthenticationProvider authenticationProvider() {
        DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
        provider.setUserDetailsService(userDetailsService);
        provider.setPasswordEncoder(new BCryptPasswordEncoder(12));
        return provider;
    }

@Bean
CorsConfigurationSource corsConfigurationSource() {
    CorsConfiguration configuration = new CorsConfiguration();
    configuration.setAllowedOrigins(Arrays.asList("<hostName>"));
    configuration.setAllowedMethods(Arrays.asList("GET", "POST", "PUT", "DELETE", "OPTIONS"));
    configuration.setAllowCredentials(true);
    configuration.setAllowedHeaders(Arrays.asList("Authorization", "Content-Type"));
    configuration.setExposedHeaders(Arrays.asList("Authorization"));

    UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
    source.registerCorsConfiguration("/**", configuration);
    return source;
}

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity httpSecurity) throws Exception {
        httpSecurity.csrf(AbstractHttpConfigurer::disable)
                        .cors(c -> c.configurationSource(corsConfigurationSource()))
                .authorizeHttpRequests(auth ->
                        auth
                                .requestMatchers("/register")
                                .permitAll().anyRequest()
                                .authenticated())
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .httpBasic(Customizer.withDefaults());


        return httpSecurity.build();
    }


}

        

Comparison with the Old Spring Security Versions:

Back in the day, we used something called AntMatchers to decide which URLs folks could access in Spring Security. But now, we've got RequestMatchers, which are a bit fancier. They not only handle URL patterns but also think about stuff like the type of HTTP request. This upgrade lets us get more specific and flexible with our security setups.

Remember how we used to extend the WebSecurityConfigurerAdapter class to set up security stuff? Well, turns out Spring Security isn't a fan of that anymore. They want us to be more modern and direct, so now we're encouraged to configure a SecurityFilterChain bean instead. It's supposed to be less rigid and easier to tweak when needed.

And hey, have you heard about DSL Lambdas? They're the new cool kids on the block. They make our security config code look a lot cleaner and easier to understand. No more verbose setups - just straightforward and concise rules.

Oh, and the SecurityFilterChain thing? It's a game-changer. Instead of dumping all our security settings in one huge class, we now split them up into smaller bits with the SecurityFilterChain bean. It's like organizing your closet - everything's easier to find and manage.

Now, onto the SecurityConfig class:

  1. authenticationProvider() Method: This part is all about figuring out who's trying to get into our app:

  • User Details Service: It's like a detective that checks if the username and password match up during logins.
  • Password Encoder: We use this fancy method (BCrypt) to hide passwords. Even if someone snoops around, they won't crack it easily.

In simple terms, the authenticationProvider() method makes sure our app checks users' identities safely.

  1. securityFilterChain(HttpSecurity httpSecurity) Method: Here, we lay down the rules for who can do what in our app:

  • CSRF Protection: We disable this thing to beef up security, especially for APIs.
  • Access Control: We say that anyone can use the /register page without logging in, but everything else needs authentication.
  • Session Management: We make sure our app doesn't remember any session info, which is good for stateless apps like APIs.
  • Basic Authentication: This makes sure users have to give a username and password with their requests.

By setting up the SecurityFilterChain, we tell our app how to handle security checks and who gets access to what. It's like being the bouncer at a club - only the right people get in.

We have created bean of CorsConfigurationSource and added in securityFilterChain which is used to allow cors request .Because @CrossOrigin annotation on @RestController class alone not gonna work when you add spring security.

Looking Ahead:

In the next articles, we're going to take a deeper dive into some advanced Spring Security topics. Get ready for:

Resource Access Control: We'll explore how to manage access to resources based on user authorization. It's like setting up VIP areas in your app!

JSON Web Tokens (JWT): Ever heard of JWT? We'll break it down and show you how to use it for secure authentication in Spring Boot. It's like having a secret code to enter the coolest parties.

Social Login (OAuth2) Integration: Want to let users log in with their social media accounts? We'll show you how to integrate OAuth2 with Spring Boot for seamless social login. It's like giving your users a VIP pass to your app!

So, stay tuned for more exciting adventures in the world of Spring Security. We've got lots more to explore together!



要查看或添加评论,请登录

社区洞察

其他会员也浏览了