Co-authored-by: TakahashiNg <83152264+TakahashiNguyen@users.noreply.github.com> Reviewed-on: #19
This commit was merged in pull request #19.
This commit is contained in:
@@ -16,6 +16,7 @@ RUN apt-get update --fix-missing && apt-get install --no-install-recommends --no
|
|||||||
docker-buildx \
|
docker-buildx \
|
||||||
git \
|
git \
|
||||||
docker.io \
|
docker.io \
|
||||||
|
nodejs \
|
||||||
maven \
|
maven \
|
||||||
libz-dev
|
libz-dev
|
||||||
|
|
||||||
|
|||||||
+3
-16
@@ -17,6 +17,9 @@
|
|||||||
<packaging>quarkus</packaging>
|
<packaging>quarkus</packaging>
|
||||||
|
|
||||||
<properties>
|
<properties>
|
||||||
|
<quarkus.package.jar.enabled>false</quarkus.package.jar.enabled>
|
||||||
|
<skipITs>false</skipITs>
|
||||||
|
<quarkus.native.enabled>true</quarkus.native.enabled>
|
||||||
<compiler-plugin.version>3.15.0</compiler-plugin.version>
|
<compiler-plugin.version>3.15.0</compiler-plugin.version>
|
||||||
<maven.compiler.release>25</maven.compiler.release>
|
<maven.compiler.release>25</maven.compiler.release>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
@@ -158,20 +161,4 @@
|
|||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
|
|
||||||
<profiles>
|
|
||||||
<profile>
|
|
||||||
<id>native</id>
|
|
||||||
<activation>
|
|
||||||
<property>
|
|
||||||
<name>native</name>
|
|
||||||
</property>
|
|
||||||
</activation>
|
|
||||||
<properties>
|
|
||||||
<quarkus.package.jar.enabled>false</quarkus.package.jar.enabled>
|
|
||||||
<skipITs>false</skipITs>
|
|
||||||
<quarkus.native.enabled>true</quarkus.native.enabled>
|
|
||||||
</properties>
|
|
||||||
</profile>
|
|
||||||
</profiles>
|
|
||||||
</project>
|
</project>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.drinkool.services;
|
package com.drinkool.services;
|
||||||
|
|
||||||
import com.drinkool.Jwt;
|
import com.drinkool.InternalValue;
|
||||||
import com.drinkool.Role;
|
import com.drinkool.Role;
|
||||||
import com.drinkool.Url;
|
import com.drinkool.Url;
|
||||||
import com.drinkool.dtos.*;
|
import com.drinkool.dtos.*;
|
||||||
@@ -25,8 +25,8 @@ public class AuthenticationService {
|
|||||||
newCustomer.signup(input.name, input.phone, input.password);
|
newCustomer.signup(input.name, input.phone, input.password);
|
||||||
|
|
||||||
return Response.status(Response.Status.CREATED)
|
return Response.status(Response.Status.CREATED)
|
||||||
.header(Jwt.userId, newCustomer.id.toString())
|
.header(InternalValue.userId, newCustomer.id.toString())
|
||||||
.header(Jwt.role, Role.Customer)
|
.header(InternalValue.role, Role.Customer)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -39,8 +39,8 @@ public class AuthenticationService {
|
|||||||
newCustomer.signup(input.phone);
|
newCustomer.signup(input.phone);
|
||||||
|
|
||||||
return Response.status(Response.Status.CREATED)
|
return Response.status(Response.Status.CREATED)
|
||||||
.header(Jwt.userId, newCustomer.id.toString())
|
.header(InternalValue.userId, newCustomer.id.toString())
|
||||||
.header(Jwt.role, Role.Customer)
|
.header(InternalValue.role, Role.Customer)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -63,8 +63,8 @@ public class AuthenticationService {
|
|||||||
.build();
|
.build();
|
||||||
|
|
||||||
return Response.accepted()
|
return Response.accepted()
|
||||||
.header(Jwt.userId, customer.id.toString())
|
.header(InternalValue.userId, customer.id.toString())
|
||||||
.header(Jwt.role, Role.Customer)
|
.header(InternalValue.role, Role.Customer)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,8 +83,8 @@ public class AuthenticationService {
|
|||||||
).firstResult();
|
).firstResult();
|
||||||
|
|
||||||
return Response.accepted()
|
return Response.accepted()
|
||||||
.header(Jwt.userId, customer.id.toString())
|
.header(InternalValue.userId, customer.id.toString())
|
||||||
.header(Jwt.role, Role.Customer)
|
.header(InternalValue.role, Role.Customer)
|
||||||
.build();
|
.build();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package com.drinkool.services;
|
package com.drinkool.services;
|
||||||
|
|
||||||
import com.drinkool.Jwt;
|
import com.drinkool.InternalValue;
|
||||||
import com.drinkool.Url;
|
import com.drinkool.Url;
|
||||||
import com.drinkool.models.UserModel;
|
import com.drinkool.models.UserModel;
|
||||||
import jakarta.transaction.Transactional;
|
import jakarta.transaction.Transactional;
|
||||||
@@ -14,7 +14,7 @@ public class UserService {
|
|||||||
@POST
|
@POST
|
||||||
@Path(Url.Me)
|
@Path(Url.Me)
|
||||||
@Transactional
|
@Transactional
|
||||||
public Response me(@HeaderParam(Jwt.userId) UUID id) {
|
public Response me(@HeaderParam(InternalValue.userId) UUID id) {
|
||||||
if (id == null) return Response.status(Response.Status.BAD_REQUEST)
|
if (id == null) return Response.status(Response.Status.BAD_REQUEST)
|
||||||
.entity("InvalidUserId")
|
.entity("InvalidUserId")
|
||||||
.build();
|
.build();
|
||||||
|
|||||||
@@ -21,4 +21,8 @@ quarkus.redis.hosts=redis://localhost:6379
|
|||||||
|
|
||||||
# Dependency
|
# Dependency
|
||||||
quarkus.index-dependency.lib.group-id=com.drinkool
|
quarkus.index-dependency.lib.group-id=com.drinkool
|
||||||
quarkus.index-dependency.lib.artifact-id=lib
|
quarkus.index-dependency.lib.artifact-id=lib
|
||||||
|
|
||||||
|
# Build
|
||||||
|
quarkus.native.container-build=false
|
||||||
|
quarkus.native.additional-build-args=--initialize-at-run-time=com.password4j.AlgorithmFinder
|
||||||
@@ -4,7 +4,7 @@ import static io.restassured.RestAssured.*;
|
|||||||
import static org.hamcrest.CoreMatchers.*;
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
import static org.junit.jupiter.api.Assertions.*;
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
import com.drinkool.Jwt;
|
import com.drinkool.InternalValue;
|
||||||
import com.drinkool.Role;
|
import com.drinkool.Role;
|
||||||
import com.drinkool.Url;
|
import com.drinkool.Url;
|
||||||
import com.drinkool.dtos.*;
|
import com.drinkool.dtos.*;
|
||||||
@@ -55,8 +55,8 @@ public class AuthenticationServiceTest {
|
|||||||
.when()
|
.when()
|
||||||
.post(Url.QuickSignup)
|
.post(Url.QuickSignup)
|
||||||
.then()
|
.then()
|
||||||
.header(Jwt.userId, notNullValue())
|
.header(InternalValue.userId, notNullValue())
|
||||||
.header(Jwt.role, is(Role.Customer))
|
.header(InternalValue.role, is(Role.Customer))
|
||||||
.statusCode(201);
|
.statusCode(201);
|
||||||
|
|
||||||
assertEquals(1, CustomerEntity.find("phone", phone).count());
|
assertEquals(1, CustomerEntity.find("phone", phone).count());
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ package com.drinkool.services;
|
|||||||
import static io.restassured.RestAssured.*;
|
import static io.restassured.RestAssured.*;
|
||||||
import static org.hamcrest.CoreMatchers.*;
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
|
||||||
import com.drinkool.Jwt;
|
import com.drinkool.InternalValue;
|
||||||
import com.drinkool.Url;
|
import com.drinkool.Url;
|
||||||
import com.drinkool.dtos.QuickSignup;
|
import com.drinkool.dtos.QuickSignup;
|
||||||
import io.quarkus.test.junit.QuarkusTest;
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
@@ -26,10 +26,10 @@ public class UserServiceTest {
|
|||||||
.post(Url.QuickSignup)
|
.post(Url.QuickSignup)
|
||||||
.then()
|
.then()
|
||||||
.extract()
|
.extract()
|
||||||
.header(Jwt.userId);
|
.header(InternalValue.userId);
|
||||||
|
|
||||||
given()
|
given()
|
||||||
.header(Jwt.userId, testUserId)
|
.header(InternalValue.userId, testUserId)
|
||||||
.contentType(ContentType.JSON)
|
.contentType(ContentType.JSON)
|
||||||
.when()
|
.when()
|
||||||
.post(Url.Me)
|
.post(Url.Me)
|
||||||
|
|||||||
+14
-13
@@ -1,8 +1,8 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8" ?>
|
<?xml version="1.0" encoding="UTF-8"?>
|
||||||
<project
|
<project
|
||||||
xmlns="http://maven.apache.org/POM/4.0.0"
|
xmlns="http://maven.apache.org/POM/4.0.0"
|
||||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
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"
|
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>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
|
|
||||||
@@ -23,11 +23,9 @@
|
|||||||
<compiler-plugin.version>3.15.0</compiler-plugin.version>
|
<compiler-plugin.version>3.15.0</compiler-plugin.version>
|
||||||
<maven.compiler.release>25</maven.compiler.release>
|
<maven.compiler.release>25</maven.compiler.release>
|
||||||
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
|
||||||
<project.reporting.outputEncoding
|
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
|
||||||
>UTF-8</project.reporting.outputEncoding>
|
|
||||||
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
<quarkus.platform.artifact-id>quarkus-bom</quarkus.platform.artifact-id>
|
||||||
<quarkus.platform.group-id
|
<quarkus.platform.group-id>io.quarkus.platform</quarkus.platform.group-id>
|
||||||
>io.quarkus.platform</quarkus.platform.group-id>
|
|
||||||
<quarkus.platform.version>3.32.4</quarkus.platform.version>
|
<quarkus.platform.version>3.32.4</quarkus.platform.version>
|
||||||
<skipITs>true</skipITs>
|
<skipITs>true</skipITs>
|
||||||
<surefire-plugin.version>3.5.4</surefire-plugin.version>
|
<surefire-plugin.version>3.5.4</surefire-plugin.version>
|
||||||
@@ -46,6 +44,11 @@
|
|||||||
</dependencyManagement>
|
</dependencyManagement>
|
||||||
|
|
||||||
<dependencies>
|
<dependencies>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.bitbucket.b_c</groupId>
|
||||||
|
<artifactId>jose4j</artifactId>
|
||||||
|
<version>0.9.6</version>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.drinkool</groupId>
|
<groupId>com.drinkool</groupId>
|
||||||
<artifactId>lib</artifactId>
|
<artifactId>lib</artifactId>
|
||||||
@@ -104,8 +107,7 @@
|
|||||||
<configuration>
|
<configuration>
|
||||||
<argLine>@{argLine}</argLine>
|
<argLine>@{argLine}</argLine>
|
||||||
<systemPropertyVariables>
|
<systemPropertyVariables>
|
||||||
<java.util.logging.manager
|
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||||
>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
|
||||||
<maven.home>${maven.home}</maven.home>
|
<maven.home>${maven.home}</maven.home>
|
||||||
</systemPropertyVariables>
|
</systemPropertyVariables>
|
||||||
</configuration>
|
</configuration>
|
||||||
@@ -126,12 +128,11 @@
|
|||||||
<systemPropertyVariables>
|
<systemPropertyVariables>
|
||||||
<native.image.path>
|
<native.image.path>
|
||||||
${project.build.directory}/${project.build.finalName}-runner</native.image.path>
|
${project.build.directory}/${project.build.finalName}-runner</native.image.path>
|
||||||
<java.util.logging.manager
|
<java.util.logging.manager>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
||||||
>org.jboss.logmanager.LogManager</java.util.logging.manager>
|
|
||||||
<maven.home>${maven.home}</maven.home>
|
<maven.home>${maven.home}</maven.home>
|
||||||
</systemPropertyVariables>
|
</systemPropertyVariables>
|
||||||
</configuration>
|
</configuration>
|
||||||
</plugin>
|
</plugin>
|
||||||
</plugins>
|
</plugins>
|
||||||
</build>
|
</build>
|
||||||
</project>
|
</project>
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
package com.drinkool.filters;
|
||||||
|
|
||||||
|
import com.drinkool.InternalValue;
|
||||||
|
import io.smallrye.mutiny.Uni;
|
||||||
|
import jakarta.ws.rs.Priorities;
|
||||||
|
import jakarta.ws.rs.container.*;
|
||||||
|
import jakarta.ws.rs.core.*;
|
||||||
|
import java.util.*;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
import org.jboss.resteasy.reactive.server.ServerRequestFilter;
|
||||||
|
import org.jboss.resteasy.reactive.server.ServerResponseFilter;
|
||||||
|
import org.jose4j.jws.AlgorithmIdentifiers;
|
||||||
|
import org.jose4j.jws.JsonWebSignature;
|
||||||
|
import org.jose4j.jwt.JwtClaims;
|
||||||
|
import org.jose4j.jwt.consumer.JwtConsumer;
|
||||||
|
import org.jose4j.jwt.consumer.JwtConsumerBuilder;
|
||||||
|
import org.jose4j.keys.HmacKey;
|
||||||
|
|
||||||
|
public class InternalHeaderGatewayFilter {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "gateway.jwt.secret")
|
||||||
|
String SECRET_KEY;
|
||||||
|
|
||||||
|
private boolean isSystemClaim(String key) {
|
||||||
|
return List.of("iss", "sub", "aud", "exp", "nbf", "iat", "jti").contains(
|
||||||
|
key
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ServerRequestFilter(preMatching = true)
|
||||||
|
public Uni<Response> handleRequestHeaders(
|
||||||
|
ContainerRequestContext requestContext
|
||||||
|
) {
|
||||||
|
requestContext
|
||||||
|
.getHeaders()
|
||||||
|
.keySet()
|
||||||
|
.removeIf(
|
||||||
|
key ->
|
||||||
|
key.equalsIgnoreCase(InternalValue.headerId) ||
|
||||||
|
key.toLowerCase().startsWith(InternalValue.headerId.toLowerCase())
|
||||||
|
);
|
||||||
|
|
||||||
|
Map<String, Cookie> cookies = requestContext.getCookies();
|
||||||
|
Cookie authCookie = cookies.get(InternalValue.cookieName);
|
||||||
|
|
||||||
|
if (authCookie == null) return Uni.createFrom().nullItem();
|
||||||
|
|
||||||
|
return Uni.createFrom().item(() -> {
|
||||||
|
try {
|
||||||
|
String token = authCookie.getValue();
|
||||||
|
|
||||||
|
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
|
||||||
|
.setRequireExpirationTime()
|
||||||
|
.setVerificationKey(new HmacKey(SECRET_KEY.getBytes()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
JwtClaims claims = jwtConsumer.processToClaims(token);
|
||||||
|
|
||||||
|
Map<String, Object> allClaims = claims.getClaimsMap();
|
||||||
|
|
||||||
|
allClaims.forEach((key, value) -> {
|
||||||
|
if (!isSystemClaim(key) && value != null) {
|
||||||
|
String headerName = InternalValue.headerId + (key);
|
||||||
|
|
||||||
|
requestContext.getHeaders().add(headerName, value.toString());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} catch (Exception e) {
|
||||||
|
return Response.status(Response.Status.UNAUTHORIZED)
|
||||||
|
.entity("InvalidToken")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@ServerResponseFilter(priority = Priorities.USER + 100)
|
||||||
|
public void handleResponseHeaders(ContainerResponseContext responseContext) {
|
||||||
|
JwtClaims claims = new JwtClaims();
|
||||||
|
boolean hasInternalData = false;
|
||||||
|
|
||||||
|
MultivaluedMap<String, Object> headers = responseContext.getHeaders();
|
||||||
|
|
||||||
|
List<String> keysToRemove = new ArrayList<>();
|
||||||
|
|
||||||
|
for (String key : headers.keySet()) {
|
||||||
|
if (key.toLowerCase().startsWith(InternalValue.headerId.toLowerCase())) {
|
||||||
|
Object value = headers.getFirst(key);
|
||||||
|
if (value != null) {
|
||||||
|
String claimKey = key.substring(InternalValue.headerId.length());
|
||||||
|
claims.setClaim(claimKey, value.toString());
|
||||||
|
hasInternalData = true;
|
||||||
|
}
|
||||||
|
keysToRemove.add(key);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (String key : keysToRemove) {
|
||||||
|
headers.remove(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasInternalData) {
|
||||||
|
try {
|
||||||
|
claims.setExpirationTimeMinutesInTheFuture(60);
|
||||||
|
claims.setGeneratedJwtId();
|
||||||
|
claims.setIssuedAtToNow();
|
||||||
|
|
||||||
|
JsonWebSignature jws = new JsonWebSignature();
|
||||||
|
jws.setPayload(claims.toJson());
|
||||||
|
jws.setKey(new HmacKey(SECRET_KEY.getBytes()));
|
||||||
|
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
|
||||||
|
|
||||||
|
String jwt = jws.getCompactSerialization();
|
||||||
|
|
||||||
|
NewCookie jwtCookie = new NewCookie.Builder(InternalValue.cookieName)
|
||||||
|
.value(jwt)
|
||||||
|
.path("/")
|
||||||
|
.httpOnly(true)
|
||||||
|
.secure(true)
|
||||||
|
.sameSite(NewCookie.SameSite.STRICT)
|
||||||
|
.maxAge(3600)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
headers.add(HttpHeaders.SET_COOKIE, jwtCookie);
|
||||||
|
} catch (Exception e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,4 +8,4 @@ quarkus.container-image.builder=docker
|
|||||||
quarkus.container-image.additional-tags=latest
|
quarkus.container-image.additional-tags=latest
|
||||||
|
|
||||||
# Build
|
# Build
|
||||||
quarkus.native.container-build=false
|
quarkus.native.container-build=false
|
||||||
+97
@@ -0,0 +1,97 @@
|
|||||||
|
package com.drinkool.filters;
|
||||||
|
|
||||||
|
import static io.restassured.RestAssured.*;
|
||||||
|
import static org.hamcrest.CoreMatchers.*;
|
||||||
|
import static org.junit.jupiter.api.Assertions.*;
|
||||||
|
|
||||||
|
import com.drinkool.InternalValue;
|
||||||
|
import io.quarkus.test.junit.QuarkusTest;
|
||||||
|
import io.restassured.response.Response;
|
||||||
|
import org.eclipse.microprofile.config.inject.ConfigProperty;
|
||||||
|
import org.jose4j.jws.AlgorithmIdentifiers;
|
||||||
|
import org.jose4j.jws.JsonWebSignature;
|
||||||
|
import org.jose4j.jwt.JwtClaims;
|
||||||
|
import org.jose4j.keys.HmacKey;
|
||||||
|
import org.junit.jupiter.api.DisplayName;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
@QuarkusTest
|
||||||
|
public class InternalHeaderGatewayFilterTest {
|
||||||
|
|
||||||
|
@ConfigProperty(name = "gateway.jwt.secret")
|
||||||
|
String TEST_SECRET;
|
||||||
|
|
||||||
|
private String createTestToken(String userId) throws Exception {
|
||||||
|
JwtClaims claims = new JwtClaims();
|
||||||
|
claims.setExpirationTimeMinutesInTheFuture(10);
|
||||||
|
claims.setClaim("UserId", userId);
|
||||||
|
claims.setClaim("Role", "user");
|
||||||
|
|
||||||
|
JsonWebSignature jws = new JsonWebSignature();
|
||||||
|
jws.setPayload(claims.toJson());
|
||||||
|
jws.setKey(new HmacKey(TEST_SECRET.getBytes()));
|
||||||
|
jws.setAlgorithmHeaderValue(AlgorithmIdentifiers.HMAC_SHA256);
|
||||||
|
return jws.getCompactSerialization();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Nên xóa header X-Internal nếu client tự ý gửi lên")
|
||||||
|
public void testShouldRemoveClentInternalHeaders() {
|
||||||
|
given()
|
||||||
|
.header("X-Internal-userId", "hacker-id")
|
||||||
|
.when()
|
||||||
|
.get("/test-gateway")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body(is("not-found")); // Filter phải xóa sạch trước khi vào Resource
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Nên trích xuất JWT từ Cookie và gắn vào Header nội bộ")
|
||||||
|
public void testValidJwtInCookie() throws Exception {
|
||||||
|
String token = createTestToken("user123");
|
||||||
|
|
||||||
|
given()
|
||||||
|
.cookie(InternalValue.cookieName, token)
|
||||||
|
.when()
|
||||||
|
.get("/test-gateway")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.body(is("user123")); // Resource nhận được userId từ Header do Filter gắn vào
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName("Nên trả về 401 nếu JWT sai chữ ký")
|
||||||
|
public void testInvalidSignature() {
|
||||||
|
given()
|
||||||
|
.cookie(InternalValue.cookieName, "invalid.token.string")
|
||||||
|
.when()
|
||||||
|
.get("/test-gateway")
|
||||||
|
.then()
|
||||||
|
.statusCode(401)
|
||||||
|
.body(containsString("InvalidToken"));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
@DisplayName(
|
||||||
|
"Nên đóng gói X-Internal headers từ service thành JWT gửi về client"
|
||||||
|
)
|
||||||
|
public void testResponseFilterConvertsHeadersToJwt() throws Exception {
|
||||||
|
String token = createTestToken("user123");
|
||||||
|
|
||||||
|
Response response = given()
|
||||||
|
.cookie(InternalValue.cookieName, token)
|
||||||
|
.when()
|
||||||
|
.post("/test-gateway/update")
|
||||||
|
.then()
|
||||||
|
.statusCode(200)
|
||||||
|
.cookie(InternalValue.cookieName) // Kiểm tra có Set-Cookie trả về không
|
||||||
|
.header("X-Internal-newStatus", nullValue()) // Header nội bộ phải bị xóa khỏi response
|
||||||
|
.extract()
|
||||||
|
.response();
|
||||||
|
|
||||||
|
String setCookie = response.getHeader("Set-Cookie");
|
||||||
|
assertTrue(setCookie.contains("HttpOnly"));
|
||||||
|
assertTrue(setCookie.contains("SameSite=Strict"));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
package com.drinkool.utils;
|
||||||
|
|
||||||
|
import jakarta.ws.rs.*;
|
||||||
|
import jakarta.ws.rs.core.Context;
|
||||||
|
import jakarta.ws.rs.core.HttpHeaders;
|
||||||
|
import jakarta.ws.rs.core.Response;
|
||||||
|
|
||||||
|
@Path("/test-gateway")
|
||||||
|
public class TestResource {
|
||||||
|
|
||||||
|
@GET
|
||||||
|
public Response getHeaders(@Context HttpHeaders headers) {
|
||||||
|
// Trả về header nội bộ để chúng ta kiểm tra xem Filter có gắn đúng không
|
||||||
|
String userId = headers.getHeaderString("X-Internal-userId");
|
||||||
|
return Response.ok().entity(userId != null ? userId : "not-found").build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@POST
|
||||||
|
@Path("/update")
|
||||||
|
public Response updateInternal(
|
||||||
|
@HeaderParam("X-Internal-userId") String userId
|
||||||
|
) {
|
||||||
|
// Giả lập service trả về header nội bộ để Gateway đóng gói vào JWT
|
||||||
|
return Response.ok("updated")
|
||||||
|
.header("X-Internal-newStatus", "active")
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
# Quarkus
|
||||||
|
quarkus.http.host=0.0.0.0
|
||||||
|
|
||||||
|
# JWT
|
||||||
|
gateway.jwt.secret=1uZk07Hqu1316z9YqrcSGPTTJDhMWU1ZhFSLBrDQvmU=
|
||||||
@@ -44,14 +44,6 @@ spec:
|
|||||||
requests:
|
requests:
|
||||||
cpu: "100m"
|
cpu: "100m"
|
||||||
memory: "256Mi"
|
memory: "256Mi"
|
||||||
volumeMounts:
|
|
||||||
- name: keys-volume
|
|
||||||
mountPath: "/etc/keys"
|
|
||||||
readOnly: true
|
|
||||||
volumes:
|
|
||||||
- name: keys-volume
|
|
||||||
secret:
|
|
||||||
secretName: jwt-keys
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
@@ -88,6 +80,11 @@ spec:
|
|||||||
env:
|
env:
|
||||||
- name: QUARKUS_REST_CLIENT_ACCOUNT_URL
|
- name: QUARKUS_REST_CLIENT_ACCOUNT_URL
|
||||||
value: "http://account-service"
|
value: "http://account-service"
|
||||||
|
- name: GATEWAY_JWT_SECRET
|
||||||
|
valueFrom:
|
||||||
|
secretKeyRef:
|
||||||
|
name: jwt-secret
|
||||||
|
key: secret
|
||||||
resources:
|
resources:
|
||||||
limits:
|
limits:
|
||||||
cpu: "500m"
|
cpu: "500m"
|
||||||
@@ -95,14 +92,6 @@ spec:
|
|||||||
requests:
|
requests:
|
||||||
cpu: "100m"
|
cpu: "100m"
|
||||||
memory: "256Mi"
|
memory: "256Mi"
|
||||||
volumeMounts:
|
|
||||||
- name: keys-volume
|
|
||||||
mountPath: "/etc/keys"
|
|
||||||
readOnly: true
|
|
||||||
volumes:
|
|
||||||
- name: keys-volume
|
|
||||||
secret:
|
|
||||||
secretName: jwt-keys
|
|
||||||
---
|
---
|
||||||
apiVersion: v1
|
apiVersion: v1
|
||||||
kind: Service
|
kind: Service
|
||||||
|
|||||||
+2
-1
@@ -1,7 +1,8 @@
|
|||||||
package com.drinkool;
|
package com.drinkool;
|
||||||
|
|
||||||
public class Jwt {
|
public class InternalValue {
|
||||||
|
|
||||||
|
public static final String cookieName = "auth";
|
||||||
public static final String headerId = "X-Internal-";
|
public static final String headerId = "X-Internal-";
|
||||||
public static final String userId = headerId + "UserId";
|
public static final String userId = headerId + "UserId";
|
||||||
public static final String role = headerId + "Role";
|
public static final String role = headerId + "Role";
|
||||||
Reference in New Issue
Block a user