Unveiling the Power of Spring Security 6.2 in Your Spring Boot Project
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:
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:
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.
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:
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:
In simple terms, the authenticationProvider() method makes sure our app checks users' identities safely.
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!