Monday, June 23, 2025

Spring Boot + Spring Security JWT Authentication Example

In this tutorial we’ll see how to create a Spring Boot application that uses Spring Security and JWT token based authentication to bring authentication and authorization to the exposed REST APIs. DB used is MySQL.

What does JWT do

JWT (JSON Web Token) is used for securing REST APIs.

In the JWT authentication process a client application first need to authenticate using credentials. The server side verifies the sent credentials, if valid then it generates and returns a JWT.

Once the client has been authenticated it has to sent the token in the request’s Authorization header in the Bearer Token form with each request. The server will check the validity of the token to verify the validity of the client and authorize or reject requests. You can also store roles and method usage will be authorized based on the role.

You can also configure the URLs that should be authenticated and those that will be permitted without authentication.

Spring Boot + Spring Security with JWT authentication example

In the application we’ll have the user signup and user login logic. Once the signup is done user should be authenticated when logging in, that configuration would be done using Spring security and JWT.

Technologies Used

  1. SpringBoot 3.5.x
  2. Java 21
  3. MySql 8.x
  4. Spring Security 6.x
  5. Hibernate 7.x
  6. JJWT (Java JWT library) 0.12.x

Maven Dependencies

These are the dependencies needed in the pom.xml file which include Spring security, Spring Data JPA, JWT and MySQL.

<?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>3.5.2</version>
    <relativePath/> <!-- lookup parent from repository -->
  </parent>
  <groupId>com.netjstech</groupId>
  <artifactId>springsecurity</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <name>myproj-1</name>
  <description>Spring Security</description>
  <url/>
  <properties>
    <java.version>21</java.version>
     <jjwt.version>0.12.6</jjwt.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>org.springframework.boot</groupId>
      <artifactId>spring-boot-devtools</artifactId>
      <scope>runtime</scope>
      <optional>true</optional>
    </dependency>
    <!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt-api</artifactId>
        <version>${jjwt.version}</version>
    </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
      <groupId>com.mysql</groupId>
      <artifactId>mysql-connector-j</artifactId>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.springframework.boot</groupId>
      <artifactId>spring-boot-starter-test</artifactId>
      <scope>test</scope>
    </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>

Project Structure

Once ready the project structure for the Spring Boot authentication application looks like as given below-

DB Tables

Since we are doing both authentication and authorization so there are two master tables for storing User and Role records. There is also a table user_role to capture roles assigned to particular users.

CREATE TABLE `netjs`.`users` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(30) NOT NULL,
  `email` VARCHAR(45) NOT NULL,
  `password` VARCHAR(150) NOT NULL,
  PRIMARY KEY (`id`),
  UNIQUE INDEX `name_UNIQUE` (`name` ASC),
  UNIQUE INDEX `email_UNIQUE` (`email` ASC));


CREATE TABLE `netjs`.`role` (
  `id` INT NOT NULL AUTO_INCREMENT,
  `name` VARCHAR(45) NULL,
  PRIMARY KEY (`id`));


CREATE TABLE `netjs`.`user_role` (
  `user_id` INT NOT NULL,
  `role_id` INT NOT NULL);

Insert the required roles in the Role table.

insert into role (name) values ("ROLE_ADMIN");
insert into role (name) values ("ROLE_USER");

Entity classes

Entity classes that map to the DB tables are as follows.

User.java

import java.util.HashSet;
import java.util.Set;
import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.FetchType;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.JoinColumn;
import jakarta.persistence.JoinTable;
import jakarta.persistence.ManyToMany;
import jakarta.persistence.Table;

@Entity
@Table(name="users")
public class User {
  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Integer id; 
  @Column(name="name")
  private String userName; 
  @Column(name="email")
  private String email;
  @Column(name="password")
  private String password;
  @ManyToMany(fetch = FetchType.LAZY)
  @JoinTable(name = "user_role", 
      joinColumns = @JoinColumn(name="USER_ID", referencedColumnName="ID"),
      inverseJoinColumns = @JoinColumn(name="ROLE_ID", referencedColumnName="ID"))
  private Set<Role> roles = new HashSet<>();
  public Integer getId() {
    return id;
  }
  public void setId(Integer id) {
    this.id = id;
  }
  public String getUserName() {
    return userName;
  }
  public void setUserName(String userName) {
    this.userName = userName;
  }
  public String getEmail() {
    return email;
  }
  public void setEmail(String email) {
    this.email = email;
  }
  public String getPassword() {
    return password;
  }
  public void setPassword(String password) {
    this.password = password;
  }
  public Set<Role> getRoles() {
    return roles;
  }
  public void setRoles(Set<Role> roles) {
    this.roles = roles;
  }
}

User has Many-to-Many relationship with Role, that association is captured using the join table user_role.

Role.java

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
import jakarta.persistence.Enumerated;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name="role")
public class Role {
	@Id
	@GeneratedValue(strategy = GenerationType.IDENTITY)
	private Integer id; 
	@Enumerated(EnumType.STRING)
	@Column(name="name")
	private Roles roleName;
	public Integer getId() {
		return id;
	}
	public void setId(Integer id) {
		this.id = id;
	}
	public Roles getRoleName() {
		return roleName;
	}
	public void setRoleName(Roles roleName) {
		this.roleName = roleName;
	}
}

Roles.java (Enum)

public enum Roles {
	ROLE_USER,
	ROLE_ADMIN
}

Ensure that you follow the same nomenclature ROLE_XXX as Spring security uses ROLE_ prefix by default when using hasRole() method.

CustomUserBean.java

Spring Security has an interface org.springframework.security.core.userdetails.UserDetails which provides core user information. You need to provide a concrete implemetation of this interface to add more fields. There is also a concrete implementation org.springframework.security.core.userdetails.User provided by Spring security that can be used directly. Here we are using our own implementation ConcreteUserBean.

Note that in the class there is also a getAuthorities() method that returns authorities of type GrantedAuthority. That list of GrantedAuthority objects is built using the instances of SimpleGrantedAuthority which is the basic concrete implementation of a GrantedAuthority. Stores a String representation of an authority granted to the Authentication object.

import java.util.List;
import java.util.stream.Collectors;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import com.fasterxml.jackson.annotation.JsonIgnore;

public class CustomUserBean implements UserDetails {
  private static final long serialVersionUID = -4709084843450077569L;  
  private Integer id; 
  private String userName; 
  private String email;
  @JsonIgnore
  private String password;
  private List<GrantedAuthority> authorities;
  CustomUserBean(Integer id, String userName, String email, 
      String password, List<GrantedAuthority> authorities){
    this.id = id;
    this.userName = userName;
    this.email = email;
    this.password = password;
    this.authorities = authorities;
  }
  
  public static CustomUserBean createInstance(User user) {
    List<GrantedAuthority> authorities = user.getRoles()
                         .stream()
                         .map(role -> new SimpleGrantedAuthority(role.getRoleName().name()))
                         .collect(Collectors.toList());
    return new CustomUserBean(user.getId(), user.getUserName(), 
        user.getEmail(), user.getPassword(), authorities);
  }
  @Override
  public List<GrantedAuthority> getAuthorities() {
    return authorities;
  }

  @Override
  public String getPassword() {
    return password;
  }

  @Override
  public String getUsername() {
    return userName;
  }

  public Integer getId() {
    return id;
  }

  public String getEmail() {
    return email;
  }

  @Override
  public boolean isAccountNonExpired() {
    return true;
  }

  @Override
  public boolean isAccountNonLocked() {
    return true;
  }

  @Override
  public boolean isCredentialsNonExpired() {
    return true;
  }

  @Override
  public boolean isEnabled() {
    return true;
  }

  @Override
  public boolean equals(Object rhs) {
    if (rhs instanceof CustomUserBean) {
      return userName.equals(((CustomUserBean) rhs).userName);
    }
    return false;
  }

  /**
   * Returns the hashcode of the {@code username}.
   */
  @Override
  public int hashCode() {
    return userName.hashCode();
  }
}

DTO Classes

Apart from these Entity classes there are DTO classes related to the request that is sent, response received and error reponse.

SignupRequestDto.java

This class captures the data for the SignUp request which is sent when a new user registers.

public class SignupRequestDto {
	private String userName; 
	private String email;
	private String password;
	private String[] roles;
	public String getUserName() {
		return userName;
	}
	public void setUserName(String userName) {
		this.userName = userName;
	}
	public String getEmail() {
		return email;
	}
	public void setEmail(String email) {
		this.email = email;
	}
	public String getPassword() {
		return password;
	}
	public void setPassword(String password) {
		this.password = password;
	}
	public String[] getRoles() {
		return roles;
	}
	public void setRoles(String[] roles) {
		this.roles = roles;
	}
}

AuthResponseDto.java

This class represents the response you get back when logging in. It includes the authentication token and the roles user is authorized for.

public class AuthResponseDto {
  private String token;
  private List<String> roles;

  public String getToken() {
    return token;
  }

  public void setToken(String token) {
    this.token = token;
  }

  public List<String> getRoles() {
    return roles;
  }

  public void setRoles(List<String> roles) {
    this.roles = roles;
  }  
}

ErrorResponseDto.java

This class represents the response you get back when there is an error.

import java.time.LocalDateTime;
import org.springframework.http.HttpStatus;
import com.fasterxml.jackson.annotation.JsonFormat;

public class ErrorResponseDto {
	private HttpStatus statusCode;
	@JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "dd-MM-yyyy hh:mm:ss")
	private LocalDateTime timestamp;
	private String message;
	private String exceptionMessage;
	ErrorResponseDto() {}
	public ErrorResponseDto(HttpStatus statusCode, String message, String exceptionMessage) {
		this.statusCode = statusCode;
		this.message = message;
		this.exceptionMessage = exceptionMessage;
		this.timestamp = LocalDateTime.now();
	}
	
	public HttpStatus getStatusCode() {
		return statusCode;
	}
	public void setStatus(HttpStatus statusCode) {
		this.statusCode = statusCode;
	}
	public LocalDateTime getTimestamp() {
		return timestamp;
	}
	public void setTimestamp(LocalDateTime timestamp) {
		this.timestamp = timestamp;
	}
	public String getMessage() {
		return message;
	}
	public void setMessage(String message) {
		this.message = message;
	}
	public String getExceptionMessage() {
		return exceptionMessage;
	}
	public void setExceptionMessage(String exceptionMessage) {
		this.exceptionMessage = exceptionMessage;
	}
	
}
  

Repositories

Since we are using Spring Data JPA so we just need to create interfaces extending the JpaRepository.

UserRepository

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.netjstech.entity.User;

@Repository
public interface UserRepository extends JpaRepository<User, Integer>{
  public Optional<User> findByUserName(String userName);
  public boolean existsByEmail(String email);
  public boolean existsByUserName(String userName);
}

RoleRepository

import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import com.netjstech.entity.Role;
import com.netjstech.entity.Roles;

@Repository
public interface RoleRepository extends JpaRepository<Role, Integer> {
  Optional<Role> findByRoleName(Roles role);
}

Configuring Spring Security and JWT

The following security configuration ensures that only authenticated users can access the APIs.

package com.netjstech.security;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpMethod;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import com.netjstech.service.UserDetailsServiceImpl;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig{
  private final UserDetailsServiceImpl userDetailsService;
  private final JwtTokenFilter jwtTokenFilter;
  
  SecurityConfig(UserDetailsServiceImpl userDetailsService, JwtTokenFilter jwtTokenFilter){
    this.userDetailsService = userDetailsService;
    this.jwtTokenFilter = jwtTokenFilter;
  }
  
  @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
    http.csrf((csrf) -> csrf.disable())
      .authorizeHttpRequests(auth -> auth.requestMatchers("/auth/**").permitAll()
          .requestMatchers(HttpMethod.GET, "/user/allusers").permitAll()
          .anyRequest().authenticated())
      .authenticationProvider(authenticationProvider())
      .addFilterBefore(jwtTokenFilter, UsernamePasswordAuthenticationFilter.class);
      
      return http.build();
  }
  
  @Bean 
  public PasswordEncoder passwordEncoder() {
    return new BCryptPasswordEncoder();
  }

  @Bean
  public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
    return config.getAuthenticationManager();
  }
  
  /* 
   * Configure to use DaoAuthenticationProvider
   * also set PasswordEncoder to encode password before persisting
   */
  public AuthenticationProvider authenticationProvider() {
    DaoAuthenticationProvider provider = new DaoAuthenticationProvider(userDetailsService);
    provider.setPasswordEncoder(passwordEncoder());
    return provider;
  }
}

Important points to note here-

The SecurityConfig class is annotated with @EnableWebSecurity to enable Spring Security’s web security support and provide the Spring MVC integration.

In Spring Boot 3, the SecurityFilterChain is a core component of Spring Security that defines how security filters are applied to HTTP requests within your application.

@EnableMethodSecurity- Used to enable method level security based on annotations.

Spring Security supports three different kinds of security annotations:

  • @Secured provided by Spring security itself
  • JSR-250’s @RolesAllowed annotation
  • Expression based annotations with @PreAuthorize, @PostAuthorize, @PreFilter and @PostFilter

We’ll be using @PreAuthorize annotation for securing methods as you will see in the Controller class.

If you see the security configuration any request whose path is /auth/** and GET request with path /user/allusers should not be authenticated. Any other request should be authenticated.

JwtTokenUtil.java

A utility class that does following three tasks-

  1. Generate the token by setting user name, issued time, expiration time.
  2. Validate token
  3. Get user name from taken.
import java.security.Key;
import java.util.Date;
import javax.crypto.SecretKey;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.UnsupportedJwtException;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;

@Component
public class JwtTokenUtil {
	private static final Logger logger = LoggerFactory.getLogger(JwtTokenUtil.class);
	@Value("${jwttoken.secret}")
	private String secretKey;
	@Value("${jwttoken.expiration}")
	private long jwtTokenExpiration;
	
	public String generateJwtToken(String userName) {
		return Jwts.builder()
				   .subject(userName)
				   .issuedAt(new Date(System.currentTimeMillis()))
				   .expiration(new Date(System.currentTimeMillis() + jwtTokenExpiration))
				   .signWith(getSignInKey())
				   .compact();
	}
	
	public boolean validateJwtToken(String token) {
		logger.info("Enter validateJwtToken");
		boolean flag = false;
		try {
			Jwts.parser()
				.verifyWith((SecretKey) getSignInKey())
				.build()
				.parseSignedClaims(token);
			flag = true;
		}catch(UnsupportedJwtException exp) {
			System.out.println("claimsJws argument does not represent Claims JWS " + exp.getMessage());
			throw new JwtException("claimsJws argument does not represent Claims JWS " + exp.getMessage());
		}catch(MalformedJwtException exp) {
			System.out.println("claimsJws string is not a valid JWS " + exp.getMessage());
			throw new JwtException("claimsJws string is not a valid JWS " + exp.getMessage());
		}catch(ExpiredJwtException exp) {
			System.out.println("Claims has an expiration time before the method is invoked " + exp.getMessage());
			throw new JwtException("Claims has an expiration time before the method is invoked " + exp.getMessage());
		}catch(IllegalArgumentException exp) {
			System.out.println("claimsJws string is null or empty or only whitespace " + exp.getMessage());
			throw new JwtException("claimsJws string is null or empty or only whitespace " + exp.getMessage());
		}
		logger.info("Exit validateJwtToken");
		return flag;
	}
	
	public String getUserNameFromJwtToken(String token) throws JwtException {
		logger.info("In getUserNameFromJwtToken");
		Claims claims = Jwts.parser()
					.verifyWith((SecretKey) getSignInKey())
					.build()
					.parseSignedClaims(token)
					.getPayload();
		return claims.getSubject();
		
	}
	
    private Key getSignInKey() {
        byte[] keyBytes = Decoders.BASE64.decode(secretKey);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

JwtTokenFilter.java

A filter class that extends OncePerRequestFilter that guarantees a single execution per requestdispatch.

In the overridden doFilterInternal() method, token is extracted from the request header and validated. If token is validated then get user name from it and use it to get UserDetails. From these user details and its authorities create an Authentication object.

import java.io.IOException;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.web.authentication.WebAuthenticationDetailsSource;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
import org.springframework.web.servlet.HandlerExceptionResolver;
import com.netjstech.service.UserDetailsServiceImpl;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;

@Component
public class JwtTokenFilter extends OncePerRequestFilter{
  private static final Logger logger = LoggerFactory.getLogger(JwtTokenFilter.class);

  private final JwtTokenUtil jwtTokenUtil;

  private final UserDetailsServiceImpl userDetailsService;
  
  private final HandlerExceptionResolver handlerExceptionResolver;
  
  public JwtTokenFilter(JwtTokenUtil jwtTokenUtil, UserDetailsServiceImpl userDetailsService, HandlerExceptionResolver handlerExceptionResolver){
    this.jwtTokenUtil = jwtTokenUtil;
    this.userDetailsService = userDetailsService;
    this.handlerExceptionResolver = handlerExceptionResolver;
  }
  @Override
  protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
      throws ServletException, IOException  {
    logger.info("Entering doFilterInternal");
    String token = getTokenFromRequest(request);
    System.out.println("Token-- " + token);
    try {
      
      if (token != null && jwtTokenUtil.validateJwtToken(token)) {
        String username = jwtTokenUtil.getUserNameFromJwtToken(token);
        //System.out.println("User Name--JwtTokenFilter-- " + username);
        UserDetails userDetails = userDetailsService.loadUserByUsername(username);
        //System.out.println("Authorities--JwtTokenFilter-- " + userDetails.getAuthorities());
        UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
            userDetails, null, userDetails.getAuthorities());
        authentication.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));

        SecurityContextHolder.getContext().setAuthentication(authentication);
        
      }
    }catch(JwtException ex) {
      // resolve exception so that it can be handled by GlobalExceptionHandler
      handlerExceptionResolver.resolveException(request, response, null, ex);
    }
    logger.info("Exiting doFilterInternal");
    filterChain.doFilter(request, response);
  }
  
  private String getTokenFromRequest(HttpServletRequest request) {
    String token = request.getHeader("Authorization");
    System.out.println("Token from Request-- " + token);
    if (StringUtils.hasText(token) && token.startsWith("Bearer ")) {
      // remove "Bearer "
      return token.substring(7, token.length());
    }
    return null;
  }
}

src/main/resources/application.properties

Properties to configure Spring Datasource, JPA and also JWT. Please update it as per your credentials.

spring.datasource.url=jdbc:mysql://localhost:3306/netjs
spring.datasource.username=root
spring.datasource.password=admin

spring.jpa.properties.hibernate.showsql=true

jwttoken.secret=c221abc194aca0629e8bafb0cea2f4acffeb58071d65cf2c0894651ba6202eeb
# in milliseconds (5 mins)
jwttoken.expiration=300000

Controller classes

AuthController.java

This controller class has two methods-

  • userSignup() which is a handler method for any POST request to /auth/signup path.
  • userLogin() which is a handler method for any POST request to /auth/login path.
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import com.netjstech.dto.AuthResponseDto;
import com.netjstech.dto.SignupRequestDto;
import com.netjstech.entity.User;
import com.netjstech.service.AuthService;
import com.netjstech.service.UserService;


@RestController
@RequestMapping("/auth")
public class AuthController {
  private static final Logger logger = LoggerFactory.getLogger(AuthController.class);
  private final UserService userService;
  private final AuthService authService;
  public AuthController(UserService userService, AuthService authService){
    this.userService = userService;
    this.authService = authService;
  }
  
  @PostMapping("/login")
  public ResponseEntity<?> userLogin(@Validated @RequestBody User user) {
    System.out.println("AuthController111 -- userLogin");
    AuthResponseDto authResponseDto = authService.userLogin(user);
    return ResponseEntity.ok(authResponseDto);
  }
  
  @PostMapping("/signup")
  public ResponseEntity<String> userSignup(@Validated @RequestBody SignupRequestDto signupRequestDto) {
    System.out.println("in signup");
    try {
      if(userService.isUserExists(signupRequestDto.getUserName())){
        return ResponseEntity.badRequest().body("Username is already taken");
      }
      if(userService.isEmailTaken(signupRequestDto.getEmail())){
        return ResponseEntity.badRequest().body("Email is already taken");
      }
      authService.userSignup(signupRequestDto);
      return ResponseEntity.ok("User signed up successfully");
    }catch(Exception e) {
      logger.error("Failed to register User " + e.getMessage());
      return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Failed to register User");
      }
      
  }
}

In the userSignup() method first thing is to verify that the passed user name or email are not in use already. Then create a User object using the values passed in SignupRequest object. Extract the roles and set those also in the User object and then save the User. If everything works fine then set the status code as ok in the response with the message.

In the userLogin() method authenticate the user and set the authentication object in SecurityContext. Then generate the token and get the list of user roles. Set both token and list of roles in the response that is sent back.

UserController.java

This controller just demonstrates the use of authorization by access controlling the methods using @PreAuthorize annotation.

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user")
public class UserController {
	@GetMapping("/allusers")
	public String displayUsers() {
		return "Display All Users";
	}
	
	@GetMapping("/displayuser")
	@PreAuthorize("hasRole('ROLE_USER') or hasRole('ROLE_ADMIN')")
	public String displayToUser() {
		return "Display to both user and admin";
	}
	
	@GetMapping("/displayadmin")
	@PreAuthorize("hasRole('ROLE_ADMIN')")
	public String displayToAdmin() {
		return "Display only to admin";
	}
}

As evident from the annotations-

  • displayUsers() method can be accessed by all.
  • displayToUser() method can be accessed by user having role user or admin.
  • displayToAdmin() method can be accessed by user having role admin.

Service Class

Within Spring security there is an interface org.springframework.security.core.userdetails.UserDetailsService that has a method loadUserByUsername(java.lang.String username) to load user-specific data.

We’ll provide a concrete implementation of this interface where the loadUserByUsername() method implementation acts as a wrapper over the userRepository.findByUserName() method call. Returned user is passed to the CustomUserBean.createInstance() method to create instance of CustomUserBean as per our implementation.

import javax.transaction.Transactional;
import org.springframework.beans.factory.annotation.Autowired;
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.netjstech.dao.UserRepository;
import com.netjstech.model.CustomUserBean;
import com.netjstech.model.User;

@Service
public class UserDetailsServiceImpl implements UserDetailsService {
  @Autowired
  UserRepository userRepository;
  @Override
  @Transactional
  public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    User user = userRepository.findByUserName(username)
                    .orElseThrow(() -> new UsernameNotFoundException("User with "
                        + "user name "+ username + " not found"));
    return CustomUserBean.createInstance(user);
  }
}

UserService Interface

import com.netjstech.entity.User;

public interface UserService {
	boolean isUserExists(String userName);
	boolean isEmailTaken(String email);
	User save(User user);
	
}

UserServiceImpl Class

import org.springframework.stereotype.Service;
import com.netjstech.dao.UserRepository;
import com.netjstech.entity.User;

@Service
public class UserServiceImpl implements UserService {
	private final UserRepository userRepository;
	UserServiceImpl(UserRepository userRepository){
		this.userRepository = userRepository;
	}
	@Override
	public boolean isUserExists(String userName) {
		return userRepository.existsByUserName(userName);
	}

	@Override
	public boolean isEmailTaken(String email) {
		// TODO Auto-generated method stub
		return userRepository.existsByEmail(email);
	}
	@Override
	public User save(User user) {
		// TODO Auto-generated method stub
		return userRepository.save(user);
	}

}

AuthService Interface

import com.netjstech.dto.AuthResponseDto;
import com.netjstech.dto.SignupRequestDto;
import com.netjstech.entity.User;

public interface AuthService {
	void userSignup(SignupRequestDto signupRequestDto);
	AuthResponseDto userLogin(User user);
}
 

AuthServiceImpl Class

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

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import com.netjstech.dao.RoleRepository;
import com.netjstech.dto.AuthResponseDto;
import com.netjstech.dto.SignupRequestDto;
import com.netjstech.entity.CustomUserBean;
import com.netjstech.entity.Role;
import com.netjstech.entity.Roles;
import com.netjstech.entity.User;
import com.netjstech.exception.RoleNotFoundException;
import com.netjstech.security.JwtTokenUtil;

@Service
public class AuthServiceImpl implements AuthService {
  private static final Logger logger = LoggerFactory.getLogger(AuthServiceImpl.class);
  private final RoleRepository roleRepository;
  private final UserService userService;
  private final PasswordEncoder encoder;
  private final AuthenticationManager authenticationManager;
  private final JwtTokenUtil jwtTokenUtil;
  public AuthServiceImpl(RoleRepository roleRepository, UserService userService, 
      PasswordEncoder encoder, AuthenticationManager authenticationManager, JwtTokenUtil jwtTokenUtil){
    this.roleRepository = roleRepository;
    this.userService = userService;
    this.encoder = encoder;
    this.authenticationManager = authenticationManager;
    this.jwtTokenUtil = jwtTokenUtil;
  }
  @Override
  public void userSignup(SignupRequestDto signupRequestDto) {
    logger.info("Entering userSignup");
    User user = new User();
    Set<Role> roles = new HashSet<>();
    user.setUserName(signupRequestDto.getUserName());
    user.setEmail(signupRequestDto.getEmail());
    user.setPassword(encoder.encode(signupRequestDto.getPassword()));
    //System.out.println("Encoded password--- " + user.getPassword());
    String[] roleArr = signupRequestDto.getRoles();
    
    // give User role by default
    if(roleArr == null) {
      roles.add(roleRepository.findByRoleName(Roles.ROLE_USER).get());
    }
    // 
    for(String role: roleArr) {
      switch(role.toLowerCase()) {
        case "admin":
          roles.add(roleRepository.findByRoleName(Roles.ROLE_ADMIN).get());
          break;
        case "user":
          roles.add(roleRepository.findByRoleName(Roles.ROLE_USER).get());
          break;  
        default:
          throw new RoleNotFoundException("Specified role not found");
      }
    }
    user.setRoles(roles);
    userService.save(user);
    logger.info("Exiting userSignup");
  }

  @Override
  public AuthResponseDto userLogin(User user) {
    Authentication authentication = authenticationManager.authenticate(
        new UsernamePasswordAuthenticationToken(user.getUserName(), user.getPassword()));
  
    CustomUserBean userBean = (CustomUserBean) authentication.getPrincipal();
    String token = jwtTokenUtil.generateJwtToken(userBean.getUsername());
    List<String> roles = userBean.getAuthorities().stream()
                   .map(GrantedAuthority::getAuthority)
                   .collect(Collectors.toList());
    AuthResponseDto authResponseDto = new AuthResponseDto();
    authResponseDto.setToken(token);
    authResponseDto.setRoles(roles);
    return authResponseDto;
    
  }
}

Exceptions

There are some custom exceptions.

AuthException.java

public class AuthException extends RuntimeException {
	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	public AuthException(String message) {
		super(message);
  }

}
 

RoleNotFoundException.java

public class RoleNotFoundException  extends RuntimeException {

	/**
	 * 
	 */
	private static final long serialVersionUID = 1L;

	public RoleNotFoundException(String message) {
		super(message);
  }

}

Global Exception Handler

A global exception handler class is setup to catch exceptions across application.

GlobalExcpetionHandler.java

import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import com.netjstech.dto.ErrorResponseDto;
import io.jsonwebtoken.JwtException;

@RestControllerAdvice
public class GlobalExcpetionHandler {
  @ExceptionHandler(value = AuthException.class)
  public ResponseEntity<ErrorResponseDto> handleAuthException(AuthException ex) {
    //System.out.println("In global error");
    String message = "Error while processing request";
    ErrorResponseDto errorResponse = new ErrorResponseDto(HttpStatus.CREATED, message, ex.getMessage());
    return new ResponseEntity<ErrorResponseDto>(errorResponse, HttpStatus.FORBIDDEN);
  }
  
  @ExceptionHandler(value = UsernameNotFoundException.class)
  public ResponseEntity<ErrorResponseDto> handleUserNotFoundException(UsernameNotFoundException ex) {
    //System.out.println("In global error - UsernameNotFoundException");
    String message = "User name not found";
    ErrorResponseDto errorResponse = new ErrorResponseDto(HttpStatus.UNAUTHORIZED, message, ex.getMessage());
    return new ResponseEntity<ErrorResponseDto>(errorResponse, HttpStatus.FORBIDDEN);
  }
  
  // This is thrown from UsernamePasswordAuthenticationFilter when attempting Authentication
  // and credentials are wrong
  @ExceptionHandler(value = AuthenticationException.class)
  public ResponseEntity<ErrorResponseDto> handleAuthenticationException(AuthenticationException ex) {
    //System.out.println("In global error - handleAuthenticationException");
    String message = "User name or password is wrong";
    ErrorResponseDto errorResponse = new ErrorResponseDto(HttpStatus.UNAUTHORIZED, message, ex.getMessage());
    return new ResponseEntity<ErrorResponseDto>(errorResponse, HttpStatus.UNAUTHORIZED);
  }
  
  //When using @PreAuthorize in Spring Boot with Spring Security, if the specified authorization expression evaluates 
  //to false, an AccessDeniedException is thrown
  @ExceptionHandler(AccessDeniedException.class)
  public ResponseEntity<ErrorResponseDto> handleAccessDeniedException(AccessDeniedException ex) {
    String message = "Access Denied: You do not have permission to access this resource";
    ErrorResponseDto errorResponse = new ErrorResponseDto(HttpStatus.FORBIDDEN, message, ex.getMessage());
    return new ResponseEntity<ErrorResponseDto>(errorResponse, HttpStatus.FORBIDDEN);
  }
    
  @ExceptionHandler({JwtException.class})
  public ResponseEntity<ErrorResponseDto> handleTokenException(Exception ex) {
    String message = "Token has a problem";
    ErrorResponseDto errorResponse = new ErrorResponseDto(HttpStatus.UNAUTHORIZED, message, ex.getMessage());
    return new ResponseEntity<ErrorResponseDto>(errorResponse, HttpStatus.UNAUTHORIZED);
  }
  
  @ExceptionHandler({Exception.class})
  public ResponseEntity<ErrorResponseDto> handleException(Exception ex) {
    String message = "Error while processing request";
    ErrorResponseDto errorResponse = new ErrorResponseDto(HttpStatus.INTERNAL_SERVER_ERROR, message, ex.getMessage());
    return new ResponseEntity<ErrorResponseDto>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
  }
}
 

Application class

Class with main method to run the application.

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class SpringsecurityApplication {

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

Testing the application

You can run the above class with main method as a Java application that would set up the Spring Boot application and start the web server to listen on a given port.

Registering user by sending POST request to signup.

Spring Boot token authentication example

In the DB you can check the USER table to verify that user data is inserted.

Also the mapped roles.

With the registered user you can login. As a response you’ll get the generated token you need to send that token with other requests.

Spring Boot login example

Display all users, this method displayUsers() is permitted to all as configured in SecurityConfig class. With this URL- http://localhost:8080/user/allusers user should be able to access it even when not logged in.

displayToUser() method need authentication and also authorization that the logged user should have role USER or ADMIN. You need to send the token which is returned after login so go to Authorization tab select “Bearer Token” in the Type dropdown and add the token.

Spring Boot authorization

displayToAdmin() method need authentication and also authorization that the logged user should have role ADMIN. Trying to access this method with the user having only USER role authorization results in “Forbidden” error.

Spring Boot roles

Trying to access after token is expired

JWT expired token

Source code from GitHub- https://github.com/netjs/spring_code/tree/main/Spring/Spring_Security

That's all for this topic Spring Boot + Spring Security JWT Authentication Example. If you have any doubt or any suggestions to make please drop a comment. Thanks!

>>>Return to Spring Tutorial Page


Related Topics

  1. Angular + Spring Boot JWT Authentication Example
  2. Spring Boot REST API CRUD Example With Spring Data JPA
  3. Angular HttpClient + Spring Boot REST API CRUD Backend Service
  4. Spring Boot StandAlone (Console Based) Application Example
  5. Spring NamedParameterJdbcTemplate Insert, Update And Delete Example

You may also like-

  1. Spring MVC Pagination Example Using PagedListHolder
  2. Spring MVC Exception Handling - @ExceptionHandler And @ControllerAdvice Example
  3. Spring Transaction Management Example - @Transactional Annotation and JDBC
  4. Connection Pooling With Apache DBCP Spring Example
  5. Angular HttpClient to Communicate With Backend Service
  6. Exception Handling in Java Lambda Expressions
  7. Core Java Basics Interview Questions And Answers
  8. Variable Length Arguments (*args), Keyword Varargs (**kwargs) in Python

No comments:

Post a Comment