Spring Security Has-Role
In this tutorial I will show you an example on @PreAuthorize
annotation – hasRole()
example in Spring Security. @PreAuthorize
is the most useful annotation that decides whether a method can actually be invoked or not based on user’s 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
.
I will authenticate user using in-memory credentials as well as database credentials. I will use here MySQL database to authenticate role based authentication by implementing Spring’s built-in service UserDetailsService
.
Related Post:
@PreAuthorize annotation – hasPermission example in Spring Security
Where is @PreAuthorize applicable?
This @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+, Gradle 4.10.2/5.6, Maven 3.6.1/3.8.5, Spring Boot 2.1.5/2.2.4/2.7.4, MySQL 8.0.17/8.0.26
Project Setup
Create a gradle or maven based project in your favorite IDE or tool.
If you are using maven based build tool, then you can use the following pom.xml file. I will add the required dependencies for our Spring Security @PreAuthorize
hasRole()
example. I have added dependencies for spring security.
<?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-security-preauthorize-has-role</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.7.5</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-jdbc</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>
If you are using gradle as a build tool, then you can use the following build.gradle script:
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')
}
MySQL Tables
I am going to authenticate user using database, so I am 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 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;
I will 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/roytuts
jdbc.username=root
jdbc.password=root
Database Configuration
I will create Datasource and JdbcTemplate beans which are required to interact with database and fetch data from database.
@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());
}
}
Model Class
I will create model classes to represent table data as an object.
public class User {
private String username;
private String userpwd;
//getters and setters
}
The following class is for user roles.
public class Role {
private String role;
//getter and setter
}
Row Mapper Class
I need to map the fetched rows from database table to Java object. Therefore, I need to implement Spring RowMapper
interface to map rows into java object.
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
I will create a Spring Repository class to fetch data from database.
@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, I am using Spring JDBC template API to fetch data.
If you are using spring boot version 2.6 or later then you need to change the queryForObject()
method, because it has been deprecated:
return jdbcTemplate.queryForObject("select user_name, user_pass from user where user_name = ?", new UserRowMapper(), username);
Spring UserDetailsService
I will use Spring’s UserDetailsService
to authenticate user with his/her role(s) from database.
@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;
}
}
Spring Security Configuration
The below class configures Spring Security.
I 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.
I 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)
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.
Source code of the configuration class is given below:
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringPreAuthorizeSecurityConfig extends WebSecurityConfigurerAdapter {
@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 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.
Update for Spring 2.7.5
If you are using spring boot 2.6 or later version, then you need a separate class for Password Encoder.
@Configuration
public class EncoderConfig {
@Bean
PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Then class should be used as a bean into the security config class.
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class SpringPreAuthorizeSecurityConfig {
@Autowired
private UserAuthService userAuthService;
@Autowired
private PasswordEncoder passwordEncoder;
/*@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);
}
}
Spring REST Controller
Now create below REST Controller class to test the user’s access to a particular URL based on role using @PreAuthorize
annotation.
@RestController
public class PreAuthorizeRestController {
@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);
}
}
Spring Boot Main Class
Spring Boot application created as standalone application and can be easily deployed into embedded Tomcat server using main class.
@SpringBootApplication
public class SpringSecurityPreauthHasRoleApp {
public static void main(String[] args) {
SpringApplication.run(SpringSecurityPreauthHasRoleApp.class, args);
}
}
Testing Spring Security hasRole
Run the main class to deploy your application into Tomcat server.
Now access the URL http://localhost:8080/admin using credentials user/user then you will get HTTP Status 403 – Access is denied because user does not have the role ADMIN.
But if you had logged in using credentials admin/admin, you could have got the access.