Spring Boot Microservices Security with JWT Authentication

In this tutorial, we will create a Spring Boot microservices architecture secured with JWT (JSON Web Tokens). We will use the latest versions of Spring Boot 3.2, Spring Cloud 2023, and jjwt. The architecture will include:
  1. Authentication Service: Manages user authentication and issues JWT tokens.
  2. Employee Service: Manages employee information and is secured with JWT.
  3. API Gateway: Routes requests and applies security checks.
Spring Boot Microservices Security with JWT Authentication

Prerequisites

Before we start, ensure you have the following:

  • Java Development Kit (JDK) installed
  • Apache Maven installed
  • An IDE (such as IntelliJ IDEA, Eclipse, or VS Code) installed

Overview of the Microservices

We will create three Spring Boot microservices:

  1. Authentication Service: Manages user authentication and issues JWT tokens.
  2. Employee Service: Manages employee information.
  3. API Gateway: Routes requests and handles security with JWT.

Step 1: Create the Authentication Service

1.1 Create a Spring Boot Project

  1. Open Spring Initializr:

  2. Configure Project Metadata:

    • Project: Maven Project
    • Language: Java
    • Spring Boot: Select the latest version of Spring Boot 3.2
    • Group: com.example
    • Artifact: auth-service
    • Name: auth-service
    • Description: Authentication Service
    • Package Name: com.example.authservice
    • Packaging: Jar
    • Java Version: 17 (or your preferred version)
    • Click Next.
  3. Select Dependencies:

    • On the Dependencies screen, select the dependencies you need:
      • Spring Web
      • Spring Security
      • Spring Data JPA
      • H2 Database
      • jjwt (add this manually later)
    • Click Next.
  4. Generate the Project:

    • Click Generate to download the project zip file.
    • Extract the zip file to your desired location.
  5. Open the Project in Your IDE:

    • Open your IDE and import the project as a Maven project.

1.2 Add jjwt Dependency

Add the latest version of jjwt to your pom.xml:

<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> 

1.3 Create User Entity

Create a User entity class in the com.example.authservice.model package:

package com.example.authservice.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; private String password; // Getters and Setters } 

1.4 Create User Repository

Create a UserRepository interface in the com.example.authservice.repository package:

package com.example.authservice.repository; import com.example.authservice.model.User; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface UserRepository extends JpaRepository<User, Long> { User findByUsername(String username); } 

1.5 Create User Service

Create a UserService class in the com.example.authservice.service package:

package com.example.authservice.service; import com.example.authservice.model.User; import com.example.authservice.repository.UserRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; @Service public class UserService implements UserDetailsService { private final UserRepository userRepository; private final BCryptPasswordEncoder passwordEncoder; @Autowired public UserService(UserRepository userRepository, BCryptPasswordEncoder passwordEncoder) { this.userRepository = userRepository; this.passwordEncoder = passwordEncoder; } public User saveUser(User user) { user.setPassword(passwordEncoder.encode(user.getPassword())); return userRepository.save(user); } public User findByUsername(String username) { return userRepository.findByUsername(username); } @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { User user = userRepository.findByUsername(username); if (user == null) { throw new UsernameNotFoundException("User not found"); } return org.springframework.security.core.userdetails.User.withUsername(user.getUsername()) .password(user.getPassword()) .roles(user.getRole()) .build(); } } 

1.6 Create Security Configuration

Create a SecurityConfig class in the com.example.authservice.config package:

package com.example.authservice.config; import com.example.authservice.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.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.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { private final UserService userService; @Autowired public SecurityConfig(UserService userService) { this.userService = userService; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .requestMatchers("/auth/**").permitAll() .anyRequest().authenticated() ); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } } 

1.7 Create JWT Utility Class

Create a JwtUtil class in the com.example.authservice.util package:

package com.example.authservice.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.Date; import java.util.function.Function; @Component public class JwtUtil { @Value("${jwt.secret}") private String secret; public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } public String generateToken(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public boolean validateToken(String token, String username) { final String extractedUsername = extractUsername(token); return (extractedUsername.equals(username) && !isTokenExpired(token)); } private boolean isTokenExpired(String token) { return extractClaim(token, Claims::getExpiration).before(new Date()); } } 

1.8 Create Authentication Controller

Create an AuthController class in the com.example.authservice.controller package:

package com.example.authservice.controller; import com.example.authservice.model.User; import com.example.authservice.service.UserService; import com.example.authservice.util.JwtUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/auth") public class AuthController { private final UserService userService; private final AuthenticationManager authenticationManager; private final JwtUtil jwtUtil; @Autowired public AuthController(UserService userService, AuthenticationManager authenticationManager, JwtUtil jwtUtil) { this.userService = userService; this.authenticationManager = authenticationManager; this.jwtUtil = jwtUtil; } @PostMapping("/register") public User registerUser(@RequestBody User user) { return userService.saveUser(user); } @PostMapping("/login") public String loginUser(@RequestBody User user) { Authentication authentication = authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(user.getUsername(), user.getPassword()) ); SecurityContextHolder.getContext().setAuthentication(authentication); UserDetails userDetails = userService.findByUsername(user.getUsername()); return jwtUtil.generateToken(userDetails.getUsername()); } } 

1.9 Configure JWT Secret

Add the following to your application.properties file:

jwt.secret=your_secret_key 

Replace your_secret_key with a secure key of your choice.

Step 2: Create the Employee Service

2.1 Create a Spring Boot Project

  1. Open Spring Initializr:

  2. Configure Project Metadata:

    • Project: Maven Project
    • Language: Java
    • Spring Boot: Select the latest version of Spring Boot 3.2
    • Group: com.example
    • Artifact: employee-service
    • Name: employee-service
    • Description: Employee Service
    • Package Name: com.example.employeeservice
    • Packaging: Jar
    • Java Version: 17 (or your preferred version)
    • Click Next.
  3. Select Dependencies:

    • On the Dependencies screen, select the dependencies you need:
      • Spring Web
      • Spring Data JPA
      • Spring Security
      • H2 Database
    • Click Next.
  4. Generate the Project:

    • Click Generate to download the project zip file.
    • Extract the zip file to your desired location.
  5. Open the Project in Your IDE:

    • Open your IDE and import the project as a Maven project.

2.2 Create Employee Entity

Create an Employee entity class in the com.example.employeeservice.model package:

package com.example.employeeservice.model; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; @Entity public class Employee { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String department; // Getters and Setters } 

2.3 Create Employee Repository

Create an EmployeeRepository interface in the com.example.employeeservice.repository package:

package com.example.employeeservice.repository; import com.example.employeeservice.model.Employee; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @Repository public interface EmployeeRepository extends JpaRepository<Employee, Long> { } 

2.4 Create Employee Controller

Create an EmployeeController class in the com.example.employeeservice.controller package:

package com.example.employeeservice.controller; import com.example.employeeservice.model.Employee; import com.example.employeeservice.repository.EmployeeRepository; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController @RequestMapping("/employees") public class EmployeeController { private final EmployeeRepository employeeRepository; @Autowired public EmployeeController(EmployeeRepository employeeRepository) { this.employeeRepository = employeeRepository; } @GetMapping public List<Employee> getAllEmployees() { return employeeRepository.findAll(); } @PostMapping public Employee createEmployee(@RequestBody Employee employee) { return employeeRepository.save(employee); } } 

2.5 Create Security Configuration

Create a SecurityConfig class in the com.example.employeeservice.config package:

package com.example.employeeservice.config; import com.example.employeeservice.filter.JwtRequestFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.authentication.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.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.SecurityFilterChain; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity public class SecurityConfig { private final JwtRequestFilter jwtRequestFilter; @Autowired public SecurityConfig(JwtRequestFilter jwtRequestFilter) { this.jwtRequestFilter = jwtRequestFilter; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(auth -> auth .anyRequest().authenticated() ) .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class); return http.build(); } @Bean public AuthenticationManager authenticationManager(AuthenticationConfiguration authenticationConfiguration) throws Exception { return authenticationConfiguration.getAuthenticationManager(); } } 

2.6 Create JWT Request Filter

Create a JwtRequestFilter class in the com.example.employeeservice.filter package:

package com.example.employeeservice.filter; import com.example.employeeservice.util.JwtUtil; import io.jsonwebtoken.ExpiredJwtException; 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.web.authentication.WebAuthenticationDetailsSource; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import java.io.IOException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; @Component public class JwtRequestFilter extends OncePerRequestFilter { @Autowired private JwtUtil jwtUtil; @Autowired private UserDetailsService userDetailsService; @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { final String requestTokenHeader = request.getHeader("Authorization"); String username = null; String jwtToken = null; if (requestTokenHeader != null && requestTokenHeader.startsWith("Bearer ")) { jwtToken = requestTokenHeader.substring(7); try { username = jwtUtil.extractUsername(jwtToken); } catch (IllegalArgumentException e) { System.out.println("Unable to get JWT Token"); } catch (ExpiredJwtException e) { System.out.println("JWT Token has expired"); } } else { logger.warn("JWT Token does not begin with Bearer String"); } if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = this.userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwtToken, userDetails.getUsername())) { UsernamePasswordAuthenticationToken usernamePasswordAuthenticationToken = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); usernamePasswordAuthenticationToken .setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(usernamePasswordAuthenticationToken); } } chain.doFilter(request, response); } } 

2.7 Create JWT Utility Class

Create a JwtUtil class in the com.example.employeeservice.util package:

package com.example.employeeservice.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import io.jsonwebtoken.SignatureAlgorithm; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.Date; import java.util.function.Function; @Component public class JwtUtil { @Value("${jwt.secret}") private String secret; public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } public String generateToken(String username) { return Jwts.builder() .setSubject(username) .setIssuedAt(new Date(System.currentTimeMillis())) .setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10)) .signWith(SignatureAlgorithm.HS256, secret) .compact(); } public boolean validateToken(String token, String username) { final String extractedUsername = extractUsername(token); return (extractedUsername.equals(username) && !isTokenExpired(token)); } private boolean isTokenExpired(String token) { return extractClaim(token, Claims::getExpiration).before(new Date()); } } 

2.8 Configure JWT Secret

Add the following to your application.properties file:

jwt.secret=your_secret_key 

Replace your_secret_key with the same key used in the auth-service.

Step 3: Create the API Gateway

3.1 Create a Spring Boot Project

  1. Open Spring Initializr:

  2. Configure Project Metadata:

    • Project: Maven Project
    • Language: Java
    • Spring Boot: Select the latest version of Spring Boot 3.2
    • Group: com.example
    • Artifact: api-gateway
    • Name: api-gateway
    • Description: API Gateway
    • Package Name: com.example.apigateway
    • Packaging: Jar
    • Java Version

: 17 (or your preferred version)

  • Click Next.
  1. Select Dependencies:

    • On the Dependencies screen, select the dependencies you need:
      • Spring Cloud Gateway
      • Spring Boot DevTools
      • Spring Security
      • jjwt (add this manually later)
    • Click Next.
  2. Generate the Project:

    • Click Generate to download the project zip file.
    • Extract the zip file to your desired location.
  3. Open the Project in Your IDE:

    • Open your IDE and import the project as a Maven project.

3.2 Add jjwt Dependency

Add the latest version of jjwt to your pom.xml:

<dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-api</artifactId> <version>0.11.5</version> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-impl</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> <dependency> <groupId>io.jsonwebtoken</groupId> <artifactId>jjwt-jackson</artifactId> <version>0.11.5</version> <scope>runtime</scope> </dependency> 

3.3 Update application.yml

Create an application.yml file in the src/main/resources directory and configure it as follows:

server: port: 8080 spring: application: name: api-gateway cloud: gateway: routes: - id: auth_service uri: http://localhost:8081 predicates: - Path=/auth/** - id: employee_service uri: http://localhost:8082 predicates: - Path=/employees/** security: jwt: secret: your_secret_key 

Replace your_secret_key with the same key used in the auth-service and employee-service.

3.4 Create JWT Utility Class

Create a JwtUtil class in the com.example.apigateway.util package:

package com.example.apigateway.util; import io.jsonwebtoken.Claims; import io.jsonwebtoken.Jwts; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; import java.util.function.Function; @Component public class JwtUtil { @Value("${spring.security.jwt.secret}") private String secret; public String extractUsername(String token) { return extractClaim(token, Claims::getSubject); } public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) { final Claims claims = extractAllClaims(token); return claimsResolver.apply(claims); } private Claims extractAllClaims(String token) { return Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody(); } public boolean validateToken(String token, String username) { final String extractedUsername = extractUsername(token); return (extractedUsername.equals(username) && !isTokenExpired(token)); } private boolean isTokenExpired(String token) { return extractClaim(token, Claims::getExpiration).before(new Date()); } } 

3.5 Create JWT Filter

Create a JwtAuthenticationFilter class in the com.example.apigateway.filter package:

package com.example.apigateway.filter; import com.example.apigateway.util.JwtUtil; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.stereotype.Component; import org.springframework.web.server.ServerWebExchange; import org.springframework.web.server.WebFilter; import org.springframework.web.server.WebFilterChain; import reactor.core.publisher.Mono; @Component public class JwtAuthenticationFilter implements WebFilter { @Autowired private JwtUtil jwtUtil; @Autowired private UserDetailsService userDetailsService; @Override public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) { ServerHttpRequest request = exchange.getRequest(); String authHeader = request.getHeaders().getFirst("Authorization"); if (authHeader != null && authHeader.startsWith("Bearer ")) { String jwt = authHeader.substring(7); String username = jwtUtil.extractUsername(jwt); if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) { UserDetails userDetails = userDetailsService.loadUserByUsername(username); if (jwtUtil.validateToken(jwt, userDetails.getUsername())) { UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken( userDetails, null, userDetails.getAuthorities()); SecurityContextHolder.getContext().setAuthentication(authentication); } } } return chain.filter(exchange); } } 

3.6 Create Security Configuration

Create a SecurityConfig class in the com.example.apigateway.config package:

package com.example.apigateway.config; import com.example.apigateway.filter.JwtAuthenticationFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebFluxSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.web.server.SecurityWebFilterChain; @Configuration @EnableWebFluxSecurity public class SecurityConfig { private final JwtAuthenticationFilter jwtAuthenticationFilter; @Autowired public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) { this.jwtAuthenticationFilter = jwtAuthenticationFilter; } @Bean public SecurityWebFilterChain securityWebFilterChain(HttpSecurity http) throws Exception { return http.csrf(AbstractHttpConfigurer::disable) .authorizeExchange(exchange -> exchange .pathMatchers("/auth/**").permitAll() .anyExchange().authenticated() ) .addFilterBefore(jwtAuthenticationFilter, SecurityWebFilterChain.class) .build(); } } 

Step 4: Running the Microservices

4.1 Running Authentication Service

  1. Open the AuthServiceApplication class in the src/main/java/com/example/authservice directory.
  2. Click the green Run button in your IDE or use the terminal to run the application:
    ./mvnw spring-boot:run 

4.2 Running Employee Service

  1. Open the EmployeeServiceApplication class in the src/main/java/com/example/employeeservice directory.
  2. Click the green Run button in your IDE or use the terminal to run the application:
    ./mvnw spring-boot:run 

4.3 Running API Gateway

  1. Open the ApiGatewayApplication class in the src/main/java/com/example/apigateway directory.
  2. Click the green Run button in your IDE or use the terminal to run the application:
    ./mvnw spring-boot:run 

Step 5: Testing the Application

You can use tools like Postman to test the authentication and secure access to the microservices.

5.1 Register a User

  1. Create a new POST request to http://localhost:8081/auth/register.

  2. Set the body to JSON and add a user object:

    { "username": "testuser", "password": "testpassword" } 
  3. Send the request.

5.2 Login to Get JWT Token

  1. Create a new POST request to http://localhost:8081/auth/login.

  2. Set the body to JSON and add the user object:

    { "username": "testuser", "password": "testpassword" } 
  3. Send the request and copy the token from the response.

5.3 Access Secured Employee Service

  1. Create a new GET request to http://localhost:8080/employees.
  2. Add the Authorization header with the value Bearer <JWT_TOKEN>.
  3. Send the request and verify that you receive the list of employees.

Conclusion

In this tutorial, we created a microservices architecture with Spring Boot and secured it using JWT. We built an authentication service to issue tokens, secured the employee service with JWT, and created an API Gateway to route and secure requests. This setup provides a solid foundation for developing secure microservices architectures.


Comments

  1. in employee service as we not defined the UserDetailsService.loadUserByUsername(), it is asking for the definition. How to resolve this?

    ReplyDelete
    Replies
    1. Fixed the issue. Check out the UserService class.

      Delete

Post a Comment