Show List

Sample Cloud Application Using Spring Cloud, API Gateway, DynamoDB, JWT Authentication, RabbitMQ - Part4

This is the fourth part of Food Delivery demonstration Application is an application. In this part we will be focusing on creating Spring Batch application to load the initial data, creating service discovery, API gateway and setting up the central authentication and authorization.

Spring Batch application for batch data load

Here is the module structure of this application:

Here are the key classes and configuration:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>searchRestaurant</artifactId>
<groupId>com.food</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>batchLoad</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.batch</groupId>
<artifactId>spring-batch-core</artifactId>
<version>4.3.7</version>
</dependency>

<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-batch</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>

<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.11.4</version>
</dependency>
<dependency>
<groupId>org.springframework.amqp</groupId>
<artifactId>spring-rabbit</artifactId>
<version>2.4.7</version>
</dependency>
</dependencies>
</project>
BatchConf.java
  • The JobBuilderFactory and StepBuilderFactory are autowired, indicating that they will be used for creating batch jobs and steps.
  • The demoJob method is annotated with @Bean and defines the main batch job. It uses the JobBuilderFactory to create a job named "demoJob". It also specifies a RunIdIncrementer to generate unique job run IDs. The job starts with stepOne.
  • The stepOne method is annotated with @Bean and defines the first step in the batch job. It uses the StepBuilderFactory to create a step named "stepOne". It specifies a tasklet (myTaskOne) that executes the data loading task defined earlier.
package dataload.config;

import dataload.repository.RestaurantRepository;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.Job;
import org.springframework.batch.core.Step;
import org.springframework.batch.core.configuration.annotation.JobBuilderFactory;
import org.springframework.batch.core.configuration.annotation.StepBuilderFactory;
import org.springframework.batch.core.launch.support.RunIdIncrementer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class BatchConf {

// Logger for logging purposes
private static final Logger LOGGER = LoggerFactory.getLogger(BatchConf.class);

// Repository for accessing restaurant data
@Autowired
RestaurantRepository restaurantRepository;

// Bean definition for the task that loads data
@Autowired
public LoadData myTaskOne(RestaurantRepository restaurantRepository) {
return new LoadData(restaurantRepository);
}

// Job builder factory for creating batch jobs
@Autowired
private JobBuilderFactory jobs;

// Step builder factory for creating batch steps
@Autowired
private StepBuilderFactory steps;

// Bean definition for the main batch job
@Bean
public Job demoJob() {
return jobs.get("demoJob")
.incrementer(new RunIdIncrementer()) // Incrementer for generating unique job run IDs
.start(stepOne()) // Starting point of the job: Step "stepOne"
.build();
}

// Bean definition for the first step in the batch job
@Bean
public Step stepOne() {
return steps.get("stepOne")
.tasklet(myTaskOne(restaurantRepository)) // Tasklet that executes the data loading task
.build();
}

}
LoadData.java
  • The LoadData class implements the Tasklet interface, which requires implementing the execute method. This method contains the logic that will be executed when the tasklet is run.
  • The execute method takes two parameters: StepContribution and ChunkContext. These parameters provide information about the step and chunk being processed.
  • It creates an instance of ObjectMapper, which is a Jackson library class used for JSON deserialization.
  • It defines a TypeReference<List<Restaurant>> to preserve the generic type information during deserialization. This is necessary because the readValue method of ObjectMapper cannot directly deserialize to a generic list.
  • It reads a JSON data file named "data.json" as an InputStream.
  • Inside a try-catch block, it uses the ObjectMapper to deserialize the JSON data from the InputStream into a list of Restaurant objects.
  • It saves each Restaurant object to the RestaurantRepository using the saveRestaurant method.
package dataload.config;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import dataload.model.Restaurant;
import dataload.repository.RestaurantRepository;
import lombok.AllArgsConstructor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.batch.core.StepContribution;
import org.springframework.batch.core.scope.context.ChunkContext;
import org.springframework.batch.core.step.tasklet.Tasklet;
import org.springframework.batch.repeat.RepeatStatus;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.io.IOException;
import java.io.InputStream;
import java.util.List;

@Component
public class LoadData implements Tasklet {

private static final Logger LOGGER = LoggerFactory.getLogger(LoadData.class);

private RestaurantRepository restaurantRepository;

// Constructor-based dependency injection for the RestaurantRepository
public LoadData(RestaurantRepository restaurantRepository) {
this.restaurantRepository = restaurantRepository;
}

// Method that gets executed when the tasklet is run
public RepeatStatus execute(StepContribution contribution, ChunkContext chunkContext) throws Exception {
LOGGER.info("LoadData start..");

// ObjectMapper for JSON deserialization
ObjectMapper mapper = new ObjectMapper();

// TypeReference to preserve generic type information during deserialization
TypeReference<List<Restaurant>> typeReference = new TypeReference<List<Restaurant>>(){};

// Read the JSON data file as an InputStream
InputStream inputStream = TypeReference.class.getResourceAsStream("/data.json");

try {
// Deserialize the JSON data into a list of Restaurant objects
List<Restaurant> restaurants = mapper.readValue(inputStream, typeReference);

// Save each restaurant to the repository
restaurants.forEach(restaurant -> restaurantRepository.saveRestaurant(restaurant));

LOGGER.info("Data Saved!");
} catch (IOException e) {
LOGGER.info("Unable to save data: " + e.getMessage());
}

LOGGER.info("LoadData done..");
return RepeatStatus.FINISHED;
}
}
FilePathUtils.java
  • Within the try block, the method obtains an InputStream by calling getClassLoader().getResourceAsStream(path) on the provided aClazz object. This retrieves the input stream for the resource located at the specified path using the class loader.
  • If the obtained stream is null, an IOException is thrown with the message "Stream is null".
  • If the stream is not null, the method uses IOUtils.toString(stream, Charset.defaultCharset()) to read the contents of the stream and convert them to a string. It uses the default character encoding of the system to decode the stream bytes into characters.
  • The method returns the string representation of the file content.
package dataload.util;

import io.micrometer.core.instrument.util.IOUtils;

import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.Charset;

public class FilePathUtils {
public static String readFileToString(String path, Class aClazz) throws IOException {

try (InputStream stream = aClazz.getClassLoader().getResourceAsStream(path)) {
if (stream == null) {
throw new IOException("Stream is null");
}
return IOUtils.toString(stream, Charset.defaultCharset());
}
}
}
Data.json
[
{"restaurantName": "Test-1",
"address": "Address-1",
"createdAt": "2023-06-01T19:19:37.371303500",
"updatedAt": "2023-06-01T19:19:37.371303500",
"menuList":{
"items":
[
{
"itemName": "Naan",
"ratings": "8",
"price": "111"
},
{
"itemName": "Pizza",
"ratings": "8",
"price": "110"
}
]

}
},
{"restaurantName": "Test-2",
"address": "Address-1",
"createdAt": "2023-06-01T19:19:37.371303500",
"updatedAt": "2023-06-01T19:19:37.371303500",
"menuList":{
"items":
[
{
"itemName": "Naan",
"ratings": "8",
"price": "111"
},
{
"itemName": "French Fries",
"ratings": "8",
"price": "110"
}
]

}
}
]

Service Discovery

Service discovery will be implemented using Eureka service registry. Each microservice will register itself with the registry, enabling other microservices to discover and communicate with it.
pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>searchRestaurant</artifactId>
<groupId>com.food</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>discovery</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-netflix-eureka-server</artifactId>
<version>3.1.4</version>
</dependency>

<!-- https://mvnrepository.com/artifact/javax.xml.bind/jaxb-api -->
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.0-b170201.1204</version>
</dependency>

<!-- https://mvnrepository.com/artifact/javax.activation/activation -->
<dependency>
<groupId>javax.activation</groupId>
<artifactId>activation</artifactId>
<version>1.1</version>
</dependency>

<!-- https://mvnrepository.com/artifact/org.glassfish.jaxb/jaxb-runtime -->
<dependency>
<groupId>org.glassfish.jaxb</groupId>
<artifactId>jaxb-runtime</artifactId>
<version>2.3.0-b170127.1453</version>
</dependency>
</dependencies>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>

</project>
EurekaServerApplication.java

The class is annotated with @EnableEurekaServer. This annotation enables the Eureka server functionality in the Spring Boot application. The Eureka server is a component of Netflix OSS that allows service registration and discovery in a microservices architecture.
package eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@EnableEurekaServer // Enables the Eureka server functionality
@SpringBootApplication // Indicates that this class is a Spring Boot application
public class EurekaServerApplication {
public static void main(String[] args) {
SpringApplication.run(EurekaServerApplication.class, args);
// Starts the Spring Boot application and initializes the Eureka server
}
}
application.yaml

  • fetch-registry property: Specifies whether the Eureka server registry should be fetched or not. In this case, it is set to false, which means the Eureka server will not fetch the registry from other Eureka servers. This setting is relevant if the Eureka server is part of a Eureka server cluster.
  • register-with-eureka property: Specifies whether the application should register itself with the Eureka server or not. In this case, it is set to false, which means the application will not register itself with the Eureka server. This setting is useful if the application is the Eureka server itself or if it does not need to register with the Eureka server for service discovery.
spring:
application:
name: eureka-discovery-server
# Specifies the name of the Spring Boot application as "eureka-discovery-server"

server:
port: 9000
# Configures the server port to 9000

eureka:
client:
fetch-registry: false
# Specifies whether the Eureka server registry should be fetched or not (set to false)

register-with-eureka: false
# Specifies whether the application should register itself with the Eureka server or not (set to false)

API Gateway

API Gateway exposes all REST endpoints. The API Gateway will handle authentication and request routing. It will serve as a single entry point for client applications to access the microservices.
Here are the key classes and configurations.

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>searchRestaurant</artifactId>
<groupId>com.food</groupId>
<version>0.0.1-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>gateway</artifactId>

<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.security</groupId>
<artifactId>spring-security-core</artifactId>
</dependency>
<dependency>
<groupId>org.apache.tomcat.embed</groupId>
<artifactId>tomcat-embed-core</artifactId>
</dependency>
<dependency>
<groupId>javax.xml.bind</groupId>
<artifactId>jaxb-api</artifactId>
<version>2.3.1</version>
</dependency>
</dependencies>
</project>
Model AppUser.java

This maps to the database table storing user information.
package apigateway.model;

import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBAttribute;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBHashKey;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBTable;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@DynamoDBTable(tableName = "user") // Specifies the name of the DynamoDB table
public class AppUser {

@DynamoDBHashKey // Marks the field as the hash key for the DynamoDB table
@DynamoDBAttribute // Indicates that this field is an attribute of the DynamoDB table
private String email; // Represents the email attribute in the DynamoDB table

@DynamoDBAttribute
private String name; // Represents the name attribute in the DynamoDB table

@DynamoDBAttribute
private String mobileNumber; // Represents the mobileNumber attribute in the DynamoDB table

@DynamoDBAttribute
private String password; // Represents the password attribute in the DynamoDB table

@DynamoDBAttribute
private String role; // Represents the role attribute in the DynamoDB table

}
DynamoDBConfig.java

The dynamoDBMapper() method internally calls the buildAmazonDynamoDB() method to obtain an instance of AmazonDynamoDB. The buildAmazonDynamoDB() method configures and builds the AmazonDynamoDB client. It uses the AmazonDynamoDBClientBuilder to construct the client and sets various configurations:
  • It sets the endpoint configuration using the provided dynamodbEndpoint and awsRegion.
  • It sets the AWS credentials using the provided dynamodbAccessKey and dynamodbSecretKey.
  • It returns the configured AmazonDynamoDB client.
package apigateway.config;

import com.amazonaws.auth.AWSStaticCredentialsProvider;
import com.amazonaws.auth.BasicAWSCredentials;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDB;
import com.amazonaws.services.dynamodbv2.AmazonDynamoDBClientBuilder;
import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DynamoDBConfig {

private static final Logger LOGGER = LoggerFactory.getLogger(DynamoDBConfig.class);

@Value("${amazon.dynamodb.endpoint}")
private String dynamodbEndpoint;

@Value("${amazon.aws.region}")
private String awsRegion;

@Value("${amazon.aws.accesskey}")
private String dynamodbAccessKey;

@Value("${amazon.aws.secretkey}")
private String dynamodbSecretKey;


/**
* Creates a bean for the DynamoDBMapper.
*
* @return The DynamoDBMapper object.
*/
@Bean
public DynamoDBMapper dynamoDBMapper() {
return new DynamoDBMapper(buildAmazonDynamoDB());
}

/**
* Builds and configures the AmazonDynamoDB client.
*
* @return The configured AmazonDynamoDB client.
*/
public AmazonDynamoDB buildAmazonDynamoDB() {
try{
return AmazonDynamoDBClientBuilder
.standard()
.withEndpointConfiguration(
new AwsClientBuilder.EndpointConfiguration(dynamodbEndpoint,awsRegion))
.withCredentials(new AWSStaticCredentialsProvider(
new BasicAWSCredentials(dynamodbAccessKey,dynamodbSecretKey)))
.build();
} catch (Exception e) {
LOGGER.error("Error occurred while building AmazonDynamoDB client", e);
throw e;
}
}
}
UserRepository.java
  • The saveUser() method saves an AppUser object to the DynamoDB table using the dynamoDBMapper.save() method. It returns the saved AppUser object.
  • The getUserByEmail() method retrieves a user from the DynamoDB table based on the email. It uses the dynamoDBMapper.load() method and returns the retrieved AppUser object.
  • The getDynamoDBMapper() method returns the DynamoDBMapper instance used by the repository.
  • The setDynamoDBMapper() method allows setting the DynamoDBMapper instance to be used by the repository.
package apigateway.repository;


import com.amazonaws.services.dynamodbv2.datamodeling.DynamoDBMapper;
import apigateway.model.AppUser;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class UserRepository {

@Autowired
private DynamoDBMapper dynamoDBMapper;

/**
* Saves a appUser to the DynamoDB table.
*
* @param appUser The AppUser object to be saved.
* @return The saved appUser.
*/
public AppUser saveUser(AppUser appUser) {
dynamoDBMapper.save(appUser);
return appUser;
}

/**
* Retrieves a customer by email from the DynamoDB table.
*
* @param email The email of the customer to retrieve.
* @return The retrieved customer or null if not found.
*/
public AppUser getUserByEmail(String email) {
return dynamoDBMapper.load(AppUser.class, email);
}

/**
* Returns the DynamoDBMapper instance used by the repository.
*
* @return The DynamoDBMapper instance.
*/
public DynamoDBMapper getDynamoDBMapper() {
return dynamoDBMapper;
}

/**
* Sets the DynamoDBMapper instance to be used by the repository.
*
* @param dynamoDBMapper The DynamoDBMapper instance.
*/
public void setDynamoDBMapper(DynamoDBMapper dynamoDBMapper) {
this.dynamoDBMapper = dynamoDBMapper;
}
}
AppUserDetailsServce.java
  • The user details are retrieved from the repository by calling the getUserByEmail() method of the UserRepository instance.
  • If the appUser is null, indicating that the user was not found, a UsernameNotFoundException is thrown.
  • If the appUser is found, a UserDetails object is created using the retrieved user details. The User class from Spring Security is used to build the UserDetails object, providing the user's email as the username, password, and authorities (roles).
package apigateway.service;

import apigateway.model.AppUser;
import apigateway.repository.UserRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
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;

@Service
public class AppUserDetailsService implements UserDetailsService {

@Autowired
UserRepository repoUsr;

@Override
public UserDetails loadUserByUsername(String useremail) throws UsernameNotFoundException {

// Retrieve the AppUser entity from the UserRepository based on the provided useremail
AppUser appUser = repoUsr.getUserByEmail(useremail);

if (appUser == null) {
// If the user is not found, throw a UsernameNotFoundException
throw new UsernameNotFoundException(useremail);
}

// Create a UserDetails object using the retrieved AppUser entity
UserDetails usr = User.withUsername(appUser.getEmail())
.password(appUser.getPassword())
.authorities(appUser.getRole())
.build();

return usr;
}
}
JwtTokenUtil.java

The class provides several methods for JWT token handling, including:
  • extractUsername: Extracts the username (subject) from the token.
  • extractExpiration: Extracts the expiration date from the token.
  • extractClaim: Extracts a specific claim from the token using a Claims resolver function.
  • extractAllClaims: Extracts all claims from the token by parsing its body.
  • isTokenExpired: Checks if the token is expired based on its expiration date.
  • generateToken: Generates a new token for the given user details.
  • createToken: Creates a token with specified claims and subject.
  • validateToken: Validates the token for the given user details, checking if the username matches and the token is not expired.
  • hasRole: Checks if the user associated with the token has the specified role. It extracts the token from the HTTP request headers, validates it, and verifies if the user details contain the requested role.
package apigateway.util;

import apigateway.service.AppUserDetailsService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.stereotype.Service;

import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;

@Service
public class JwtTokenUtil {

private static final Logger LOGGER = LoggerFactory.getLogger(JwtTokenUtil.class);

@Autowired
private AppUserDetailsService appUserDetailsService;

private String SECRET_KEY = "secret";

// Extract the username from the token
public String extractUsername(String token) {
return extractClaim(token, Claims::getSubject);
}

// Extract the expiration date from the token
public Date extractExpiration(String token) {
return extractClaim(token, Claims::getExpiration);
}

// Extract a specific claim from the token
public <T> T extractClaim(String token, Function<Claims, T> claimsResolver) {
final Claims claims = extractAllClaims(token);
return claimsResolver.apply(claims);
}

// Extract all claims from the token
private Claims extractAllClaims(String token) {
return Jwts.parser().setSigningKey(SECRET_KEY).parseClaimsJws(token).getBody();
}

// Check if the token is expired
private Boolean isTokenExpired(String token) {
return extractExpiration(token).before(new Date());
}

// Generate a new token for the given user details
public String generateToken(UserDetails userDetails) {
Map<String, Object> claims = new HashMap<>();
return createToken(claims, userDetails.getUsername());
}

// Create a token with the specified claims and subject
private String createToken(Map<String, Object> claims, String subject) {
return Jwts.builder()
.setClaims(claims)
.setSubject(subject)
.setIssuedAt(new Date(System.currentTimeMillis()))
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 60 * 10))
.signWith(SignatureAlgorithm.HS256, SECRET_KEY)
.compact();
}

// Validate the token for the given user details
public Boolean validateToken(String token, UserDetails userDetails) {
final String username = extractUsername(token);
return (username.equals(userDetails.getUsername()) && !isTokenExpired(token));
}

// Check if the user has the specified role based on the token
public boolean hasRole(ServerHttpRequest request, String role) {
LOGGER.info("Checking user role");
final List<String> authorizationHeaders = request.getHeaders().get(HttpHeaders.AUTHORIZATION);

String username = null;
String jwt = null;

if (authorizationHeaders != null && !authorizationHeaders.isEmpty()) {
String authorizationHeader = authorizationHeaders.get(0);
LOGGER.info("Header contains bearer token");
if (authorizationHeader.startsWith("Bearer ")) {
jwt = authorizationHeader.substring(7);
username = extractUsername(jwt);
}
}

if (username != null && SecurityContextHolder.getContext().getAuthentication() == null) {
UserDetails userDetails = appUserDetailsService.loadUserByUsername(username);

if (validateToken(jwt, userDetails)) {
LOGGER.info("JWT Token is Valid.");
LOGGER.info(userDetails.getAuthorities().toString());
LOGGER.info(role);
if (userDetails.getAuthorities().toString().contains(role)) {
return true;
}
}
}

LOGGER.info("Role is not found");
return false;
}
}
RoleAuthGatewayFilterFactory
  • The class provides an overridden apply method that creates and returns a GatewayFilter. This filter performs role-based authorization by checking if the requested role is present in the JWT token associated with the request. If the role is not present, an unauthorized response is returned. If the role is present, the request is passed to the next filter in the chain.
  • The class defines an inner Config class annotated with @Data, which represents the configuration for the filter factory. In this case, the Config class only has a single field role, representing the role that needs to be authorized.
  • The shortcutFieldOrder method is overridden to define the order of fields in the application.yml shortcut configuration. In this case, the role field is the only field and is returned as a singleton list.
package apigateway.filter;

import apigateway.util.JwtTokenUtil;
import lombok.Data;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cloud.gateway.filter.GatewayFilter;
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

@Component
public class RoleAuthGatewayFilterFactory extends AbstractGatewayFilterFactory<RoleAuthGatewayFilterFactory.Config> {

private static final Logger LOGGER = LoggerFactory.getLogger(RoleAuthGatewayFilterFactory.class);

@Autowired
private JwtTokenUtil jwtTokenUtil;

public RoleAuthGatewayFilterFactory() {
super(Config.class);
}

@Override
public GatewayFilter apply(Config config) {
return (exchange, chain) -> {
var request = exchange.getRequest();
LOGGER.info("Inside Gateway Filter");
try {
if (!jwtTokenUtil.hasRole(request, config.getRole())) {
// If the role is not available in the token, return an unauthorized response
var response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}
} catch (Exception e) {
LOGGER.info("Authorization failed");
var response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
return response.setComplete();
}

// If the role is available, continue to the next filter in the chain
return chain.filter(exchange);
};
}

@Data
public static class Config {
private String role;
}

@Override
public List<String> shortcutFieldOrder() {
// This method defines the order of fields in the application.yml shortcut configuration
return Arrays.asList("role");
}
}
application.yaml
amazon:
dynamodb:
endpoint: http://localhost:8000 # Endpoint for the DynamoDB service
aws:
region: us-east-1 # AWS region
accesskey: dummyid # Access key for authentication
secretkey: dummypw # Secret key for authentication
server:
port: 9999 # Port on which the server will listen
spring:
application:
name: apigateway # Name of the Spring Boot application
cloud:
gateway:
routes: # Configuration for the gateway routes
- id: addrestaurant-route-id # ID for the route
uri: lb://addrestaurant # URI for load-balanced routing
filters:
- RoleAuth=ADMIN # Filter for role-based authentication (for ADMIN role)
predicates:
- Path=/food/api/v1/admin/add-restaurant/** # Predicate for matching request path
- id: updateprice-route-id
uri: lb://updateprice
filters:
- RoleAuth=ADMIN
predicates:
- Path=/food/api/v1/admin/update-price/**
- id: registration-route-id
uri: lb://registration
predicates:
- Path=/food/api/v1/user/register/** , /food/api/v1/user/login/**
- id: search-route-id
uri: lb://searchfood
filters:
- RoleAuth=CUSTOMER # Filter for role-based authentication (for CUSTOMER role)
predicates:
- Path=/food/api/v1/user/restaurantname/** , /food/api/v1/user/menuitem/**
eureka:
client:
service-url:
defaultZone: http://localhost:9000/eureka # Eureka server URL for service discovery
fetch-registry: true # Fetch registry from Eureka server
register-with-eureka: true # Register with Eureka server
instance:
hostname: localhost # Hostname of the current instance
Complete source code:

https://github.com/it-code-lab/restaurant

    Leave a Comment


  • captcha text