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
- SpringBoot 3.5.x
- Java 21
- MySql 8.x
- Spring Security 6.x
- Hibernate 7.x
- 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-
- Generate the token by setting user name, issued time, expiration time.
- Validate token
- 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.
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.
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.
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.
Trying to access after token is expired
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
You may also like-
No comments:
Post a Comment