This commit is contained in:
+25
-23
@@ -1,6 +1,7 @@
|
||||
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.*;
|
||||
@@ -27,7 +28,9 @@ public class InternalHeaderGatewayFilter {
|
||||
}
|
||||
|
||||
@ServerRequestFilter(preMatching = true)
|
||||
public void handleRequestHeaders(ContainerRequestContext requestContext) {
|
||||
public Uni<Response> handleRequestHeaders(
|
||||
ContainerRequestContext requestContext
|
||||
) {
|
||||
requestContext
|
||||
.getHeaders()
|
||||
.keySet()
|
||||
@@ -40,9 +43,10 @@ public class InternalHeaderGatewayFilter {
|
||||
Map<String, Cookie> cookies = requestContext.getCookies();
|
||||
Cookie authCookie = cookies.get(InternalValue.cookieName);
|
||||
|
||||
try {
|
||||
if (authCookie == null) return;
|
||||
if (authCookie == null) return Uni.createFrom().nullItem();
|
||||
|
||||
return Uni.createFrom().item(() -> {
|
||||
try {
|
||||
String token = authCookie.getValue();
|
||||
|
||||
JwtConsumer jwtConsumer = new JwtConsumerBuilder()
|
||||
@@ -62,12 +66,13 @@ public class InternalHeaderGatewayFilter {
|
||||
}
|
||||
});
|
||||
} catch (Exception e) {
|
||||
requestContext.abortWith(
|
||||
Response.status(Response.Status.UNAUTHORIZED)
|
||||
return Response.status(Response.Status.UNAUTHORIZED)
|
||||
.entity("InvalidToken")
|
||||
.build()
|
||||
);
|
||||
.build();
|
||||
}
|
||||
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
@ServerResponseFilter(priority = Priorities.USER + 100)
|
||||
@@ -75,20 +80,24 @@ public class InternalHeaderGatewayFilter {
|
||||
JwtClaims claims = new JwtClaims();
|
||||
boolean hasInternalData = false;
|
||||
|
||||
Map<String, List<Object>> headers = responseContext.getHeaders();
|
||||
Set<String> headerNames = new HashSet<>(
|
||||
responseContext.getHeaders().keySet()
|
||||
);
|
||||
MultivaluedMap<String, Object> headers = responseContext.getHeaders();
|
||||
|
||||
for (String key : headerNames) {
|
||||
List<String> keysToRemove = new ArrayList<>();
|
||||
|
||||
for (String key : headers.keySet()) {
|
||||
if (key.toLowerCase().startsWith(InternalValue.headerId.toLowerCase())) {
|
||||
Object value = headers.get(key).get(0);
|
||||
|
||||
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) {
|
||||
@@ -113,14 +122,7 @@ public class InternalHeaderGatewayFilter {
|
||||
.maxAge(3600)
|
||||
.build();
|
||||
|
||||
responseContext.getHeaders().add("Set-Cookie", jwtCookie);
|
||||
|
||||
responseContext
|
||||
.getHeaders()
|
||||
.keySet()
|
||||
.removeIf(key ->
|
||||
key.toLowerCase().startsWith(InternalValue.cookieName)
|
||||
);
|
||||
headers.add(HttpHeaders.SET_COOKIE, jwtCookie);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
|
||||
+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=
|
||||
Reference in New Issue
Block a user