PreAuthorize HasPermission Security Using Spring Boot Data JPA

Introduction

The ability of hasPermission() method in Spring Boot Security applications is to determine the accessibility of a particular user for the application. The permission of an access can be read, write, etc. So, a particular user has defined set of access permission by which a user can perform certain activities in the application.

The most useful annotation @PreAuthorize, which decides whether a method can actually be invoked or not based on user’s role and permission.

Related Post:

Where is @PreAuthorize applicable?

This @PreAuthorize annotation is applicable on the method as a Method Security Expression. For example,

@PreAuthorize("hasRole('ADMIN') and hasPermission('hasAccess','WRITE')")
public void create(Contact contact);

Which means that access will only be allowed for users with the role ROLE_ADMIN and has WRITE permission. Obviously the same thing could easily be achieved using a traditional configuration and a simple configuration attribute for the required role.

Prerequisites

Java 1.8+ (tested in 11 – 12), Maven 3.8.5, Spring Boot 2.6.7, MySQL 8.0.26

Project Setup

The following pom.xml file can be used for building the maven based project:

<?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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>

	<groupId>com.roytuts</groupId>
	<artifactId>spring-preauthorize-has-permission-data-jpa</artifactId>
	<version>0.0.1-SNAPSHOT</version>

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
		<maven.compiler.source>11</maven.compiler.source>
		<maven.compiler.target>11</maven.compiler.target>
	</properties>

	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.6.7</version>
	</parent>

	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</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-data-jpa</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
		</dependency>

		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<scope>runtime</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>
</project>

MySQL Table Data

Here are sample tables with some sample data for testing the Spring Boot Security application with hasRole example. The credentials in plain text format are admin/admin and user/user with roles defined.

CREATE TABLE IF NOT EXISTS `user` (
  `user_id` int unsigned COLLATE utf8mb4_unicode_ci NOT NULL AUTO_INCREMENT,
  `user_name` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL,
  `user_pass` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
  `enable` tinyint COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT '1',
  PRIMARY KEY (`user_id`),
  UNIQUE KEY `user_unique_key` (`user_name`),
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

CREATE TABLE IF NOT EXISTS `user_role` (
  `role_id` int unsigned COLLATE utf8mb4_unicode_ci NOT NULL AUTO_INCREMENT,
  `user_id` int unsigned COLLATE utf8mb4_unicode_ci NOT NULL,
  `user_role` varchar(15) COLLATE utf8mb4_unicode_ci NOT NULL,
  PRIMARY KEY (`role_id`),
  UNIQUE KEY `user_unique_key` (`user_id`, `user_role`),
  CONSTRAINT `user_role_fk` FOREIGN KEY (`user_id`) REFERENCES `user` (`user_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;

INSERT INTO `user` (`user_id`, `user_name`, `user_pass`, `enable`) VALUES
	(1, 'admin', '$2a$10$dl8TemMlPH7Z/mpBurCX8O4lu0FoWbXnhsHTYXVsmgXyzagn..8rK', 1),
	(2, 'user', '$2a$10$9Xn39aPf4LhDpRGNWvDFqu.T5ZPHbyh8iNQDSb4aNSnLqE2u2efIu', 1);


INSERT INTO `user_role` (`role_id`, `user_id`, `user_role`) VALUES
	(1, 2, 'ROLE_USER'),
	(2, 1, 'ROLE_USER'),
	(3, 1, 'ROLE_ADMIN');

Application Config

The following application.properties file is kept under src/main/resources folder with the datasource configuration.

#Spring Datasource
spring.datasource.driverClassName=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/roytuts
spring.datasource.username=root
spring.datasource.password=root
 
#SQL related
spring.jpa.show-sql = true
spring.jpa.properties.hibernate.format_sql=true

spring.jpa.hibernate.ddl-auto = none

I am logging SQL statement in the console or it could be also logged into the log file. I am also formatting the SQL statements in the logs. I do not want to create any table from the entity class, so I am using the following directive:

spring.jpa.hibernate.ddl-auto = none

As the standard naming conventions for datasource configuration have been used in the above application.properties file, so I don’t need to create any datasource bean in the Java file.

Entity Classes

The following entity classes maps the users with their corresponding roles. The classes have one-to-many/many-to-one bidirectional mapping.

The user entity class is as follows:

package com.roytuts.spring.preauthorize.haspermission.data.jpa.entity;

import java.util.List;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name = "user")
public class User {

	@Id
	@Column(name = "user_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer userId;

	@Column(name = "user_name")
	private String userName;

	@Column(name = "user_pass")
	private String userPass;

	private boolean enable;

	@OneToMany(mappedBy = "user")
	private List<UserRole> userRoles;

	// getters and setters

}

The user role class is defined as follows:

package com.roytuts.spring.preauthorize.haspermission.data.jpa.entity;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name = "user_role")
public class UserRole {

	@Id
	@Column(name = "role_id")
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer roleId;

	@Column(name = "user_role")
	private String userRole;

	@ManyToOne
	@JoinColumn(name = "user_id")
	private User user;

        //getters and setters
}

Repository Interfaces

One of the most important advantages of Spring Data JPA API is you can use method to query your database and for that Spring data JPA already provides some methods for basic needs.

package com.roytuts.spring.preauthorize.haspermission.data.jpa.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.roytuts.spring.preauthorize.haspermission.data.jpa.entity.User;

public interface UserRepository extends JpaRepository<User, Integer> {

	User findByUserName(final String userName);

}

And

package com.roytuts.spring.preauthorize.haspermission.data.jpa.repository;

import org.springframework.data.jpa.repository.JpaRepository;

import com.roytuts.spring.preauthorize.haspermission.data.jpa.entity.UserRole;

public interface UserRoleRepository extends JpaRepository<UserRole, Integer> {

}

Service Class

The service class implements UserDetailsService from Spring Security framework to authenticate and load the user details with roles from the database.

package com.roytuts.spring.preauthorize.haspermission.data.jpa.service;

import java.util.List;
import java.util.stream.Collectors;

import javax.transaction.Transactional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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 com.roytuts.spring.preauthorize.haspermission.data.jpa.entity.User;
import com.roytuts.spring.preauthorize.haspermission.data.jpa.entity.UserRole;
import com.roytuts.spring.preauthorize.haspermission.data.jpa.repository.UserRepository;

@Service
@Transactional
public class UserAuthService implements UserDetailsService {

	@Autowired
	private UserRepository userRepository;

	@Override
	public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
		User user = userRepository.findByUserName(username);

		if (user == null) {
			throw new UsernameNotFoundException("User with '" + username + "' not found.");
		}

		List<UserRole> roles = user.getUserRoles();

		List<GrantedAuthority> grantedAuthorities = roles.stream().map(r -> {
			return new SimpleGrantedAuthority(r.getUserRole());
		}).collect(Collectors.toList());

		org.springframework.security.core.userdetails.User usr = new org.springframework.security.core.userdetails.User(
				user.getUserName(), user.getUserPass(), grantedAuthorities);

		return usr;
	}

}

Permission Evaluator

The method hasPermission() is used in @PreAuthorize annotation in order to evaluate permission of a user. The use of the hasPermission() expression has different look.

hasPermission() expressions are delegated to an instance of PermissionEvaluator. It is intended to bridge between the expression system and Spring Security’s ACL system, allowing you to specify authorization constraints on domain objects, based on abstract permissions. It has no explicit dependencies on the ACL module, so you could swap that out for an alternative implementation if required. The interface has two methods:

boolean hasPermission(Authentication authentication, Object targetDomainObject, Object permission);
boolean hasPermission(Authentication authentication, Serializable targetId, String targetType, Object permission);

which map directly to the available versions of the expression, with the exception that the first argument (the Authentication object) is not supplied. The first is used in situations where the domain object, to which access is being controlled, is already loaded. Then expression will return true if the current user has the given permission for that object. The second version is used in cases where the object is not loaded, but its identifier is known. An abstract “type” specifier for the domain object is also required, allowing the correct ACL permissions to be loaded.

To use hasPermission() expressions, you have to explicitly configure a PermissionEvaluator in your application context.

The custom permission evaluator is given below:

package com.roytuts.spring.preauthorize.haspermission.data.jpa.config;

import java.io.Serializable;

import org.springframework.security.access.PermissionEvaluator;
import org.springframework.security.core.Authentication;
import org.springframework.stereotype.Component;

@Component
public class CustomPermissionEvaluator implements PermissionEvaluator {

	@Override
	public boolean hasPermission(Authentication authentication, Object accessType, Object permission) {
		if (authentication != null && accessType instanceof String) {
			if ("hasAccess".equalsIgnoreCase(String.valueOf(accessType))) {
				boolean hasAccess = validateAccess(String.valueOf(permission));
				return hasAccess;
			}
			return false;
		}
		return false;
	}

	private boolean validateAccess(String permission) {
		// ideally should be checked with user role, permission in database
		if ("READ".equalsIgnoreCase(permission)) {
			return true;
		}
		return false;
	}

	@Override
	public boolean hasPermission(Authentication authentication, Serializable serializable, String targetType,
			Object permission) {
		return false;
	}

}

The following configuration is required to use the above permission evaluator:

package com.roytuts.spring.preauthorize.haspermission.data.jpa.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.access.expression.method.DefaultMethodSecurityExpressionHandler;
import org.springframework.security.access.expression.method.MethodSecurityExpressionHandler;

@Configuration
public class PermissionConfig {

	@Autowired
	private CustomPermissionEvaluator permissionEvaluator;

	@Bean
	public MethodSecurityExpressionHandler methodSecurityExpressionHandler() {
		DefaultMethodSecurityExpressionHandler handler = new DefaultMethodSecurityExpressionHandler();
		handler.setPermissionEvaluator(permissionEvaluator);
		return handler;
	}

}

Security Config

The following security config class enables security for pre and post. prePostEnabled = true – enables processing of @PreAuthorize/@PreFilter and @PostAuthorize/@PostFilter. Without enabling it the annotations mean nothing.

package com.roytuts.spring.preauthorize.haspermission.data.jpa.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
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.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.password.PasswordEncoder;

import com.roytuts.spring.preauthorize.haspermission.data.jpa.service.UserAuthService;

@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {

	@Autowired
	private PasswordEncoder passwordEncoder;

	@Autowired
	private UserAuthService userAuthService;

	@Autowired
	public void registerGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userAuthService).passwordEncoder(passwordEncoder);
	}

}

The password encoder configuration is given below:

package com.roytuts.spring.preauthorize.haspermission.data.jpa.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
public class EncoderConfig {

	@Bean
	PasswordEncoder passwordEncoder() {
		return new BCryptPasswordEncoder();
	}

}

Rest Controller Class

REST controller class that exposes some REST API methods for various purpose in software programming. Here I am going to publish few methods to show the example working based on users’ roles and permission.

package com.roytuts.spring.preauthorize.haspermission.data.jpa.rest.controller;

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class AppRestController {

	@GetMapping("/user")
	@PreAuthorize("hasRole('USER')")
	public ResponseEntity<String> user() {
		return new ResponseEntity<String>("You have USER role.", HttpStatus.OK);
	}

	@GetMapping("/admin")
	@PreAuthorize("hasRole('ADMIN') and hasPermission('hasAccess','READ')")
	public ResponseEntity<String> admin() {
		return new ResponseEntity<String>("You have ADMIN role with READ access.", HttpStatus.OK);
	}

}

Spring Boot Main Class

A class with main method and @SpringBootApplication annotation will be enough to deploy the app in embedded Tomcat server.

package com.roytuts.spring.preauthorize.haspermission.data.jpa;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.domain.EntityScan;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;

@SpringBootApplication
@EntityScan(basePackages = "com.roytuts.spring.preauthorize.haspermission.data.jpa.entity")
@EnableJpaRepositories(basePackages = "com.roytuts.spring.preauthorize.haspermission.data.jpa.repository")
public class App {

	public static void main(String[] args) {
		SpringApplication.run(App.class, args);
	}

}

Testing Spring Security – PreAuthorize hasPermission

Now it’s time to test the application. Hope your MySQL server is running and you have deployed the app by executing the main class.

If you try to access the URL http://localhost:8080/admin using credentials admin/admin, then you will see the output You have ADMIN role with READ access.

spring security haspermission

Source Code

Download

Leave a Reply

Your email address will not be published. Required fields are marked *