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
- Spring boot is a module that provides rapid application development feature to the spring framework including auto-configuration, standalone-code, and production-ready code
- It creates applications that are packaged as jar and are directly started using embedded servlet container (such as Tomcat, Jetty or, Undertow). Thus, no need to deploy the war files
- It simplifies the maven configuration by providing the starter template and helps to resolve the dependency conflicts. It automatically identifies the required dependencies and imports them into the application
- It helps in removing the boilerplate code, extra annotations, and XML configurations
- It provides powerful batch processing and manages the rest endpoints
- It provides an efficient JPA-starter library to effectively connect the application with the relational databases
- It offers a Microservice architecture and cloud configuration that manages all the application related configuration properties in a centralized manner
1.2 Lombok
- Lombok is nothing but a small library that reduces the amount of boilerplate Java code from the project
- Automatically generates the getters and setters for the object by using the Lombok annotations
- Hooks in via the Annotation processor API
- Raw source code is passed to Lombok for code generation before the Java Compiler continues. Thus, produces properly compiled Java code in conjunction with the Java Compiler
- Under the
target/classes
folder you can view the compiled class files - Can be used with Maven, Gradle IDE, etc.
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 |
|
@ToString |
|
@NoArgsConstructor |
|
@RequiredArgsContructor |
|
@AllArgsConstructor |
|
@Data |
|
@Builder |
|
@Value |
|
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.
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 –
User.java
– Entity class to persist the data in the databaseRole.java
– Enum class that contains the role constants for the usersUserRepository.java
– Repository interface that extends theJpaRepository
interface to perform the SQL operations. The interface provides an explicit implementation to thefindByUsername
method and returns an optionalUserService.java
– Service class that interact with the DAO layer methodsDefaultUsersLoader.java
– Bootstrap class to populate dummy data to the h2 database once the application is started successfullyUserDto.java
– Response DTO to be used by the service layer method for sending out the get all users response. It is basically acting as a mapper to theUser.java
class
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 –
- We will extend the
WebSecurityConfigurerAdapter
class - Override the
configure(..)
method to provide implementation to theAuthenticationManagerBuilder
class. In this tutorial, we will use theUserDetailsService
- Override another variation of
configure(..)
method to define the security mechanism for our application and define the protected and non-protected endpoints of the application - Annotate the class with the
@EnableGlobalMethodSecurity
annotation to enable the method-level security - A password encoder for encoding purposes as spring security expects an encoder to the present. If you don’t want this simply remove this method and add the
{noop}
parameter before the password. The{noop}
parameter prevent an error related toPasswordEncode
not present
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.
- With
@Secured
annotation we cannot have multiple conditions i.e. the roles cannot be combined with an AND/OR condition @Secured
annotation does not support spring expression language
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
.
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:
- Spring boot and Lombok introduction
- Steps to implement method-level security in a spring boot application
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.
You can download the full source code of this example here: Spring Boot Method-Level Security