Introduction

In this tutorial I will show you an example on @PreAuthorize annotation – hasPermission() example in Spring Security. The most useful annotation @PreAuthorize, which decides whether a method can actually be invoked or not based on user’s role and permission. hasRole() method returns true if the current principal has the specified role and hasPermission() method returns true if the current user’s role has the specific permission such as READ, WRITE, UPDATE or DELETE. By default if the supplied role does not start with ROLE_, then it will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler.

We will authenticate user using in-memory credentials as well as database credentials. We will use here MySQL database to authenticate role based authentication by implementing Spring’s built-in service UserDetailsService. We will build the project both using maven and gradle build tools.

You can check my previous tutorial on hasRole @PreAuthorize annotation – hasRole example in Spring Security

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

Eclipse 4.12, At least Java 1.8, Gradle 4.10.2/5.6, Maven 3.6.1, Spring Boot 2.1.5/2.2.4, MySQL 8.0.17

Example with Source Code

Let’s see the working example below…

Creating Project

Create a gradle project in Eclipse, the project structure looks similar to the below image:

preauthorize haspermission

Updating Build Script

We will add the required dependencies for our Spring Security PreAuthorize hasRole() and hasPermission() example.

We have added dependencies for spring security.

buildscript {
	ext {
		springBootVersion = '2.2.4.RELEASE'
	}
	
    repositories {
        mavenCentral()
    }
    
    dependencies {
        classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
    }
}

plugins {
    id 'java-library'
    id 'org.springframework.boot' version '2.2.4.RELEASE'
}

sourceCompatibility = 12
targetCompatibility = 12

repositories {
    mavenCentral()
}

dependencies {
    implementation("org.springframework.boot:spring-boot-starter-web:${springBootVersion}")
	implementation("org.springframework.boot:spring-boot-starter-security:${springBootVersion}")
	implementation("org.springframework.boot:spring-boot-starter-jdbc:${springBootVersion}")
	implementation('mysql:mysql-connector-java:8.0.17')
    //required only if jdk 9 or higher version is used
    runtimeOnly('javax.xml.bind:jaxb-api:2.4.0-b180830.0359')
}

If you are using maven based build tool then you can use below pom.xml file:

<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-security-preauthorize-has-permission</artifactId>
	<version>0.0.1-SNAPSHOT</version>

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

	<properties>
		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
	</properties>

	<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-jdbc</artifactId>
		</dependency>
		
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<version>8.0.17</version>
		</dependency>
		
		//required only if jdk 9 or higher version is used
		<dependency>
			<groupId>javax.xml.bind</groupId>
			<artifactId>jaxb-api</artifactId>
			<version>2.4.0-b180830.0359</version>
			<scope>runtime</scope>
		</dependency>
	</dependencies>

    <build>
        <plugins>
			<plugin>
				<groupId>org.apache.maven.plugins</groupId>
				<artifactId>maven-compiler-plugin</artifactId>
				<version>3.8.1</version>
				<configuration>
					<source>at least 8</source>
					<target>at least 8</target>
				</configuration>
			</plugin>
		</plugins>
	</build>
</project>

Creating Permission Evaluator

We need to use hasPermission() method 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.security.preauth.permission;
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;
	}
}

Create MySQL Tables

We are going to authenticate user using database, so we are going to create two tables in the MySQL database – user and user_role.

CREATE TABLE IF NOT EXISTS `user` (
  `user_name` varchar(30) NOT NULL,
  `user_pass` varchar(255) NOT NULL,
  `enable` tinyint(4) NOT NULL DEFAULT '1',
  PRIMARY KEY (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

CREATE TABLE IF NOT EXISTS `user_role` (
  `user_name` varchar(30) NOT NULL,
  `user_role` varchar(15) NOT NULL,
  KEY `user_name` (`user_name`),
  CONSTRAINT `user_role_ibfk_1` FOREIGN KEY (`user_name`) REFERENCES `user` (`user_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

We need to dump some users and their roles to test our application. Note that you need to put ROLE_ as a prefix when you insert data for role of a user.

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


INSERT INTO `user_role` (`user_name`, `user_role`) VALUES
	('user', 'ROLE_USER'),
	('admin', 'ROLE_USER'),
	('admin', 'ROLE_ADMIN');

Application Properties

The classpath file src/main/resources/application.properties file is used to declare the database settings.

#datasource
jdbc.driverClassName=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/jeejava
jdbc.username=root
jdbc.password=root

Database Configuration Class

We will create Datasource and JdbcTemplate beans which are required to interact with database and fetch data from database.

package com.roytuts.spring.security.preauth.config;

import javax.sql.DataSource;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.datasource.DriverManagerDataSource;

@Configuration
public class DatabaseConfig {

	@Autowired
	private Environment environment;

	@Bean
	public DataSource dataSource() {
		DriverManagerDataSource dataSource = new DriverManagerDataSource();

		dataSource.setDriverClassName(environment.getProperty("jdbc.driverClassName"));
		dataSource.setUrl(environment.getProperty("jdbc.url"));
		dataSource.setUsername(environment.getProperty("jdbc.username"));
		dataSource.setPassword(environment.getProperty("jdbc.password"));

		return dataSource;
	}

	@Bean
	public JdbcTemplate jdbcTemplate() {
		return new JdbcTemplate(dataSource());
	}

}

Create Model Class

We will create model classes to represent table data as an object.

package com.roytuts.spring.security.preauth.model;

public class User {

	private String username;
	private String userpwd;

	//getters and setters

}
package com.roytuts.spring.security.preauth.model;

public class Role {

	private String role;

	//getter and setter

}

Row Mapper Class

We need to map the fetched rows from database table to Java object. Therefore we need to implement Spring RowMapper interface to map rows into java object.

package com.roytuts.spring.security.preauth.row.mapper;

import java.sql.ResultSet;
import java.sql.SQLException;

import org.springframework.jdbc.core.RowMapper;

import com.roytuts.spring.security.preauth.model.User;

public class UserRowMapper implements RowMapper<User> {

	@Override
	public User mapRow(ResultSet rs, int rowNum) throws SQLException {
		User user = new User();
		user.setUsername(rs.getString("user_name"));
		user.setUserpwd(rs.getString("user_pass"));
		return user;
	}

}

DAO Class

We will create a Spring Repository class to fetch data from database.

package com.roytuts.spring.security.preauth.dao;

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

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import com.roytuts.spring.security.preauth.model.Role;
import com.roytuts.spring.security.preauth.model.User;
import com.roytuts.spring.security.preauth.row.mapper.UserRowMapper;

@Repository
public class UserDao {

	@Autowired
	private JdbcTemplate jdbcTemplate;

	public User getUser(String username) {
		return jdbcTemplate.queryForObject("select user_name, user_pass from user where user_name = ?",
				new Object[] { username }, new UserRowMapper());
	}

	public List<Role> getRoles(String username) {
		List<Map<String, Object>> results = jdbcTemplate
				.queryForList("select user_role from user_role where user_name = ?", new Object[] { username });
		List<Role> roles = results.stream().map(m -> {
			Role role = new Role();
			role.setRole(String.valueOf(m.get("user_role")));
			return role;
		}).collect(Collectors.toList());
		return roles;
	}

}

In the above class we are using Spring JDBC template API to fetch data.

Spring UserDetailsService

We will use Spring’s UserDetailsService to authenticate user with his/her role(s) from database.

package com.roytuts.spring.security.preauth.service;

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

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.security.preauth.dao.UserDao;
import com.roytuts.spring.security.preauth.model.Role;
import com.roytuts.spring.security.preauth.model.User;

@Service
public class UserAuthService implements UserDetailsService {

	@Autowired
	private UserDao userDao;

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

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

		List<Role> roles = userDao.getRoles(username);

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

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

		return usr;
	}

}

Creating Configuration Class

The below class configures Spring Security.

We need below security configuration using Java annotation in order to use authorization.

Enable Web Security using below class. Configure global-method-security pre-post-annotation using Java configuration.

Here, the in-memory authentication has been provided. Ideally in the application, the authentication should happen through database or LDAP or any other third party API etc.

We have used PasswordEncoder because plain text password is not acceptable in current version of Spring Security and you will get below exception if you do not use PasswordEncoder.

java.lang.IllegalArgumentException: There is no PasswordEncoder mapped for the id "null"
	at org.springframework.security.crypto.password.DelegatingPasswordEncoder$UnmappedIdPasswordEncoder.matches(DelegatingPasswordEncoder.java:244) ~[spring-security-core-5.1.5.RELEASE.jar:5.1.5.RELEASE]

As the passwords are in encrypted format in the below class, so you won’t find it easier until I tell you. The password for user is user and for admin is admin.

The below example shows both ways to authenticate the user – in-memory authentication as well as database authentication.

Source code of the configuration class is given below:

package com.roytuts.spring.security.preauth.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;
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.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

import com.roytuts.spring.security.preauth.permission.CustomPermissionEvaluator;
import com.roytuts.spring.security.preauth.service.UserAuthService;

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

	@Autowired
	private CustomPermissionEvaluator permissionEvaluator;
	
	@Autowired
	private UserAuthService userAuthService;

	/*@Autowired
	public void registerGlobal(AuthenticationManagerBuilder auth) throws Exception {
		// Ideally database authentication is required
		auth.inMemoryAuthentication().passwordEncoder(passwordEncoder()).withUser("user")
				.password("$2a$10$9Xn39aPf4LhDpRGNWvDFqu.T5ZPHbyh8iNQDSb4aNSnLqE2u2efIu").roles("USER").and()
				.passwordEncoder(passwordEncoder()).withUser("admin")
				.password("$2a$10$dl8TemMlPH7Z/mpBurCX8O4lu0FoWbXnhsHTYXVsmgXyzagn..8rK").roles("USER", "ADMIN");
	}*/
	
	@Autowired
	public void registerGlobal(AuthenticationManagerBuilder auth) throws Exception {
		auth.userDetailsService(userAuthService).passwordEncoder(passwordEncoder());
	}

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

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

}

Here in the above class, admin has USER as well as ADMIN roles but user has only one role USER. Therefore admin can access its own URL as well as user’s URL but user can access only its own URL but not admin’s URL.

Creating REST Controller

Now create below REST Controller class to test the user’s access to a particular URL based on role using @PreAuthorize annotation.

We have defined three end-points with roles and one with role and read permission.

package com.roytuts.spring.security.preauth.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 PreAuthorizeRestController {

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

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

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

}

Creating Main Class

Spring Boot application created as standalone application and can be easily deployed into embedded Tomcat server using main class.

You need to exclude WebMvcAutoConfiguration class to avoid below exception because it does auto configuration for WebMvc too:

org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'resourceHandlerMapping' defined in class path resource [org/springframework/boot/autoconfigure/web/servlet/WebMvcAutoConfiguration
package com.roytuts.spring.security.preauth;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration;

@SpringBootApplication(exclude = WebMvcAutoConfiguration.class)
public class SpringSecurityPreauthHasPermissionApp {

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

}

Testing the Application

Run the main class to deploy your application into Tomcat server.

Now access the URL – http://localhost:8080/admin/access using credentials admin/admin then you will see the output You have ADMIN role and READ access. The output is shown in the below image:

@PreAuthorize annotation - hasPermission example in Spring Security

Now if you change the permission as hasPermission(‘hasAccess’,’WRITE’) then you will get HTTP Status 403 – Access is denied because admin does not have the WRITE permission.

Source Code

download source code

Thanks for reading.

Tags:

Leave a Reply

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