Introduction
One of the most popular and effective authentication methods in modern web applications is JSON Web Tokens (JWT). It provides a flexible and stateless way to verify users’ identities and secure API endpoints; it is also called Token-Based Authentication.
With the rise of microservices and stateless applications, securing web applications using traditional session-based authentication mechanisms has become less practical. JSON Web Tokens (JWT) offer a modern approach to secure APIs by providing a stateless authentication mechanism. In this blog, we will explore how to secure a Spring Boot application using Spring Security 6 and JWT.
Prerequisites
- Basic knowledge of Java and Spring Boot.
- Spring Boot and Java installed
- JDK 11 or above
Internal Working of Spring Security with JWT Token
When we add the Spring Security dependency to our Spring Boot application, it enables the security filter chain. These filters play a crucial role in securing the application. Here’s a detailed explanation of how Spring Security works internally with JWT tokens for authentication and authorization:
1. Security Filter Chain Initialization
- When the application starts, Spring Security initializes a chain of filters. These filters intercept every incoming HTTP request to the application.
2. Authentication Process
- When a client sends a request to a secured endpoint, the request first hits the security filter chain.
- One of the filters in this chain is the JwtRequestFilter, which is a custom filter we configure to handle JWT tokens.
3. JwtRequestFilter Execution
- The JwtRequestFilter intercepts the request and looks for an Authorization header.
- If the header is present and starts with “Bearer “, the filter extracts the JWT token from the header.
4. Token Validation
- The extracted token is then validated. The JwtUtil class is used to:
- Parse the token.
- Validate the token by checking its signature and expiration date.
- Extract the username and other claims from the token.
5. User Authentication
- If the token is valid, the JwtRequestFilter retrieves the user details using a custom UserDetailsService.
- A UsernamePasswordAuthenticationToken is created using the retrieved user details.
- This authentication token is then set in the SecurityContextHolder, making the user authenticated for the current request.
6. Authorization Process
- After authentication, Spring Security proceeds to the authorization phase.
- The security configuration (defined in SecurityConfig) checks if the authenticated user has the necessary permissions to access the requested resource.
- If the user is authorized, the request is passed to the controller.
- If the user is not authorized, a 403 Forbidden response is returned.
In this blog post, we’ll walk through the process of implementing spring security 6 with JWT token.
Set Up Spring Boot Application
Create a Spring Boot Project
You can create a new Spring Boot project using Spring Initializr (https://start.spring.io/). Add the following dependencies:
- Spring Web
- Spring Security
- Spring Boot OAuth2 Client
- Spring Boot Starter Data JPA
- Spring Boot Starter Validation
- jjwt (JWT library)
Step 1 : Configure JWT Utility Class
Create a utility class to handle JWT creation and validation.
package com.surprise.surprise.auth.security;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.function.Function;
import lombok.extern.slf4j.Slf4j;
importorg.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class JwtUtil {
private final String SECRET_KEY =
"KJJh3sbOa5r1nFdL7WbL1k5s95kR9J3YXK9zV4HJk9Y2";
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}
public T extractClaim(String token, Function claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}
private Claims extractAllClaims(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY)
.parseClaimsJws(token)
.getBody();
}
public Boolean isTokenExpired(String token) {
try {
return extractExpiration(token).before(new Date());
} catch (ExpiredJwtException ex) {
log.info("Token is expired");
return true;
}
}
public String generateToken(UserDetails userDetails) {
Map claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}
private String createToken(Map claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (
username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}
}
Step 2 : Create User and UserDetailsService
Create a user model and a custom UserDetailsService to load user-specific data.
User Model
package com.surprise.surprise.auth.model;
import jakarta.persistence.*;
@Entity
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "auth_id")
private long id;
@Column(name = "name") private String username;
@Column(name = "password") private String password;
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;
}
}
Custom UserDetailsService
package com.surprise.surprise.auth.security;
import com.surprise.surprise.auth.common.CustomException;
import com.surprise.surprise.user.model.User;
import com.surprise.surprise.user.repository.UserRepo;
import java.util.ArrayList;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;
@Service
public class CustomUserDetailsService implements UserDetailsService {
@Autowired private UserRepo authRepo;
public UserDetails loadUserByUsername(String username)
throws CustomException {
User user = authRepo.findByUserName(username);
if (user == null) {
throw new CustomException("User name is invalid");
}
return new org.springframework.security.core.userdetails.User(
user.getUserName(), user.getPassword(), new ArrayList<>());
}
}
Step 3 : Configure Security
Configure Spring Security to use JWT for authentication.
SecurityConfig
package com.surprise.surprise.auth.security;
import java.util.List;
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.authentication.dao.DaoAuthenticationProvider;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.CorsConfigurationSource;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
@Configuration
@EnableWebSecurity
public class SecurityConfig {
@Autowired private CustomUserDetailsService customUserDetailsService;
@Autowired private RequestFilter jwtFilter;
@Bean
public static BCryptPasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
@Bean
public DaoAuthenticationProvider authenticationProvider() {
DaoAuthenticationProvider authProvider = new DaoAuthenticationProvider();
authProvider.setUserDetailsService(customUserDetailsService);
authProvider.setPasswordEncoder(passwordEncoder());
return authProvider;
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf(csrf -> csrf.disable())
.sessionManagement(session
-> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth
-> auth.requestMatchers("/login", "/add-admin", "/add-mobile-user")
.permitAll()
.anyRequest()
.authenticated());
http.cors(Customizer.withDefaults());
http.authenticationProvider(authenticationProvider());
http.addFilterBefore(
this.jwtFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(
AuthenticationConfiguration authConfig) throws Exception {
return authConfig.getAuthenticationManager();
}
@Bean
CorsConfigurationSource corsConfigurationSource() {
CorsConfiguration configuraton = new CorsConfiguration();
configuration.setAllowedOrigins(List.of("http://localhost:3000",
"https://surprises.world")); // allows React to access the API from
// origin on port 3000. Change accordingly
configuration.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE"));
configuration.setAllowCredentials(true);
configuration.addAllowedHeader("*");
UrlBasedCorsConfigurationSource source =
new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", configuration);
return source;
}
}
Request Filter
package com.surprise.surprise.auth.security;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.surprise.surprise.auth.common.Response;
import com.surprise.surprise.hotelInformation.model.Hotel;
import com.surprise.surprise.hotelInformation.repository.HotelRepo;
import com.surprise.surprise.user.model.User;
import com.surprise.surprise.user.repository.UserRepo;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
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.web.filter.OncePerRequestFilter;
@Slf4j
@Component
public class RequestFilter extends OncePerRequestFilter {
@Autowired CustomUserDetailsService customUserDetailsService;
@Autowired private JwtUtil jwtUtil;
@Autowired private UserRepo userRepo;
@Autowired private HotelRepo hotelRepo;
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String requestTokenHeader = request.getHeader("Authorization");
String username = null;
String jwtToken = null;
if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")
&& !requestTokenHeader.trim().equals("Bearer null")) {
jwtToken = requestTokenHeader.substring(7);
Boolean isExpired = this.jwtUtil.isTokenExpired(jwtToken);
if (isExpired) {
Response customResponse = new Response("Token expired", false);
String jsonResponse =
new ObjectMapper().writeValueAsString(customResponse);
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
response.getWriter().write(jsonResponse);
return;
}
try {
username = this.jwtUtil.extractUsername(jwtToken);
User user = userRepo.findByUserName(username);
if (user.getHotelId().getDisable()) {
Response customResponse = new Response("User is Disable", false);
String jsonResponse =
new ObjectMapper().writeValueAsString(customResponse);
response.setStatus(HttpServletResponse.SC_MOVED_PERMANENTLY);
response.getWriter().write(jsonResponse);
return;
}
} catch (Exception e) {
e.printStackTrace();
}
UserDetails userDetails =
this.customUserDetailsService.loadUserByUsername(username);
if (username != null
&& SecurityContextHolder.getContext().getAuthentication() == null) {
UsernamePasswordAuthenticationToken
usernamePasswordAuthenticationToken =
new UsernamePasswordAuthenticationToken(
userDetails, null, userDetails.getAuthorities());
usernamePasswordAuthenticationToken.setDetails(
new WebAuthenticationDetailsSource().buildDetails(request));
SecurityContextHolder.getContext().setAuthentication(
usernamePasswordAuthenticationToken);
} else {
log.info("token not validate");
}
}
filterChain.doFilter(request, response);
}
}
Step 3 : define service for login.
package com.surprise.surprise.auth.impl;
import com.surprise.surprise.auth.common.LoginResponse;
import com.surprise.surprise.auth.model.Auth;
import com.surprise.surprise.auth.repository.AuthRepo;
import com.surprise.surprise.auth.security.CustomUserDetailsService;
import com.surprise.surprise.auth.security.JwtUtil;
import com.surprise.surprise.auth.services.AuthServices;
import com.surprise.surprise.hotelInformation.model.Hotel;
import com.surprise.surprise.user.model.Role;
import com.surprise.surprise.user.model.User;
import com.surprise.surprise.user.repository.RoleRepo;
import com.surprise.surprise.user.repository.UserRepo;
import io.micrometer.core.annotation.Timed;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
@Service
@Slf4j
public class AuthServiceImpl implements AuthServices {
@Autowired BCryptPasswordEncoder passwordEncoder;
@Autowired AuthenticationManager authenticationManager;
@Autowired JwtUtil jwtUtil;
@Autowired RoleRepo roleRepo;
@Autowired CustomUserDetailsService userDetailsService;
@Autowired private AuthRepo authRepo;
@Autowired private UserRepo userRepo;
@Override
@Timed(value = "login.service.execution.time",
description = "Time taken to execute logIn service")
public ResponseEntity
logIn(Auth auth) {
long startTime = System.currentTimeMillis();
try {
User user = userRepo.findByUserName(auth.getUsername());
authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(
auth.getUsername(), auth.getPassword()));
final UserDetails userDetails =
userDetailsService.loadUserByUsername(auth.getUsername());
final String token = jwtUtil.generateToken(userDetails);
long userId = user.getId();
return ResponseEntity.ok(new LoginResponse(
token, "login successfully", userId, Boolean.TRUE, isAdmin));
catch (Exception e) {
return new ResponseEntity<>(
new LoginResponse(e.getMessage(), Boolean.FALSE),
HttpStatus.BAD_REQUEST);
}
}
}
Step 4 : make controller and declare endpoint.
@RestController
@CrossOrigin(origins = "*")
public class AuthController {
@Autowired private AuthServices authServices;
@Timed(value = "login.api.execution.time",
description = "Time taken to execute login API")
@PostMapping("/login")
public ResponseEntity
login(@RequestBody Auth auth) {
return authServices.logIn(auth);
}
}
Step 5 : Your main application class
package com.surprise.surprise;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootApplication
public class SurpriseApplication {
public static void main(String[] args) {
SpringApplication.run(SurpriseApplication.class, args);
}
}
Run your Spring Boot application. Navigate to http://localhost:8080/login in postman. You have to add your username and password in your request body and you get your token and this token used in another api in the authorization header to authenticate all the api.
Request body:
{
"username":"test",
"password":"test"
}
Response:
{
"token": "eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJBZG1pbiIsImV4cCI6MTcyMDYwMzU5OCwiaWF0IjoxNzIwNTE3MTk4fQ.4HzlljTYWLTUkujTIAX6fCiSVXyh8NRKSoxKsVtdeN0",
"message": "login successfully",
"userId": 1,
"status": true,
"hotelId": 0,
"admin": true
}