Spring Boot Method-Level Security

Welcome, in this tutorial, we will see how to implement method-level security in a spring boot application. We will use the @PreAuthorize annotation to handle the method-level security and will also understand the difference between @Secured and @PreAuthorize annotations.

1. Introduction

Before going further in this tutorial, we will look at the common terminology such as introduction to Spring Boot and Lombok.

1.1 Spring Boot

1.2 Lombok

1.2.1 Lombok features

Feature Details
val Local variables are declared as final
var Mutable local variables
@Slf4J Creates an SLF4J logger
@Cleanup Will call close() on the resource in the finally block
@Getter Creates getter methods for all properties
@Setter Creates setter for all non-final properties
@EqualsAndHashCode
  • Generates implementations of equals(Object other) and hashCode()
  • By default will use all non-static, non-transient properties
  • Can optionally exclude specific properties
@ToString
  • Generates String of class name, and each field separated by commas
  • Optional parameter to include field names
  • Optional parameter to include a call to the super toString method
@NoArgsConstructor
  • Generates no-args constructor
  • Will cause compiler error if there are final fields
  • Can optionally force, which will initialize final fields with 0/false/null var – mutable local variables
@RequiredArgsContructor
  • Generates a constructor for all fields that are final or marked @NonNull
  • The constructor will throw a NullPointerException if any @NonNull fields are null val – local variables are declared final
@AllArgsConstructor
  • Generates a constructor for all properties of the class
  • Any @NotNull properties will have null checks
@Data
  • Generates typical boilerplate code for POJOs
  • Combines – @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequiredArgsConstructor
  • No constructor is generated if constructors have been explicitly declared
@Builder
  • Implements the Builder pattern for object creation
@Value
  • The immutable variant of @Data
  • All fields are made private and final by default

Let us go ahead with the tutorial implementation but before going any further I’m assuming that you’re aware of the Spring boot basics.

2. Spring Boot Method-Level Security

2.1 Tools Used for Spring boot application and Project Structure

We are using Eclipse Kepler SR2, JDK 8, and Maven. In case you’re confused about where you should create the corresponding files or folder, let us review the project structure of the spring boot application.

Fig. 1: Project structure

Let us start building the application!

3. Creating a Spring Boot application

Below are the steps involved in developing the application.

3.1 Maven Dependency

Here, we specify the dependency for the Spring boot (Web, JPA, and Security), H2 database, and Lombok. The updated file will have the following code.

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.springboot.methodlevel.security</groupId>
    <artifactId>SpringbootMethodlevelsecurity</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>SpringbootMethodlevelsecurity</name>
    <description>Method level security in springboot application</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>

3.2 Application properties file

Create a new properties file at the location: SpringbootMethodlevelsecurity/src/main/resources/ and add the following code to it. Here we will define the H2 database connection, database creation, and h2 console details. You’re free to change the application or the database details as per your wish.

application.properties

server.port=9800
spring.application.name=springboot-methodlevel-security
# h2 database settings
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.url=jdbc:h2:mem:testdb
spring.datasource.driverClassName=org.h2.Driver
# db-creation settings
spring.jpa.database-platform=org.hibernate.dialect.H2Dialect
spring.jpa.hibernate.ddl-auto=create-drop
spring.jpa.properties.hibernate.show_sql=true
## browser url for h2 console - http://localhost:9800/h2-console
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

3.3 Java Classes

Let us write the important java class(es) involved in this application. For brevity, we will skip the following classes –

3.3.1 Implementation/Main class

Add the following code to the main class to bootstrap the application from the main method. Always remember, the entry point of the spring boot application is the class containing @SpringBootApplication annotation and the static main method.

SpringbootMethodlevelsecurityApplication.java

package com.springboot.methodlevel.security;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//lombok annotation
@Slf4j
//spring annotation
@SpringBootApplication
public class SpringbootMethodlevelsecurityApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringbootMethodlevelsecurityApplication.class, args);
        log.info("Spring boot and method-level security application started successfully");
    }
}

3.3.2 Model class

Add the following code to the model class that will be used to map the User object during the find user by username operation. The class will implement the UserDetails interface provided by the spring security. The implementation of this class can be seen in the CustomUserDetailsService.java class.

CustomUserDetails.java

package com.springboot.methodlevel.security.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) {
        //checking the null and empty check
        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;
    }
}

3.3.3 User details service class

Add the following code to the custom user details service class that implements the UserDetailsService interface to provide an implementation to the loadUserByUsername method. The overridden method will interact with the DAO layer method to get the user.

CustomUserDetailsService.java

package com.springboot.methodlevel.security.service;
import com.springboot.methodlevel.security.entity.CustomUserDetails;
import com.springboot.methodlevel.security.entity.User;
import com.springboot.methodlevel.security.repository.UserRepository;
import lombok.extern.slf4j.Slf4j;
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;
//lombok annotation
@Slf4j
//spring annotation
@Service
public class CustomUserDetailsService implements UserDetailsService {
    @Autowired
    UserRepository repository;
    //find user by username from the db
    @Override
    public UserDetails loadUserByUsername(final String username) throws UsernameNotFoundException {
        log.info("Fetching user = {}", username);
        final Optional<User> optionalUser = repository.findByUsername(username);
        return optionalUser.map(CustomUserDetails::new).orElseThrow(
                () -> new UsernameNotFoundException(String.format("User = %s does not exists", username)));
    }
}

3.3.4 Security config class

The security config is an important class that helps to enable fine-grained control over the authentication and authorization process. In this –

SecurityConfig.java

package com.springboot.methodlevel.security.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;
//spring annotation
@Component
//spring security annotations
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
    private static final String[] WHITELIST_PATTERNS = {"/api/anonymous", "/h2-console/**"};
    @Qualifier("customUserDetailsService")
    @Autowired
    UserDetailsService detailsService;
    @Override
    protected void configure(final AuthenticationManagerBuilder auth) throws Exception {
        //using the user details service to authenticate the user from the db
        auth.userDetailsService(detailsService);
    }
    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.httpBasic()    // using the basic authentication
                .and().authorizeRequests().antMatchers(WHITELIST_PATTERNS).permitAll()      //public endpoints
                .and().authorizeRequests().anyRequest().authenticated()     // all other application endpoints are protected
                .and().csrf().disable().headers().frameOptions().sameOrigin();
        //do not create a session
        //effective for rest api's
        http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
    }
    @Bean
    public BCryptPasswordEncoder bCryptPasswordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

3.3.5 Controller class

Add the following code to the controller class. The controller class contains methods that are annotated with the @PreAuthorize annotation that will check for authorization before the method execution. We could also use the @Secured annotation to handle the method-level security in spring but it has certain drawbacks i.e.

SecurityController.java

package com.springboot.methodlevel.security.controller;
import com.springboot.methodlevel.security.dto.UserDto;
import com.springboot.methodlevel.security.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.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
//lombok annotation
@Slf4j
//spring annotations
@RestController
@RequestMapping("/api")
public class SecurityController {
    @Autowired
    UserService service;
    //note - @PreAuthorize checks for authorization before method execution
    //will be publicly accessible
    //URL - http://localhost:9800/api/anonymous
    @GetMapping("/anonymous")
    @ResponseStatus(HttpStatus.OK)
    public String getAnonymousResponse() {
        log.info("Returning anonymous response");
        return "Hello anonymous";
    }
    //will only be accessible by the user who has ROLE_USER assigned
    //URL - http://localhost:9800/api/protected/user
    @GetMapping("/protected/user")
    @ResponseStatus(HttpStatus.OK)
    @PreAuthorize("hasRole('USER')")
    public String getUserResponse() {
        log.info("Returning user response");
        return "Hello user";
    }
    //will be accessible by the users who has ROLE_MODERATOR assigned
    //URL - http://localhost:9800/api/protected/moderator
    @GetMapping("/protected/moderator")
    @ResponseStatus(HttpStatus.OK)
    @PreAuthorize("hasRole('MODERATOR')")
    public String getModeratorResponse() {
        log.info("Returning moderator response");
        return "Hello moderator";
    }
    //will be accessible by the users who has ROLE_ADMIN assigned
    //URL - http://localhost:9800/api/protected/admin
    @GetMapping("/protected/admin")
    @ResponseStatus(HttpStatus.OK)
    @PreAuthorize("hasRole('ADMIN')")
    public String getAdminResponse() {
        log.info("Returning administrator response");
        return "Hello administrator";
    }
    //will only be accessible by the user who has both ROLE_MODERATOR and ROLE_ADMIN assigned
    //URL - http://localhost:9800/api/protected/owner
    @GetMapping("/protected/owner")
    @ResponseStatus(HttpStatus.OK)
    @PreAuthorize("hasRole('MODERATOR') AND hasRole('ADMIN')")
    public String getAppOwnerResponse() {
        log.info("Returning application owner response response");
        return "Hello application owner";
    }
    //will only be accessible by the user who has both ROLE_MODERATOR and ROLE_ADMIN assigned
    //URL - http://localhost:9800/api/protected/get-all-users
    @GetMapping("/protected/get-all-users")
    @ResponseStatus(HttpStatus.OK)
    @PreAuthorize("hasRole('MODERATOR') AND hasRole('ADMIN')")
    public List<UserDto> getUsers() {
        log.info("Returning all users");
        return service.getUsers();
    }
}

4. Run the Application

To execute the application, right-click on the SpringbootMethodlevelsecurityApplication.java class, Run As -> Java Application.

Fig. 2: Run the Application

5. Project Demo

When the application is started, open the Postman tool to hit the application endpoints. Remember to specify the authorization details in each request. You can do so through the Authorization tab dropdown to select an auth type for every request. For this tutorial, we will select the auth type as Basic Auth where you will specify the username and password (refer to DefaultUsersLoader.java class to get the users and their associated roles information).

Application endpoints

-- HTTP GET endpoints –
-- Remember to include the authorization header containing the valid basic auth in each request –
//will be publicly accessible
http://localhost:9800/api/anonymous
//will only be accessible by the user who has ROLE_USER assigned
http://localhost:9800/api/protected/user
//will be accessible by the users who have ROLE_MODERATOR assigned
http://localhost:9800/api/protected/moderator
//will be accessible by the users who have ROLE_ADMIN assigned
http://localhost:9800/api/protected/admin
//will only be accessible by the user who has both ROLE_MODERATOR and ROLE_ADMIN assigned
http://localhost:9800/api/protected/owner
//will only be accessible by the user who has both ROLE_MODERATOR and ROLE_ADMIN assigned
http://localhost:9800/api/protected/get-all-users

That is all for this tutorial and I hope the article served you whatever you were looking for. Happy Learning and do not forget to share!

6. Summary

In this section, you learned:

You can download the sample application as an Eclipse project in the Downloads section.

7. Download the Project

This was an example of implementing method-level security in a spring boot application.

Download
You can download the full source code of this example here: Spring Boot Method-Level Security
Exit mobile version