Method level Spring Security
In this example of hotelSecurity we will understand the following concepts
- lombok
- DTO
- SecurityFilterChain
- antMatchers
- Role based
- method level security
By using spring initialzr create a project and add the following dependencies and used 2.7.5 version and used maven
We have to changed the version manually in pom.xml
Project structure
And in this example I am using application.yaml
It’s not mandatory to used only the application.yaml , we can used application.properties also
In the model package a new Hotel class is created with id,name,rating,city.
And I have used @Data annotation so, that it will automatically create the setter,getter and constructors also.
package com.hotel.HotelSecurity.model;
import lombok.Data;
import javax.persistence.*;
@Entity
@Data
@Table(name = "hotel")
public class Hotel {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
private Long rating;
private String city;
}
A dto package is created and created a class called as HotelRequest
package com.hotel.HotelSecurity.dto;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data
public class HotelRequest {
private String name;
private Long rating;
private String city;
}
In this class everything is similar like the Hotel class but we can observed that id is missing and associated to the id variable.
This is because the DTO(Data transferred Object) is used to transfer data between different layers,components or modules. They don’t carry any bussiness logic , it’s a light weight object. DTO don’t have any behaviours. It also encapsulate the data and also increased in security as it’s not showing the sensitive data.
Now lets create a config package and a class inside it as HotelSecurityConfig
package com.hotel.HotelSecurity.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
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.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
@Configuration
@EnableWebSecurity
//ensured to access the pre-authorization method level in controller
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class HotelSecurityConfig {
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.authorizeHttpRequests()
// .antMatchers("hotel/create").hasRole("ADMIN") this is for single api
// .antMatchers("hotel/**").hasRole("ADMIN") for all api under hotel/
.anyRequest()
.authenticated()
.and()
// .formLogin(); //to used formLogin type
.httpBasic();
return http.build();
}
@Bean
public UserDetailsService users(){
UserDetails user1 = User.builder()
.username("tony")
.password(passwordEncoder().encode("stark"))
.roles("ADMIN")
.build();
UserDetails user2 = User.builder()
.username("steve")
.password(passwordEncoder().encode("job"))
.roles("NORMAL")
.build();
return new InMemoryUserDetailsManager(user1,user2);
}
@Bean
public PasswordEncoder passwordEncoder(){
return new BCryptPasswordEncoder();
}
}
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
@Configuration indicates that the class has a @Bean definition methods.
@EnableWebSecurity indicates that it is enabling the web security
@EnableGlobalMethodSecurity(prePostEnabled = true) this annotation enable the method level security.
If we are not going to used any method level security then we can skip the
@EnableGlobalMethodSecurity(prePostEnabled = true) annotation
Inside the class level a @Bean container is declared and SecurityFilterChain is declared and passed a HttpSecurity parameter along whith a variable of it. FilterChain used to throw Exceptions so, throw the Exception at the class level. Disable the csrf also so, that it will prevent from frogery , authorized the httpRequests which means that this will authorized the request through the given username and password. Observed the antMatchers
.antMatchers(“hotel/create”).hasRole(“ADMIN”) indicates that only the ADMIN should be able to access the api “hotel/create” and if we wanted to access the remaining api after the hotel then it can be replacred by wildcard like this
.antMatchers(“hotel/**”) .hasRole(“ADMIN”) the ** indicates that it could be anything after the hotel/
ADMIN could be changed to “NORMAL” also according to the requirements of the project.
antMatchers are useful if method level security is not used. If method level security is used then skip the antMatchers
.anyRequests will authorized the url and authenticate to what role will do what activities and if formLogin is used then used formLogin or we can used the httpBasic( ) also and build the http.
Declared the UserDetailsService and create the required users and give the roles to the particular users. Observed that passcordEncoder( ) is also used so, it’s also required to defined.
Observed that InMemoryUserDetailsManager is used and pass user1 and user2 were used and passed as an argument.
The PasswordEncoder should be write after the UserDetailsService
Write the repository interface inside the repository package and extends the jpaRepository
package com.hotel.HotelSecurity.repository;
import com.hotel.HotelSecurity.model.Hotel;
import org.springframework.data.jpa.repository.JpaRepository;
@Repository
public interface HotelRepository extends JpaRepository<Hotel, Long> {
}
Declared the service class in service package and @Autowired the HotelRepository
After this write the controller class in controller package
package com.hotel.HotelSecurity.controller;
import com.hotel.HotelSecurity.dto.HotelRequest;
import com.hotel.HotelSecurity.model.Hotel;
import com.hotel.HotelSecurity.service.HotelService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/hotel")
public class HotelController {
@Autowired
HotelService hotelService;
@PostMapping("/create")
@PreAuthorize("hasRole('ADMIN')") //giving access to a particular role
public void createHotel(@RequestBody HotelRequest hotelRequest)
{
hotelService.createHotel(hotelRequest);
}
@GetMapping("/id/{id}")
@PreAuthorize("hasRole('NORMAL')")
public Hotel getHotelById(@PathVariable Long id)
{
return hotelService.getHotelById(id);
}
@GetMapping("/getAll")
@PreAuthorize("hasRole('ADMIN')")
public List<Hotel> getAllHotels()
{
return hotelService.getAllHotels();
}
@DeleteMapping("/remove/id/{id}")
@PreAuthorize("hasRole('ADMIN')")
public void deleteHotelById(@PathVariable Long id)
{
hotelService.deleteHotelById(id);
}
}
@PreAuthorize is used for method level security and hasRole is assigning the role to ‘ADMIN’ . Role could be other than the ADMIN also it is totally depend to the security config on how the user was assigned to which role.
If we want to give more than one role that we have to used hasAnyRole and we can passed multiple arguments.
If method level security is not used then skip the @PreAuthorize part and also @EnableGlobalMethodSecurity(prePostEnabled = true) from the security config package class. In the control I am giving permission to ADMIN for createHotel,getAllHotels and deleteHotelById.
getHotelById method is accessible by the NORMAL role.
Comeback to service class and implemet the methods of the controller
package com.hotel.HotelSecurity.service;
import com.hotel.HotelSecurity.dto.HotelRequest;
import com.hotel.HotelSecurity.model.Hotel;
import com.hotel.HotelSecurity.repository.HotelRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class HotelService {
@Autowired
private HotelRepository hotelRepository;
public List<Hotel> getAllHotels() {
return hotelRepository.findAll();
}
public Hotel getHotelById(Long id) {
return hotelRepository.findById(id).get();
}
public Hotel createHotel(HotelRequest hotelRequest) {
Hotel hotel = new Hotel();
hotel.setCity(hotelRequest.getCity());
hotel.setName(hotelRequest.getName());
hotel.setRating(hotelRequest.getRating());
return hotelRepository.save(hotel);
}
public void deleteHotelById(Long id) {
hotelRepository.deleteById(id);
}
}
In the createHotel method parameter Hotelrequest is used as an DTO and set the city , name and rating of the hotel from the DTO itself through the getCity,getName,getRating to the hotel and save the hotel to the hotelRepository.
package com.hotel.HotelSecurity;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class HotelSecurityApplication {
public static void main(String[] args) {
SpringApplication.run(HotelSecurityApplication.class, args);
}
}
This is the main class.
Write the application.yaml in resources package
spring:
datasource:
url: jdbc:mysql://localhost:3306/hotel
username: root
password: root
driver-class-name: com.mysql.cj.jdbc.Driver
jpa:
hibernate.ddl-auto: update
server:
port: 8082
change the username password and port number as per your system and create the schema hotel in MySQL.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.hotel</groupId>
<artifactId>HotelSecurity</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>HotelSecurity</name>
<description>Demo project for Spring Security</description>
<url/>
<licenses>
<license/>
</licenses>
<developers>
<developer/>
</developers>
<scm>
<connection/>
<developerConnection/>
<tag/>
<url/>
</scm>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.mysql</groupId>
<artifactId>mysql-connector-j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
Lets start the application
Lets try with the postman
Lets give authorization through authentication.
Goto Authorization and select the Basic Auth and fill the Username and password correctly.
In this project the username was “tony ”and password was “stark ”for “ADMIN ”role and only ADMIN is allowed to create new hotel. Lets check
Username and password for ADMIN in inserted. Lets send it.
It’s success. Lets see in the database
Lets try to create an another hotel by using the username “steve ”and password “job ”which was assigned as NORMAL role
status code is 403 and forbidden which means it’s not authorized to access the api for “/hotel/create”
Likewise getHotelById , getAllHotels , deleteHotelById can be performed by following the same postman steps. Be sure to used the ADMIN and NORMAL accordingly.