Introduction
Welcome to my blog, where we'll embark on an exciting journey into the realm of web application security! If you're new to the world of Spring Boot or just beginning to explore the intricacies of authentication and authorization, you've come to the right place. In this inaugural article, I'm thrilled to share my take on a topic that resonates with developers of all levels: Building a Role-Based Access Control System with JWT in Spring Boot.
In this blog series, we'll dive deep into the world of Spring Security and explore how JSON Web Tokens (JWT) can amplify its capabilities. I'll guide you step by step, from laying the foundation with a solid Spring Boot setup, all the way to implementing role-based access control using JWT tokens.
In this blog, we'll cover the following topics:
- Creating Different Roles and Assigning it to Users
- Registering Users and Administrators
- Generating JWT Tokens and Authentication
- Performing Role-Based Actions
Prerequisites
Before we dive into the exciting world of building a role-based access control system with JWT in Spring Boot, let's ensure that you have the necessary tools and environment ready. Here's what you'll need to get started:
Java Development Kit (JDK)
: Make sure you have a compatible version of JDK installed on your machine. Spring Boot usually works well with JDK 8 or higher.
Integrated Development Environment (IDE)
: Choose an IDE that suits your preferences. IntelliJ IDEA and Eclipse are popular choices for Spring Boot development.
Maven or Gradle
: You'll need either Maven or Gradle as a build tool to manage your project dependencies.
Setting up the Environment:
Spring Boot Project Initialization: Create a new Spring Boot project using either Spring Initializr web tool here or your IDE's project creation wizard.
Here is my setup you can follow:
Project: Maven
Spring Boot Version:2.7.3 (if not available then use 3.1.3 then change it later in pom.xml)
Java Version:17
Dependencies:
1.Spring Data JPA
2.Spring web
3.Spring Security
4.MySQL Driver
5.JSON WEB TOKEN
You will not find JSON web Token there you have to add it manually
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</dependency>
Here is full pom.xml
<?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 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.7.3</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.alpha</groupId>
<artifactId>alpha</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>alpha</name>
<description>Demo project for Spring Boot Role based Authentication using JWT </description>
<properties>
<java.version>17</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>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.1</version>
</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>
</plugin>
</plugins>
</build>
</project>
application.properties
jwt.token.validity=18000
jwt.signing.key=YourSignInKey
jwt.authorities.key=roles
jwt.token.prefix=Bearer
jwt.header.string=Authorization
spring.datasource.url=jdbc:mysql://localhost:3306/yourdb
spring.datasource.username=root
spring.datasource.password=root
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=update
spring.user.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
server.port=8080
This is a configuration file for a Spring Boot application.
- The first line sets the validity duration for JSON Web Tokens (JWT) to 18000 seconds (5 hours).
- The second line specifies the signing key to be used for generating and validating JWTs.
- The third line defines the key for extracting authorities/roles from a JWT.
- The fourth line sets the prefix for JWT tokens to "Bearer".
- The fifth line specifies the header string to be used for JWT tokens in HTTP requests.
- The next few lines configure the MySQL database connection for the application, including the URL, username, and password.
- The line
spring.jpa.show-sql=true
enables the display of SQL statements executed by Hibernate. - The line
spring.jpa.hibernate.ddl-auto=update
configures Hibernate to automatically update the database schema based on the entity classes. - The line
spring.user.datasource.driver-class-name=com.mysql.jdbc.Driver
specifies the driver class for the user datasource. - The line
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
sets the Hibernate dialect for MySQL. - The last line sets the server port to 8080.
"As you can see, here is the clear folder structure."
src
└── main
├── java
│ └── com
│ └── alpha
│ ├── config (Package)
│ │ ├── JwtAuthenticationFilter.java
│ │ ├── PasswordEncoder.java
│ │ ├── TokenProvider.java
│ │ ├── UnauthorizedEntryPoint.java
│ │ └── WebSecurityConfig.java
│ ├── controller (Package)
│ │ └── UserController.java
│ ├── dao (Package)
│ │ ├── RoleDao.java
│ │ └── UserDao.java
│ ├── model (Package)
│ │ ├── AuthToken.java
│ │ ├── LoginUser.java
│ │ ├── Role.java
│ │ ├── User.java
│ │ └── UserDto.java
│ ├── service (Package)
│ │ ├── impl
│ │ │ ├── RoleServiceImpl.java
│ │ │ └── UserServiceImpl.java
│ │ ├── RoleService.java
│ │ └── UserService.java
│ └── AlphaApplication.java
└── resources
└── application.properties
Let's get started!
Now we will start with understanding the config package
Spring Security is a powerful and highly customizable security framework provided by the Spring Framework for Java applications. Its primary purpose is to handle authentication, authorization, and various security aspects in web and enterprise applications. Spring Security is often used to secure web applications, RESTful APIs, and other components of a software system.
Authentication:
Authentication is the process of confirming the identity of a person or entity. It ensures that the person or entity is who they claim to be before granting access to something. It's like checking someone's ID before allowing them to enter a secure area.
Imagine a professional cricket match. Before a player can step onto the field, they must prove their identity. They do this by showing their official player card with their name, photo, and a unique ID number. The match officials, like the umpires and team captains, examine the card to ensure it matches the player's appearance and is on the list of authorized players. Once confirmed, the player is authenticated and allowed to participate in the game.
Authorization:
Authorization comes after authentication and determines what actions or resources an authenticated person or entity is allowed to access or perform. It specifies the level of access and control based on roles, permissions, or rules.
Once a cricket player is authenticated and on the field, authorization kicks in. Each player has a specific role (e.g., batsman, bowler, fielder) with associated actions they can perform. For example, a bowler is authorized to bowl, a batsman is authorized to bat, and a wicketkeeper is authorized to keep wickets. The coach or captain may have special authorization to make strategic decisions during the match, like changing the batting order or field placements.
lets move on to our config files one by one
CORSFilter :
CORSFilter class is responsible for handling CORS-related settings in a web application. It intercepts incoming HTTP requests, adds the necessary CORS headers to the response, and then allows the request to continue processing using chain.doFilter(req, res). This filter helps control and secure how resources on the server are accessed by different origins in a web application.
package com.alpha.config;
import javax.servlet.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class CORSFilter implements Filter {
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
HttpServletResponse response = (HttpServletResponse) res;
response.setHeader("Access-Control-Allow-Origin", "*");
response.setHeader("Access-Control-Allow-Credentials", "true");
response.setHeader("Access-Control-Allow-Methods", "POST, GET, PUT, OPTIONS, DELETE");
response.setHeader("Access-Control-Max-Age", "3600");
response.setHeader("Access-Control-Allow-Headers", "X-Requested-With, Content-Type, Authorization, Origin, Accept, Access-Control-Request-Method, Access-Control-Request-Headers");
chain.doFilter(req, res);
}
public void init(FilterConfig filterConfig) {}
public void destroy() {}
}
This class implements the Filter interface, which is part of the Java Servlet API. Filters are used to perform actions on HTTP requests and responses as they pass through the application.
-
doFilter method :-
This method is required when implementing the Filter interface. It is called for each incoming HTTP request.
Inside this method, the code is responsible for adding necessary CORS headers to the HTTP response.
CORS headers are used to control and define the policy for cross-origin requests. They specify who can access the resources of a web page and what operations are permitted from different origins.
The code in this method sets various CORS headers, such as
Access-Control-Allow-Origin
,Access-Control-Allow-Methods
, and others. These headers dictate which domains are allowed to access the resources, which HTTP methods are permitted, and other CORS-related policies.
The init
method is used for initialization tasks that the filter may need when it's first created.
The destroy
method is called when the filter is being removed or shut down.
WebSecurityConfig
This class is responsible for configuring security settings, such as authentication, authorization, and request filtering, in a Spring Boot web application. It also integrates custom components, like the JwtAuthenticationFilter, to handle specific security
requirements. This configuration is a common setup for securing RESTful APIs or web applications using Spring Security.
package com.alpha.config;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
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.security.web.authentication.UsernamePasswordAuthenticationFilter;
import javax.annotation.Resource;
@Configuration
@EnableWebSecurity
@EnableGlobalMethodSecurity(prePostEnabled = true)
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Resource(name = "userService")
private UserDetailsService userDetailsService;
@Autowired
private PasswordEncoder encoder;
@Autowired
private UnauthorizedEntryPoint unauthorizedEntryPoint;
@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
auth.userDetailsService(userDetailsService).passwordEncoder(encoder.encoder());
}
@Override
protected void configure(HttpSecurity http) throws Exception {
http.cors().and().csrf().disable()
.authorizeRequests()
.antMatchers("/users/authenticate", "/users/register").permitAll()
.anyRequest().authenticated()
.and()
.exceptionHandling().authenticationEntryPoint(unauthorizedEntryPoint).and()
.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS);
http.addFilterBefore(authenticationTokenFilterBean(), UsernamePasswordAuthenticationFilter.class);
}
@Override
@Bean
public AuthenticationManager authenticationManagerBean() throws Exception {
return super.authenticationManagerBean();
}
@Bean
public JwtAuthenticationFilter authenticationTokenFilterBean() throws Exception {
return new JwtAuthenticationFilter();
}
}
@Configuration:
Indicates that this class contains configuration settings for the application.
@EnableWebSecurity:
Enables Spring Security features for the web application.
@EnableGlobalMethodSecurity(prePostEnabled = true):
Enables method-level security annotations such as @PreAuthorize
and @PostAuthorize
.
@Resource:
Injects the UserDetailsService
bean, likely responsible for user-related operations.
@Autowired:
Injects instances of PasswordEncoder
and UnauthorizedEntryPoint
beans, which are used for password hashing and handling unauthorized access, respectively.
configure(AuthenticationManagerBuilder auth) Method:
- This method configures the authentication manager.
- It specifies that the
userDetailsService
bean should be used for user authentication and sets the password encoder.
configure(HttpSecurity http) Method:
- This method configures the HTTP security settings.
- It includes settings for CORS (Cross-Origin Resource Sharing), CSRF (Cross-Site Request Forgery), and URL permissions.
- It specifies which URLs are accessible without authentication ("/users/authenticate" and "/users/register") and requires authentication for all other requests.
- It sets an authentication entry point for handling unauthorized access and defines the session management policy as STATELESS.
- The addFilterBefore method adds a custom
JwtAuthenticationFilter
before theUsernamePasswordAuthenticationFilter
to handle JWT (JSON Web Token) authentication.
authenticationManagerBean() Method:
- This method declares an AuthenticationManager bean, which is used for user authentication.
JwtAuthenticationFilter Bean:
- This method declares a bean for the JwtAuthenticationFilter, which is a custom filter used for JWT-based authentication.
TokenProvider
This class is responsible for handling JWTs in a Spring Boot application's security flow. It can generate tokens, extract user information from tokens, validate tokens, and create authentication tokens for users based on the information stored in the JWTs.
package com.alpha.config;
import io.jsonwebtoken.*;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Component;
import java.io.Serializable;
import java.util.Arrays;
import java.util.Collection;
import java.util.Date;
import java.util.function.Function;
import java.util.stream.Collectors;
@Component
public class TokenProvider implements Serializable {
@Value("${jwt.token.validity}")
public long TOKEN_VALIDITY;
@Value("${jwt.signing.key}")
public String SIGNING_KEY;
@Value("${jwt.authorities.key}")
public String AUTHORITIES_KEY;
public String getUsernameFromToken(String token) {
return getClaimFromToken(token, Claims::getSubject);
}
public Date getExpirationDateFromToken(String token) {
return getClaimFromToken(token, Claims::getExpiration);
}
public <T> T getClaimFromToken(String token, Function<Claims, T> claimsResolver) {
final Claims claims = getAllClaimsFromToken(token);
return claimsResolver.apply(claims);
}
private Claims getAllClaimsFromToken(String token) {
return Jwts.parser()
.setSigningKey(SIGNING_KEY)
.parseClaimsJws(token)
.getBody();
}
private Boolean isTokenExpired(String token) {
final Date expiration = getExpirationDateFromToken(token);
return expiration.before(new Date());
}
public String generateToken(Authentication authentication) {
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining(","));
return Jwts.builder()
.setSubject(authentication.getName())
.claim(AUTHORITIES_KEY, authorities)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + TOKEN_VALIDITY*1000))
.signWith(SignatureAlgorithm.HS256, SIGNING_KEY)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = getUsernameFromToken(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
UsernamePasswordAuthenticationToken getAuthenticationToken(final String token, final Authentication existingAuth, final UserDetails userDetails) {
final JwtParser jwtParser = Jwts.parser().setSigningKey(SIGNING_KEY);
final Jws<Claims> claimsJws = jwtParser.parseClaimsJws(token);
final Claims claims = claimsJws.getBody();
final Collection<? extends GrantedAuthority> authorities =
Arrays.stream(claims.get(AUTHORITIES_KEY).toString().split(","))
.map(SimpleGrantedAuthority::new)
.collect(Collectors.toList());
return new UsernamePasswordAuthenticationToken(userDetails, "", authorities);
}
}
@Component:
Marks this class as a Spring component, allowing it to be automatically scanned and registered as a bean in the Spring application context.
- getUsernameFromToken(String token): This method extracts the username (subject) from a JWT token.
- getExpirationDateFromToken(String token): Retrieves the expiration date from a JWT token.
- getClaimFromToken(String token, Function claimsResolver): A generic method to extract claims from a JWT token.
- getAllClaimsFromToken(String token): Parses and retrieves all claims (payload) from a JWT token.
- isTokenExpired(String token): Checks whether a JWT token has expired based on its expiration date.
- generateToken(Authentication authentication): Generates a new JWT token based on the provided Authentication object. It includes the subject (username), authorities, issuance time, and expiration time.
- validateToken(String token, UserDetails userDetails): Validates a JWT token against the provided UserDetails. It checks if the token's subject matches the user's username and if the token is not expired.
- getAuthenticationToken(final String token, final Authentication existingAuth, final UserDetails userDetails): This method parses a JWT token to create an Authentication object containing the user's authorities. It's used for authenticating users based on JWT tokens.
JwtAuthenticationFilter
JwtAuthenticationFilter is responsible for intercepting incoming requests, extracting JWTs from request headers, and authenticating users based on the tokens. It ensures that authenticated users have their security context set, allowing them to access protected resources within the application.
package com.alpha.config;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.SignatureException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.web.filter.OncePerRequestFilter;
import javax.annotation.Resource;
import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class JwtAuthenticationFilter extends OncePerRequestFilter {
@Value("${jwt.header.string}")
public String HEADER_STRING;
@Value("${jwt.token.prefix}")
public String TOKEN_PREFIX;
@Resource(name = "userService")
private UserDetailsService userDetailsService;
@Autowired
private TokenProvider jwtTokenUtil;
@Override
protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) throws IOException, ServletException {
String header = req.getHeader(HEADER_STRING);
String username = null;
String authToken = null;
if (header != null && header.startsWith(TOKEN_PREFIX)) {
authToken = header.replace(TOKEN_PREFIX, "");
try {
username = jwtTokenUtil.getUsernameFromToken(authToken);
} catch (IllegalArgumentException e) {
logger.error("Error occurred while retrieving Username from Token", e);
} catch (ExpiredJwtException e) {
logger.warn("The token has expired", e);
} catch (SignatureException e) {
logger.error("Authentication Failed. Invalid username or password.");
}
} else {
logger.warn("Bearer string not found, ignoring the header");
}
if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
if (jwtTokenUtil.validateToken(authToken, userDetails)) {
UsernamePasswordAuthenticationToken authentication = jwtTokenUtil.getAuthenticationToken(authToken, SecurityContextHolder.getContext().getAuthentication(), userDetails);
authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(req));
logger.info("User authenticated: " + username + ", setting security context");
SecurityContextHolder.getContext().setAuthentication(authentication);
}
}
chain.doFilter(req, res);
}
}
- This class extends
OncePerRequestFilter
, which ensures that this filter is applied only once per request. -
doFilterInternal
Method:This method is the core of the filter and is called for each incoming HTTP request. - The method first checks if a JWT is present in the request header and if it starts with the defined token prefix ("Bearer ").
- If a valid token is found, it attempts to extract the username from the token using the TokenProvider class.
- It catches exceptions for various token-related errors, such as token expiration (
ExpiredJwtException
) and invalid signatures (SignatureException
), and logs them. - If a valid username is obtained from the token and there is no existing authentication context, it loads the user details from the
UserDetailsService
based on the username. - It then validates the token against the user details using the
TokenProvider
. If the token is valid, it creates anUsernamePasswordAuthenticationToken
containing the user details and sets the authentication details. - Finally, it sets the authenticated user's security context using
SecurityContextHolder
. - After handling authentication, the filter continues the request processing by invoking
chain.doFilter(req, res)
.
BCryptPasswordEncoder
This bean can be used throughout the application for securely hashing and verifying passwords, especially in the context of user authentication and security. It's a common practice to configure and manage password encoding components like this in Spring applications to enhance security
.
package com.alpha.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
@Configuration
public class PasswordEncoder {
@Bean
public BCryptPasswordEncoder encoder(){
return new BCryptPasswordEncoder();
}
}
-
BCryptPasswordEncoder
is a popular password hashing library in the Spring Security framework. It's used to securely hash and verify passwords. - In this configuration, the
BCryptPasswordEncoder
bean is created and returned by theencoder()
method. This bean can then be injected into other parts of the application, such as Spring Security configurations, to handle password encoding and decoding.
UnauthorizedEntryPoint
UnauthorizedEntryPoint class is responsible for handling unauthorized access to protected resources in a Spring Security-enabled application. When an unauthenticated user attempts to access a protected resource, this class sends an HTTP response with a status code of 401, indicating that the request lacks valid authentication. This response informs the client that they need to provide proper authentication credentials to access the resource.
package com.alpha.config;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.stereotype.Component;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.Serializable;
@Component
public class UnauthorizedEntryPoint implements AuthenticationEntryPoint, Serializable {
@Override
public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException {
response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthenticated");
}
}
This class implements the AuthenticationEntryPoint
interface, which is part of Spring Security. The AuthenticationEntryPoint
interface is responsible for handling authentication-related exceptions, particularly unauthorized access.
- The
commence
method is the main method of this class, and it's called when an authentication exception occurs during an HTTP request. -
It takes three parameters:
1.
HttpServletRequest request
: Represents the incoming HTTP
request.
2.HttpServletResponse response
: Represents the HTTP response
that will be sent back to the client.
3.AuthenticationException authException
: Represents the
authentication exception that occurred, typically due to
unauthorized access. In this method, it sends an HTTP response with a status code of
401 Unauthorized
and a message of "Unauthenticated." This is a standard response for indicating that the request lacks valid authentication credentials or authorization.
okay lets start with our model classes
User
The User class represents a user entity with attributes like username, password, email, phone, name, and roles. It also defines a many-to-many relationship with the Role entity, allowing users to have multiple roles.
package com.alpha.model;
import com.fasterxml.jackson.annotation.JsonIgnore;
import javax.persistence.*;
import java.util.Set;
@Entity
public class User {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private long id;
@Column
private String username;
@Column
@JsonIgnore
private String password;
@Column
private String email;
@Column
private String phone;
@Column
private String name;
@ManyToMany(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
@JoinTable(name = "USER_ROLES",
joinColumns = {
@JoinColumn(name = "USER_ID")
},
inverseJoinColumns = {
@JoinColumn(name = "ROLE_ID") })
private Set<Role> roles;
public long getId() {
return id;
}
public void setId(long id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Set<Role> getRoles() {
return roles;
}
public void setRoles(Set<Role> roles) {
this.roles = roles;
}
}
- This defines a
many-to-many
relationship between the User entity and the Role entity. Users can have multiple roles, and roles can be associated with multiple users. - The
@ManyToMany
annotation indicates a many-to-many relationship. - The
fetch = FetchType.EAGER
attribute specifies that roles should be eagerly fetched when loading a user. - The
cascade = CascadeType.ALL
attribute specifies that if operations like persist, merge, remove, etc., are performed on a Userentity
, the same operations should be cascaded to its associated Role entities. - The
@JoinTable
annotation is used to define the name of the join table that holds the relationship between users and roles. It specifies the columns used for joining the tables.
Role
the Role class represents a user role entity with attributes like name and description. It is designed to be persisted in a database table and can be associated with users through a many-to-many relationship, as indicated by the User entity's relationship mapping that references Role.
package com.alpha.model;
import javax.persistence.*;
@Entity
public class Role {
@Id
@GeneratedValue(strategy= GenerationType.IDENTITY)
private long id;
@Column
private String name;
@Column
private String description;
// Getter for id
public long getId() {
return id;
}
// Setter for id
public void setId(long id) {
this.id = id;
}
// Getter for name
public String getName() {
return name;
}
// Setter for name
public void setName(String name) {
this.name = name;
}
// Getter for description
public String getDescription() {
return description;
}
// Setter for description
public void setDescription(String description) {
this.description = description;
}
}
the Role class represents a user role entity with attributes like name and description. It is designed to be persisted in a database table and can be associated with users through a many-to-many relationship, as indicated by the User entity's relationship mapping that references Role.
AuthToken
package com.alpha.model;
/**
* Represents an authentication token.
*/
public class AuthToken {
private String token;
/**
* Constructs a new AuthToken object.
*/
public AuthToken() {
}
/**
* Constructs a new AuthToken object with the specified token.
*
* @param token the authentication token
*/
public AuthToken(String token) {
this.token = token;
}
/**
* Returns the authentication token.
*
* @return the authentication token
*/
public String getToken() {
return token;
}
/**
* Sets the authentication token.
*
* @param token the authentication token to be set
*/
public void setToken(String token) {
this.token = token;
}
}
LoginUser
package com.alpha.model;
public class LoginUser {
private String username;
private String password;
// Getters and Setters for username
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
// Getters and Setters for password
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
}
UserDto
package com.alpha.model;
public class UserDto {
private String username;
private String password;
private String email;
private String phone;
private String name;
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
public String getPhone() {
return phone;
}
public void setPhone(String phone) {
this.phone = phone;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public User getUserFromDto(){
User user = new User();
user.setUsername(username);
user.setPassword(password);
user.setEmail(email);
user.setPhone(phone);
user.setName(name);
return user;
}
}
DAO
is a Spring Data JPA repository interface typically used for performing CRUD (Create, Read, Update, Delete) operations on the entity class. Let's break down its key components:
UserDao
package com.alpha.dao;
import com.alpha.model.User;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface UserDao extends CrudRepository<User, Long> {
User findByUsername(String username);
}
RoleDao
package com.alpha.dao;
import com.alpha.model.Role;
import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface RoleDao extends CrudRepository<Role, Long> {
Role findRoleByName(String name);
}
You can use JPARepository
also.
Service Layer
We will now proceed to define service interfaces for our User and Role services. These service interfaces will serve as blueprints for the actual service implementations and will encapsulate the core business logic.
As a best practice, using interfaces for service definitions promotes separation of concerns and allows for easy switching of implementations, such as when using mocking frameworks for testing.
RoleService
package com.alpha.service;
// Importing the Role model
import com.alpha.model.Role;
// Declaring the RoleService interface
public interface RoleService {
// Method to find a Role by its name
Role findByName(String name);
}
UserService
package com.alpha.service;
import com.alpha.model.User;
import com.alpha.model.UserDto;
import java.util.List;
public interface UserService {
// Saves a user
User save(UserDto user);
// Retrieves all users
List<User> findAll();
// Retrieves a user by username
User findOne(String username);
User createEmployee(UserDto user);
}
Now we will implement our service logic
RoleServiceImpl
package com.alpha.service.impl;
import com.alpha.dao.RoleDao;
import com.alpha.model.Role;
import com.alpha.service.RoleService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service(value = "roleService")
public class RoleServiceImpl implements RoleService {
@Autowired
private RoleDao roleDao;
@Override
public Role findByName(String name) {
// Find role by name using the roleDao
Role role = roleDao.findRoleByName(name);
return role;
}
}
UserServiceImpl
package com.alpha.service.impl;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import com.alpha.dao.UserDao;
import com.alpha.model.Role;
import com.alpha.model.User;
import com.alpha.model.UserDto;
import com.alpha.service.RoleService;
import com.alpha.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
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.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service(value = "userService")
public class UserServiceImpl implements UserDetailsService, UserService {
@Autowired
private RoleService roleService;
@Autowired
private UserDao userDao;
@Autowired
private BCryptPasswordEncoder bcryptEncoder;
// Load user by username
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
User user = userDao.findByUsername(username);
if(user == null){
throw new UsernameNotFoundException("Invalid username or password.");
}
return new org.springframework.security.core.userdetails.User(user.getUsername(), user.getPassword(), getAuthority(user));
}
// Get user authorities
private Set<SimpleGrantedAuthority> getAuthority(User user) {
Set<SimpleGrantedAuthority> authorities = new HashSet<>();
user.getRoles().forEach(role -> {
authorities.add(new SimpleGrantedAuthority("ROLE_" + role.getName()));
});
return authorities;
}
// Find all users
public List<User> findAll() {
List<User> list = new ArrayList<>();
userDao.findAll().iterator().forEachRemaining(list::add);
return list;
}
// Find user by username
@Override
public User findOne(String username) {
return userDao.findByUsername(username);
}
// Save user
@Override
public User save(UserDto user) {
User nUser = user.getUserFromDto();
nUser.setPassword(bcryptEncoder.encode(user.getPassword()));
// Set default role as USER
Role role = roleService.findByName("USER");
Set<Role> roleSet = new HashSet<>();
roleSet.add(role);
// If email domain is admin.edu, add ADMIN role
if(nUser.getEmail().split("@")[1].equals("admin.edu")){
role = roleService.findByName("ADMIN");
roleSet.add(role);
}
nUser.setRoles(roleSet);
return userDao.save(nUser);
}
@Override
public User createEmployee(UserDto user) {
User nUser = user.getUserFromDto();
nUser.setPassword(bcryptEncoder.encode(user.getPassword()));
Role employeeRole = roleService.findByName("EMPLOYEE");
Role customerRole = roleService.findByName("USER");
Set<Role> roleSet = new HashSet<>();
if (employeeRole != null) {
roleSet.add(employeeRole);
}
if (customerRole != null) {
roleSet.add(customerRole);
}
nUser.setRoles(roleSet);
return userDao.save(nUser);
}
}
lets create our controller class
The initial step for a user is to complete the registration process. At a minimum, users are required to provide a username and password. By invoking the service method to save the user, this essential step is completed.
To access the application's APIs securely, users must include a server-generated JWT (JSON Web Token). All the necessary groundwork for this has been laid out in our TokenProvider
. We utilize the generateToken
method and include the resulting token in the response, ensuring secure access to the APIs.
UserController
UserController
class handles user-related HTTP requests, including registration and authentication. It also demonstrates role-based access control for specific resources. The actual business logic for user operations is delegated to the UserService and TokenProvider
components.
package com.alpha.controller;
import com.alpha.config.TokenProvider;
import com.alpha.model.AuthToken;
import com.alpha.model.LoginUser;
import com.alpha.model.User;
import com.alpha.model.UserDto;
import com.alpha.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/users")
public class UserController {
@Autowired
private AuthenticationManager authenticationManager;
@Autowired
private TokenProvider jwtTokenUtil;
@Autowired
private UserService userService;
/**
* Generates a token for the given user credentials.
*
* @param loginUser The user's login credentials.
* @return A response entity containing the generated token.
* @throws AuthenticationException if authentication fails.
*/
@RequestMapping(value = "/authenticate", method = RequestMethod.POST)
public ResponseEntity<?> generateToken(@RequestBody LoginUser loginUser) throws AuthenticationException {
final Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
loginUser.getUsername(),
loginUser.getPassword()
)
);
SecurityContextHolder.getContext().setAuthentication(authentication);
final String token = jwtTokenUtil.generateToken(authentication);
return ResponseEntity.ok(new AuthToken(token));
}
/**
* Saves a new user.
*
* @param user The user to be saved.
* @return The saved user.
*/
@RequestMapping(value="/register", method = RequestMethod.POST)
public User saveUser(@RequestBody UserDto user){
return userService.save(user);
}
/**
* Returns a message that can only be accessed by users with the 'ADMIN' role.
*
* @return A message that can only be accessed by admins.
*/
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value="/adminping", method = RequestMethod.GET)
public String adminPing(){
return "Only Admins Can Read This";
}
/**
* Returns a message that can be accessed by any user.
*
* @return A message that can be accessed by any user.
*/
@PreAuthorize("hasRole('USER')")
@RequestMapping(value="/userping", method = RequestMethod.GET)
public String userPing(){
return "Any User Can Read This";
}
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value="/create/employee", method = RequestMethod.POST)
public User createEmployee(@RequestBody UserDto user){
return userService.createEmployee(user);
}
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value="/find/all", method = RequestMethod.GET)
public List<User> getAllList(){
return userService.findAll();
}
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping(value="/find/by/username", method = RequestMethod.GET)
public User getAllList(@RequestParam String username){
return userService.findOne(username);
}
}
These methods demonstrate role-based access control using Spring Security's @PreAuthorize
annotation. adminPing
can be accessed only by users with the 'ADMIN' role, while userPing
can be accessed by users with the 'USER' role.
Before testing your apis you need to add some roles into your db
INSERT INTO role (id, description, name) VALUES (1, 'Admin role', 'ADMIN');
INSERT INTO role (id, description, name) VALUES (2, 'Employee role', 'EMPLOYEE');
INSERT INTO role (id, description, name) VALUES (3, 'User role', 'USER');
Finally you can test your apis in postman
I have created collection for postman
Here is the whole code is in my github
If you find any doubt feel free to contact me on Instagram
Thanks for reading !
Happy Coding !
Top comments (0)