Spring Boot Method Level Security (2024)
Spring Boot Method Security with PreAuthorize
In this tutorial, we'll look at how to add method-level security to a spring boot application. To
manage method-level security, we will use the @PreAuthorize
annotation.
What is Spring Boot Method-Level Security?
The @PreAuthorize
annotation is used on controller methods to implement method-level
security. This annotation comprises a snippet
of Spring Expression Language (SpEL) that is evaluated to determine whether the request should be
authenticated.
Spring Boot Security Tutorial :
- Basic Authentication
- Digest Authentication
- Configuring Authentication Credentials in database
- Spring Boot Method Security with PreAuthorize
- Enable https (http+ssl)
- JWT Introduction
- JWT Token Authentication Example
- JWT Angular Example
- JWT +MYSQL Example
- OAuth2.0 Tutorial
- Advantage of JWT as OAuth Access Token Vs OAuth Default Token
- OAuth2 with JWT Access Token
- Spring Security Interview Questions
The AbstractSecurityInterceptor
in Spring Security handles the initial authorization of
an incoming request.
There are two concrete implementations of the AbstractSecurityInterceptor:
FilterSecurityInterceptor
The Spring Security filter chain's default filter. All authenticated user requests will be authorised by theFilterSecurityInterceptor
.MethodSecurityInterceptor
This is required for method level security to be implemented. It enables us to apply security to our programme at the method level.
Now, let's create Spring Boot application from Spring Initializr.
Project Structure:
Maven Dependency:
Add spring security starter, jpa, lombok and h2 database dependencies in the pom.xml.<?xml version="1.0" encoding="UTF-8"?>
<project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://maven.apache.org/POM/4.0.0"
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.4.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.techgeeknext</groupId>
<artifactId>spring-boot-method-level-security</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>spring-boot-method-level-security</name>
<description>Spring Boot Method Level Security Example</description>
<properties>
<java.version>1.8</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.h2database</groupId>
<artifactId>h2</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>
Application properties
Add h2 database connection details in properties file.
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.url=jdbc:h2:mem:securitydb
spring.datasource.driverClassName=org.h2.Driver
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.properties.hibernate.show_sql=true
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
Enable Spring Boot Method Level Security
The security config class is crucial because it allows control over the authentication and authorization processes.
- To provide implementation to the
AuthenticationManagerBuilder
class, we will extend theWebSecurityConfigurerAdapter
class and override theconfigure()
method. - In this tutorial, we will build the security mechanism for our application and define the
secured and non-secured endpoints
of the application using the
UserDetailsService
and override other form ofconfigure()
function. - To enable method-level security, annotate the class with the
@EnableGlobalMethodSecurity
annotation. - As spring security needs an encoder to be present, a password encoder is used for encoding
reasons. If we don't want this, simply remove this method and replace the password with the
{noop}
argument. The{noop}
parameter prevents an error caused by a missing PasswordEncode.
package com.techgeeknext.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
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.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
@Component
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
private static final String[] WHITELIST_PATTERNS = {"/anonymous", "/h2-console/**"};
@Qualifier("customUserDetailsService")
@Autowired
UserDetailsService detailsService;
@Override
protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(detailsService);
}
@Override
protected void configure(final HttpSecurity http) throws Exception {
http.httpBasic()
.and().authorizeRequests().antMatchers(WHITELIST_PATTERNS).permitAll()
.and().authorizeRequests().anyRequest().authenticated()
.and().csrf().disable().headers().frameOptions().sameOrigin();
//here we will not create a session
http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
}
@Bean
public BCryptPasswordEncoder bCryptPasswordEncoder() {
return new BCryptPasswordEncoder();
}
}
User Details Service
To provide an implementation to the loadUserByUsername
method, add the following code
to the custom
user details service class that implements the UserDetailsService
interface. To obtain
the user, the
overridden method will communicate with the DAO layer method.
package com.techgeeknext.service;
import com.techgeeknext.entity.CustomUserDetails;
import com.techgeeknext.entity.User;
import com.techgeeknext.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
import java.util.Optional;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired
UserRepository userRepository;
/**
* Find User by userName
* @param userName
* @return
* @throws UsernameNotFoundException
*/
@Override
public UserDetails loadUserByUsername(final String userName) throws UsernameNotFoundException {
final Optional<User> optionalUser = userRepository.findByUserName(userName);
return optionalUser.map(CustomUserDetails::new).orElseThrow(
() -> new UsernameNotFoundException(String.format("User = %s does not exists", userName)));
}
}
Map the User Object
To map the User object during the loadUserByUsername
call, add the following code to
the
model class. The UserDetails interface
given by spring security will be implemented by
the class.
This class's implementation can be found in the CustomUserDetailsService.java
class.
package com.techgeeknext.entity;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.util.CollectionUtils;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;
public class CustomUserDetails implements UserDetails {
private static final long serialVersionUID = 1L;
private final String username;
private final String password;
private final boolean isActive;
private final List<GrantedAuthority> authorities;
public CustomUserDetails(final User user) {
this.username = user.getUserName();
this.password = user.getPassword();
this.isActive = user.isActive();
this.authorities = getAuthorities(user.getRoles());
}
private List<GrantedAuthority> getAuthorities(final List<Role> roles) {
if (CollectionUtils.isEmpty(roles)) {
return Collections.emptyList();
}
return roles.stream().map(role ->
new SimpleGrantedAuthority(role.toString())).collect(Collectors.toList());
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return authorities;
}
@Override
public String getPassword() {
return password;
}
@Override
public String getUsername() {
return username;
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return isActive;
}
}
Entity
Create User entity class and Role as an enum class mapped under User entity.
package com.techgeeknext.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.stereotype.Component;
import javax.persistence.*;
import java.util.List;
@Entity
@Table(name = "users_auth_details")
@Data
@NoArgsConstructor
@AllArgsConstructor
@Builder
@Component
public class User {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
int id;
@Column(name = "user_name", nullable = false)
String userName;
String password;
boolean active;
@Enumerated(EnumType.STRING)
@ElementCollection(fetch = FetchType.EAGER)
List<Role> roles;
}
Repository
Create User Repository class using JpaRepository
.
package com.techgeeknext.repository;
import com.techgeeknext.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.Optional;
@Repository
public interface UserRepository extends JpaRepository<User, Integer> {
Optional<User> findByUserName(String userName);
}
Controller class
To the controller class, add the following code. The controller class contains methods marked with
the @PreAuthorize
annotation, which checks for authorization before executing the
method.
We could alternatively use the @Secured
annotation in spring to handle method-level
security, however it has several limitations, such as
- We cannot have several conditions with the
@Secured
annotation, i.e. the roles cannot be coupled with an AND/OR condition. - Spring expression language is not supported by the
@Secured
annotation.
package com.techgeeknext.controller;
import com.techgeeknext.dto.UserDto;
import com.techgeeknext.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@Slf4j
@RestController
public class SecurityAuthorizeController {
@Autowired
UserService userService;
/**
* Method to test for User having no roles i.e accessible to all
* @return
*/
@GetMapping("/anonymous")
@ResponseStatus(HttpStatus.OK)
public String accessToAllUsers() {
log.info("Accessible to all users.");
return "Welcome anonymous User";
}
/**
* Method to test for CLIENT ROLE.
* @return
*/
@GetMapping("/client")
@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasRole('CLIENT')")
public String testUserRoleAccess() {
log.info("Accessible to CLIENT Role.");
return "Welcome User (CLIENT)!";
}
/**
* Method to test for Users who have ADMIN Role only.
* @return
*/
@GetMapping("/admin")
@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasRole('ADMIN')")
public String testAdminRoleAccess() {
log.info("Accessible to ADMIN Role.");
return "Welcome ADMIN User!";
}
/**
* Method to test for Users who have SUPERVISOR Role only.
* @return
*/
@GetMapping("/supervisor")
@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasRole('SUPERVISOR')")
public String testSupervisorsRoleAccess() {
log.info("Accessible to SUPERVISOR Role.");
return "Welcome SUPERVISOR User!";
}
/**
* Method to test for Users who have both ADMIN AND SUPERVISOR Role.
* @return
*/
@GetMapping("/admin-supervisor")
@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasRole('SUPERVISOR') AND hasRole('ADMIN')")
public String testSupervisorAndAdminRoleAccess() {
log.info("Accessible to ADMIN OR SUPERVISOR Role.");
return "Welcome User (ADMIN OR SUPERVISOR)!";
}
/**
* ADMIN Role can get all users.
* @return
*/
@GetMapping("/users")
@ResponseStatus(HttpStatus.OK)
@PreAuthorize("hasRole('ADMIN')")
public List<UserDto> getAllUser() {
log.info("fetching users");
return userService.getUsers();
}
}
Take a look at our suggested posts:
Create User Data to test the application
Create the class to insert users, roles and password into table.
package com.techgeeknext;
import com.techgeeknext.entity.User;
import com.techgeeknext.entity.Role;
import com.techgeeknext.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
@Slf4j
@Component
public class CreateUserData implements CommandLineRunner {
@Autowired
UserService userService;
@Autowired
BCryptPasswordEncoder passwordEncoder;
@Override
public void run(String... args) {
addUsers();
}
/**
* Create users if not exist in DB
*/
private void addUsers() {
if (!userService.checkUsersDataExistInDB()) {
log.info("Save Users");
userService.saveAll(createUsers());
} else {
log.info("Users are available in DB!");
}
}
/**
* Create User with Roles for testing purpose
* @return List
*/
private List<User> createUsers() {
final List<User> users = new ArrayList<>();
users.add(create("John",
Collections.singletonList(Role.ROLE_CLIENT),"techgeeknextPassword01"));
users.add(create("Simon",
Collections.singletonList(Role.ROLE_ADMIN),"techgeeknextPassword02"));
users.add(create("Joe",
Collections.singletonList(Role.ROLE_SUPERVISOR),"techgeeknextPassword03"));
users.add(create("Brinda",
Arrays.asList(Role.ROLE_ADMIN, Role.ROLE_SUPERVISOR),"techgeeknextPassword04"));
return users;
}
private User create(final String userName, final List<Role> roles, final String password) {
return User.builder()
.userName(userName)
.password(passwordEncoder.encode(password))
.active(true)
.roles(roles)
.build();
}
}
Test the Method Level Security
Follow the below steps to test the Method Level Security applied according to the roles.
- Start the application by using command mvn
spring-boot:run
or start the application by running main() method from the main class(SpringBootMethodLevelSecurityApplication.java)
. It will start the embedded tomcat server on port 8080. - Open Postman tool, and provide Authorization details and access REST Endpoint http://localhost:8080/client for user having CLIENT ROLE as below:
- Access REST Endpoint http://localhost:8080/admin for user having ADMIN ROLE with Authorization details as below:
- Access REST Endpoint http://localhost:8080/supervisor for user having SUPERVISOR ROLE with Authorization details as below:
- Access REST Endpoint http://localhost:8080/admin-supervisor for user having both ADMIN AND SUPERVISOR ROLE with Authorization details as below:
- Get All User details : Access REST Endpoint http://localhost:8080/users for user having ADMIN ROLE with Authorization details as below:
Download Source Code
The full source code for this article can be found on below.Download it here - Spring Boot Method Security with PreAuthorize