PreAuthorize HasRole Security Using Spring Boot Data JPA

Table of Contents

Introduction

PreAuthorize hasRole Spring security will determine whether a user can invoke a method or not based on his/her role. hasRole() method returns true if the current principal has the specified role. By default if the supplied role does not start with ROLE_ will be added. This can be customized by modifying the defaultRolePrefix on DefaultWebSecurityExpressionHandler.

In this Spring Boot Security with Pre-Authorize hasRole example, I am going to use Spring Data JPA API to query the database. I am also going to implement Spring’s built-in service interface UserDetailsService and override the method loadUserByUsername(String username).

Related Posts:

Where is @PreAuthorize applicable?

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

@PreAuthorize("hasRole('USER')")
public void create(Contact contact);

which means that access will only be allowed for users with the role ROLE_USER. 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-hasrole-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.hasrole.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.hasrole.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.hasrole.data.jpa.repository;

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

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

public interface UserRepository extends JpaRepository<User, Integer> {

	User findByUserName(final String userName);

}

The following interface is for user role:

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

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

import com.roytuts.spring.preauthorize.hasrole.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.hasrole.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.hasrole.data.jpa.entity.User;
import com.roytuts.spring.preauthorize.hasrole.data.jpa.entity.UserRole;
import com.roytuts.spring.preauthorize.hasrole.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;
	}

}

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.

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

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.ui.Model;
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> defaultPage(Model model) {
		return new ResponseEntity<String>("You have USER role.", HttpStatus.OK);
	}

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

}

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.hasrole.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.hasrole.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 PasswordEncoder bean is configured as follows:

package com.roytuts.spring.preauthorize.hasrole.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();
	}

}

Plain text password is not allowed in Spring Boot 2.2.x or more.

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.hasrole.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.hasrole.data.jpa.entity")
@EnableJpaRepositories(basePackages = "com.roytuts.spring.preauthorize.hasrole.data.jpa.repository")
public class App {

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

}

Testing Spring Security – PreAuthorize hasRole

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 user/user then you will get HTTP Status 403 – Forbidden because user does not have role ADMIN.

spring security hasrole

For admin, use the credential admin/admin to see the page:

spring security preauthorize

To see user’s page, use credential user/user:

spring security preauthorize hasrole

Hope you got an idea how to work with Spring Security PreAuthorize hasRole method.

Source Code

Download

Leave a Reply

Your email address will not be published.