Skip to content

Commit 8c331e9

Browse files
committed
added security layer in api-gateway
1 parent 75ad2e8 commit 8c331e9

8 files changed

Lines changed: 187 additions & 14 deletions

File tree

‎api-gateway/pom.xml‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,23 @@
2121
<groupId>org.springframework.cloud</groupId>
2222
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
2323
</dependency>
24+
<dependency>
25+
<groupId>io.jsonwebtoken</groupId>
26+
<artifactId>jjwt-api</artifactId>
27+
<version>0.11.5</version>
28+
</dependency>
29+
<dependency>
30+
<groupId>io.jsonwebtoken</groupId>
31+
<artifactId>jjwt-impl</artifactId>
32+
<version>0.11.5</version>
33+
<scope>runtime</scope>
34+
</dependency>
35+
<dependency>
36+
<groupId>io.jsonwebtoken</groupId>
37+
<artifactId>jjwt-jackson</artifactId>
38+
<version>0.11.5</version>
39+
<scope>runtime</scope>
40+
</dependency>
2441
</dependencies>
2542

2643
<build>
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
package com.streamseat.apigateway.filter;
2+
3+
import org.springframework.http.HttpHeaders; // Use Spring's HttpHeaders
4+
import com.streamseat.apigateway.utils.JwtUtil;
5+
import org.springframework.beans.factory.annotation.Autowired;
6+
import org.springframework.cloud.gateway.filter.GatewayFilter;
7+
import org.springframework.cloud.gateway.filter.factory.AbstractGatewayFilterFactory;
8+
import org.springframework.http.server.reactive.ServerHttpRequest;
9+
import org.springframework.stereotype.Component;
10+
11+
@Component
12+
public class AuthenticationFilter extends AbstractGatewayFilterFactory<AuthenticationFilter.Config> {
13+
14+
@Autowired
15+
private RouteValidator validator; // Simple class to skip auth for /login or /register
16+
@Autowired
17+
private JwtUtil jwtUtil;
18+
19+
public AuthenticationFilter() {
20+
super(Config.class);
21+
}
22+
23+
@Override
24+
public GatewayFilter apply(Config config) {
25+
return ((exchange, chain) -> {
26+
if (validator.isSecured.test( exchange.getRequest())) {
27+
// 1. Check Header
28+
if (!exchange.getRequest().getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
29+
throw new RuntimeException("Missing Authorization Header");
30+
}
31+
32+
String authHeader = exchange.getRequest().getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
33+
if (authHeader != null && authHeader.startsWith("Bearer ")) {
34+
authHeader = authHeader.substring(7);
35+
}
36+
37+
try {
38+
// 2. Validate
39+
jwtUtil.validateToken(authHeader);
40+
41+
// 3. Extract & Inject Header for internal microservices
42+
String userId = jwtUtil.extractUserId(authHeader);
43+
ServerHttpRequest request = exchange.getRequest()
44+
.mutate()
45+
.header("X-User-Id", userId)
46+
.build();
47+
48+
return chain.filter(exchange.mutate().request(request).build());
49+
50+
} catch (Exception e) {
51+
throw new RuntimeException("Unauthorized access");
52+
}
53+
}
54+
return chain.filter(exchange);
55+
});
56+
}
57+
58+
public static class Config {}
59+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package com.streamseat.apigateway.filter;
2+
3+
import org.springframework.http.server.reactive.ServerHttpRequest;
4+
import org.springframework.stereotype.Component;
5+
6+
import java.util.Arrays;
7+
import java.util.List;
8+
import java.util.function.Predicate;
9+
10+
@Component
11+
public class RouteValidator {
12+
public static final List<String> openApiEndpoints = Arrays.asList(
13+
"/auth/register",
14+
"/auth/token",
15+
"/eureka",
16+
"/api/events" // ADD THIS LINE
17+
);
18+
19+
public Predicate<ServerHttpRequest> isSecured =
20+
request -> openApiEndpoints
21+
.stream()
22+
.noneMatch(uri -> request.getURI().getPath().contains(uri));
23+
}
Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
package com.streamseat.apigateway.utils;
2+
3+
import io.jsonwebtoken.Jwts;
4+
import io.jsonwebtoken.SignatureAlgorithm;
5+
import io.jsonwebtoken.io.Decoders;
6+
import io.jsonwebtoken.security.Keys;
7+
import org.springframework.stereotype.Component;
8+
9+
import java.security.Key;
10+
import java.util.Date;
11+
import java.util.HashMap;
12+
import java.util.Map;
13+
14+
@Component
15+
public class JwtUtil {
16+
// In production, move this to AWS Secrets Manager or environment variables
17+
private static final String SECRET = "5367566B59703373367639792F423F4528482B4D6251655468576D5A71347437";
18+
19+
public void validateToken(final String token) {
20+
Jwts.parserBuilder().setSigningKey(getSignKey()).build().parseClaimsJws(token);
21+
}
22+
23+
public String extractUserId(String token) {
24+
return Jwts.parserBuilder().setSigningKey(getSignKey()).build()
25+
.parseClaimsJws(token).getBody().getSubject();
26+
}
27+
28+
private Key getSignKey() {
29+
byte[] keyBytes = Decoders.BASE64.decode(SECRET);
30+
return Keys.hmacShaKeyFor(keyBytes);
31+
}
32+
33+
public String generateToken(String userName) {
34+
Map<String, Object> claims = new HashMap<>();
35+
return createToken(claims, userName);
36+
}
37+
38+
private String createToken(Map<String, Object> claims, String userName) {
39+
return Jwts.builder()
40+
.setClaims(claims)
41+
.setSubject(userName)
42+
.setIssuedAt(new Date(System.currentTimeMillis()))
43+
.setExpiration(new Date(System.currentTimeMillis() + 1000 * 60 * 30))
44+
.signWith(getSignKey(), SignatureAlgorithm.HS256).compact();
45+
}
46+
}
Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,17 @@
11
spring.application.name=api-gateway
22
server.port=8082
33

4-
# 1. FIX: Use the service name defined in docker-compose
5-
eureka.client.service-url.defaultZone=${EUREKA_URL:http://discovery-server:8761/eureka/}
4+
# 1. Eureka Configuration
5+
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
66

7-
# 2. PRO TIP: Use UPPERCASE for service names in lb://
8-
# Eureka usually registers names in uppercase by default.
7+
# 2. Event Service Route + Security Filter
98
spring.cloud.gateway.routes[0].id=event-service
109
spring.cloud.gateway.routes[0].uri=lb://EVENT-SERVICE
1110
spring.cloud.gateway.routes[0].predicates[0]=Path=/api/events/**
11+
spring.cloud.gateway.routes[0].filters[0]=AuthenticationFilter
1212

13+
# 3. Booking Service Route + Security Filter
1314
spring.cloud.gateway.routes[1].id=booking-service
1415
spring.cloud.gateway.routes[1].uri=lb://BOOKING-SERVICE
1516
spring.cloud.gateway.routes[1].predicates[0]=Path=/api/bookings/**
16-
17-
eureka.instance.prefer-ip-address=true
17+
spring.cloud.gateway.routes[1].filters[0]=AuthenticationFilter
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package com.streamseat.bookingservice.client;
2+
3+
import feign.RequestInterceptor;
4+
import feign.RequestTemplate;
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.web.context.request.RequestContextHolder;
7+
import org.springframework.web.context.request.ServletRequestAttributes;
8+
9+
@Component
10+
public class FeignClientInterceptor implements RequestInterceptor {
11+
@Override
12+
public void apply(RequestTemplate template) {
13+
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
14+
if (attributes != null) {
15+
String userId = attributes.getRequest().getHeader("X-User-Id");
16+
if (userId != null) {
17+
template.header("X-User-Id", userId);
18+
}
19+
}
20+
}
21+
}

‎event-service/src/main/java/com/streamseat/eventservice/service/EventService.java‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,10 @@
44
import com.streamseat.eventservice.dto.SeatType;
55
import com.streamseat.eventservice.model.Event;
66
import com.streamseat.eventservice.repository.EventRepository;
7-
import jakarta.transaction.Transactional;
87
import lombok.RequiredArgsConstructor;
98
import org.springframework.beans.factory.annotation.Autowired;
109
import org.springframework.stereotype.Service;
10+
import org.springframework.transaction.annotation.Transactional;
1111

1212
@Service
1313
@RequiredArgsConstructor

‎event-service/src/main/resources/application.properties‎

Lines changed: 14 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,22 @@ spring.application.name=event-service
22
server.port=8081
33

44
# Eureka Connection
5-
eureka.client.service-url.defaultZone=http://discovery-server:8761/eureka/
6-
spring.datasource.url=${SPRING_DATASOURCE_URL:jdbc:mysql://localhost:3306/stream_seat_db}
7-
spring.datasource.username=${SPRING_DATASOURCE_USERNAME:root}
8-
spring.datasource.password=${SPRING_DATASOURCE_PASSWORD:root}
5+
# FIX: Ensure this is on its own line
6+
eureka.client.service-url.defaultZone=http://localhost:8761/eureka/
7+
# Database Connection
8+
# FIX: Ensure this starts on a new line
9+
spring.datasource.url=jdbc:mysql://localhost:3306/stream_seat_db?createDatabaseIfNotExist=true
10+
spring.datasource.username=root
11+
spring.datasource.password=kesh@v123
12+
spring.jpa.hibernate.ddl-auto=create
13+
14+
# JPA / Hibernate Config
15+
spring.jpa.database-platform=org.hibernate.dialect.MySQLDialect
16+
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
17+
spring.jpa.properties.hibernate.boot.allow_jdbc_metadata_access=false
918

10-
spring.jpa.hibernate.ddl-auto=update
11-
# This helps you see what's happening in the logs
1219
spring.jpa.show-sql=true
1320

14-
# Force Eureka to use 'localhost' instead of the computer's name/IP
21+
# Eureka Instance Config
1522
eureka.instance.hostname=localhost
1623
eureka.instance.prefer-ip-address=true

0 commit comments

Comments
 (0)