Java Spring Boot Microservices #Part III (Gateway dan Main Service Implementasi)

Septian Reza Andrianto
6 min readDec 26, 2023

--

Hallo semua,
Kali ini saya ingin share tentang implementasi microservices arsitektur pada project Java Spring Boot,
Buat teman-teman yang belum baca tutorial implementasi part II, bisa lihat disini ya https://medium.com/@septianrezaa/java-spring-boot-microservices-part-ii-authorization-security-implementasi-58c2ae1c7485

Pada part III ini saya ingin share tentang implementasi gateway menggunakan Spring Cloud Gateway,
Pertama saya akan implement service (be-gateway) gateway dulu, kita generate projectnya dulu di https://start.spring.io/, disini saya menggunakan dependecy Spring Boot Actuator, Config Client, Reactive Gateway, Eureka Discovery Client, Lombok

Jika sudah kita generate dan open project menggunakan IDE yang biasa kita gunakan. Selain dependecy-dependecy diatas saya juga menggunakan dependency Jwt seperti jjwt-api, jjwt-impl dan jjwt-jackson.

<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-api -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-api</artifactId>
<version>0.12.3</version>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-impl -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-impl</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/io.jsonwebtoken/jjwt-jackson -->
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt-jackson</artifactId>
<version>0.12.3</version>
<scope>runtime</scope>
</dependency>

Kalian bisa menambahkan dependecy diatas pada pom.xml.
Berikut stuktur projectnya

Pertama kita setting confignya pada file application.yml, kita gunakan jwt secret yang sama seperti yang kita define pada project be-auth

application.yml

Jika sudah, kita setting confignya juga pada project be-config-server, untuk mendaftarkan project be-gateway ini.

gateway.yml

Kita buat file baru pada package configs yang terletak didalam package resource dengan nama gateway.yml (nama file disesuaikan dengan application name yang kita define pada application.yml be-gateway, untuk project be-gateway ini port yang saya gunakan adalah 8080, seperti gambar diatas.
Selanjutnya kita buat class Constant, untuk mengambil value2 yang bersifat constant, untuk mengurangi redundant code. Kita buat package constant dan interface Constant.java

Constant.java

Selanjutnya kita buat class DTO (Data Transfer Object), kita buat package dengan nama dto,
Kemudian kita buat class dengan nama Response.java

Response.java

Selanjutnya kita akan buat filter untuk validasi jwt tokennya dan juga route validasi. disini saya membuat package dengan nama filter dimana didalamnya terdapat beberapa class, diantaranya AuthFilter.java, Errorhandler.java, JwtUtils.java dan RouteValidator.java

package com.service.begateway.filter;

import com.service.begateway.constant.Constant;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
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.stereotype.Component;

import java.util.Objects;

@Component
@Slf4j
public class AuthFilter extends AbstractGatewayFilterFactory<AuthFilter.Config> {

@Autowired
private RouteValidator routeValidator;
@Autowired
private JwtUtils jwtUtils;

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

@Override
public GatewayFilter apply(Config config) {
return ((exchange, chain) -> {
if (routeValidator.isSecure.test(exchange.getRequest())) {
// check header not contain Authorization
if (!exchange.getRequest().getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
throw new RuntimeException(Constant.Message.FORBIDDEN_MESSAGE);
}

String authHeader = exchange.getRequest().getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
if (Objects.nonNull(authHeader) && authHeader.startsWith("Bearer")) {
authHeader = authHeader.split("\\s")[1];

try {
boolean isValid = jwtUtils.validateJwtToken(authHeader);
if (!isValid) {
throw new RuntimeException(Constant.Message.INVALID_TOKEN_MESSAGE);
}
} catch (Exception e) {
throw new RuntimeException(Constant.Message.INVALID_TOKEN_MESSAGE);
}

} else {
throw new RuntimeException(Constant.Message.INVALID_TOKEN_MESSAGE);
}
}
return chain.filter(exchange);
});
}

public static class Config {}

}
package com.service.begateway.filter;

import com.service.begateway.dto.Response;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import java.util.Collections;
import java.util.List;

@ControllerAdvice
public class ErrorHandler {

@ExceptionHandler(Exception.class)
public final ResponseEntity<Response<Object>> handleGeneralExceptions(Exception ex) {
List<String> errorList = Collections.singletonList(ex.getMessage());
return new ResponseEntity<>(mappingError(HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), errorList),
new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}

@ExceptionHandler(RuntimeException.class)
public final ResponseEntity<Response<Object>> handleRuntimeExceptions(RuntimeException ex) {
List<String> errorList = Collections.singletonList(ex.getMessage());
return new ResponseEntity<>(mappingError(HttpStatus.INTERNAL_SERVER_ERROR.value(),
HttpStatus.INTERNAL_SERVER_ERROR.getReasonPhrase(), errorList),
new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}

private Response<Object> mappingError(int responseCode, String responseMessage, List<String> errorList) {
return Response.builder()
.responseCode(responseCode)
.responseMessage(responseMessage)
.errorList(errorList)
.build();
}
}
package com.service.begateway.filter;

import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import java.security.Key;
import java.time.Instant;
import java.util.*;

@Component
@Slf4j
public class JwtUtils {

@Value("${jwt.secret}")
private String jwtSecret;

Date nowDate = new Date();
private Key key() {
return Keys.hmacShaKeyFor(Decoders.BASE64.decode(jwtSecret));
}

public boolean validateJwtToken(String authToken) {
try {
Base64.Decoder decoder = Base64.getUrlDecoder();
String[] chunks = authToken.split("\\.");
String header = new String(decoder.decode(chunks[0]));
String payload = new String(decoder.decode(chunks[1]));
String expiredEpochTime = payload.split(":")[3].replace("}", "");
log.info("header= " + header + " |payload= " + payload + " |expiredEpochTime " + expiredEpochTime);

if (this.nowDate.before(Date.from(Instant.ofEpochSecond(Long.valueOf(expiredEpochTime))))) {
Jwts.parser().setSigningKey(key()).build().parse(authToken);
return true;
}
} catch (MalformedJwtException e) {
log.error("Invalid JWT token: {}", e.getMessage());
} catch (ExpiredJwtException e) {
log.error("JWT token is expired: {}", e.getMessage());
} catch (UnsupportedJwtException e) {
log.error("JWT token is unsupported: {}", e.getMessage());
} catch (IllegalArgumentException e) {
log.error("JWT claims string is empty: {}", e.getMessage());
}

return false;
}

}
package com.service.begateway.filter;

import com.service.begateway.constant.Constant;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.stereotype.Component;

import java.util.function.Predicate;

@Component
public class RouteValidator {

public Predicate<ServerHttpRequest> isSecure = request -> Constant.AUTH_WHITELIST.stream()
.noneMatch(uri -> request.getURI().getPath().contains(uri));

}

Selanjutnya kita bisa langsung melakukan testing, jangan lupa untuk running service be-config-server, be-discovery sebelum kita running project be-gateway ini. Jika sudah kita bisa akses http://localhost:8761/

Dari gambar diatas kita sudah berhasil mengkoneksikan Gateway (be-gateway) kita. Yang nantinya kita akan akses semua service melalui service gateway ini.

Selanjutnya saya akan buat 2 project lagi yang berfungsi sebagai main service kita, disini saya akan membuat project dengan nama be-catalog dan be-report dengan membuat API test saja didalamnya. Langsung saja kita generate pada https://start.spring.io/, disini sya menggunakan dependecy Spring Boot Actuator, Config Client, Eureka Discovery Client, Lombok, H2 Database, Spring Boot Starter Validation, dan Spring Data JPA

Jika sudah langsung saya kita open project pada be-catalog ini kedalam IDE yang biasa kita gunakan, disini saya akan membuat sebuah controller yang berisi API test yang hanya return String, kita buat package dengan nama Controller kemudian buat CategoryController.java

package com.service.becatalog.controller;

import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping(value = "/category")
public class CategoryController {

@GetMapping(value = "/test")
public ResponseEntity<Object> doTest() {
return ResponseEntity.ok("Success Test");
}
}

Selanjutnya kita setting config pada file application.yml yang terdapat dalam package resources

application.yml

Jika sudah, kita setting confignya juga pada project be-config-server, untuk mendaftarkan project be-catalog ini.

catalog.yml

Kita buat file baru pada package configs yang terletak didalam package resource dengan nama catalog.yml (nama file disesuaikan dengan application name yang kita define pada application.yml be-catalog, seperti gambar diatas.
Jika sudah langsung saya kita running project be-catalog ini, Selanjutnya kita coba akses http://localhost:8761/

Dari gambar diatas kita sudah berhasil mengkoneksikan service catalog (be-catalog) kita.

Sekarang kita tambahkan route filter untuk service catalog ini pada gateway.yml yang terdapat pada project be-config-server

Lakukan hal yang sama seperti generate service be-catalog, untuk membuat be-report, Selanjutnya saya akan lakukan testing, untuk hit API yang terdapat pada service be-auth dan juga be-catalog melalui be-gateway (port 8080)

Pertama saya coba hit API /category/test yang terdapat pada service be-catalog

Dari response message yang kita dapat, kita tidak memiliki akses untuk mengkases API tersebut karena kita belum memasukan bearer / access token dari service be-auth melalui gateway

curl --location 'localhost:8080/verif/login' \
--header 'Content-Type: application/json' \
--data '{
"username": "test",
"password": "kmzway87aa"
}'

Selanjutnya saya akan coba login terlebih dahulu, untuk mendapatkan bearer/ access token yang kemudian kita gunakan untuk hit kembali API /category/test

curl --location 'localhost:8080/category/test' \
--header 'Authorization: Bearer eyJhbGciOiJIUzI1NiJ9.eyJzdWIiOiJ0ZXN0IiwiaWF0IjoxNzAzNTc1NzQ3LCJleHAiOjE3MDM1NzY2NDd9.tE0rvaYl06PfCUz7NhjwEi7pmC5VbLMX6r6XpuMSKgo'

Mendapatkan http status 200. Itu tandanya kita telah sukses membuat microservices menggunakan java spring boot.

Cukup sekian tutorial part III ini semoga bermanfaat,

Terima kasih.

Github Link :
backend microservices :
https://github.com/septianrezaandrianto/backend-microservices/tree/master

--

--

Septian Reza Andrianto

I'm a Software Engineer with more than three years experiences