Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,9 @@ extra["springCloudVersion"] = "2025.1.0"
dependencies {
implementation("org.springframework.boot:spring-boot-starter-webmvc")
implementation("org.springframework.cloud:spring-cloud-starter-gateway-server-webmvc")
implementation("io.jsonwebtoken:jjwt-api:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-impl:0.12.6")
runtimeOnly("io.jsonwebtoken:jjwt-jackson:0.12.6")
implementation("net.logstash.logback:logstash-logback-encoder:8.0")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("io.micrometer:micrometer-registry-prometheus")
Expand Down
1 change: 1 addition & 0 deletions environment/.local.env
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,4 @@ SEARCH_SERVICE_URL=http://devoops-search-service:8080
LOGSTASH_HOST=logstash:5000
ZIPKIN_HOST=zipkin
ZIPKIN_PORT=9411
JWT_SECRET=dGhpcyBpcyBhIHZlcnkgc2VjdXJlIGtleSBmb3IgSFMyNTYgdGhhdCBpcyBhdCBsZWFzdCAyNTYgYml0cyBsb25n
124 changes: 124 additions & 0 deletions src/main/java/com/devoops/gateway/filter/JwtAuthenticationFilter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,124 @@
package com.devoops.gateway.filter;

import com.devoops.gateway.service.JwtService;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletRequestWrapper;
import jakarta.servlet.http.HttpServletResponse;
import org.jspecify.annotations.NonNull;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.filter.OncePerRequestFilter;

import java.io.IOException;
import java.util.*;

@Component
@Order(Ordered.HIGHEST_PRECEDENCE + 10)
public class JwtAuthenticationFilter extends OncePerRequestFilter {

private static final List<String> PUBLIC_PATHS = List.of(
"/api/user/auth/**",
"/api/user/test",
"/actuator/**"
);

private final AntPathMatcher pathMatcher = new AntPathMatcher();
private final JwtService jwtService;

public JwtAuthenticationFilter(JwtService jwtService) {
this.jwtService = jwtService;
}

@Override
protected void doFilterInternal(@NonNull HttpServletRequest request,
@NonNull HttpServletResponse response,
@NonNull FilterChain filterChain) throws ServletException, IOException {
String path = request.getRequestURI();

if (isPublicPath(path)) {
filterChain.doFilter(request, response);
return;
}

String authHeader = request.getHeader("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
sendError(response, "Missing or invalid Authorization header");
return;
}

try {
String token = authHeader.substring(7);
Claims claims = jwtService.parseToken(token);

String userId = jwtService.getUserId(claims);
String role = jwtService.getRole(claims);

if (userId == null || role == null) {
sendError(response, "Invalid token claims");
return;
}

HttpServletRequest wrappedRequest = new HeaderAddingRequestWrapper(request, Map.of(
"X-User-Id", userId,
"X-User-Role", role
));

filterChain.doFilter(wrappedRequest, response);
} catch (JwtException e) {
sendError(response, "Invalid or expired token");
}
}

private boolean isPublicPath(String path) {
return PUBLIC_PATHS.stream().anyMatch(pattern -> pathMatcher.match(pattern, path));
}

private void sendError(HttpServletResponse response, String message) throws IOException {
response.setStatus(HttpStatus.UNAUTHORIZED.value());
response.setContentType("application/json");
response.getWriter().write("{\"status\":401,\"detail\":\"%s\"}".formatted(message));
}

private static class HeaderAddingRequestWrapper extends HttpServletRequestWrapper {
private final Map<String, String> extraHeaders;

public HeaderAddingRequestWrapper(HttpServletRequest request, Map<String, String> extraHeaders) {
super(request);
this.extraHeaders = extraHeaders;
}

@Override
public String getHeader(String name) {
if (extraHeaders.containsKey(name)) {
return extraHeaders.get(name);
}
return super.getHeader(name);
}

@Override
public Enumeration<String> getHeaders(String name) {
if (extraHeaders.containsKey(name)) {
return Collections.enumeration(List.of(extraHeaders.get(name)));
}
return super.getHeaders(name);
}

@Override
public Enumeration<String> getHeaderNames() {
Set<String> names = new LinkedHashSet<>();
Enumeration<String> original = super.getHeaderNames();
while (original.hasMoreElements()) {
names.add(original.nextElement());
}
names.addAll(extraHeaders.keySet());
return Collections.enumeration(names);
}
}
}
36 changes: 36 additions & 0 deletions src/main/java/com/devoops/gateway/service/JwtService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.devoops.gateway.service;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.io.Decoders;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;

import javax.crypto.SecretKey;

@Service
public class JwtService {

private final SecretKey signingKey;

public JwtService(@Value("${jwt.secret}") String secret) {
this.signingKey = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secret));
}

public Claims parseToken(String token) {
return Jwts.parser()
.verifyWith(signingKey)
.build()
.parseSignedClaims(token)
.getPayload();
}

public String getUserId(Claims claims) {
return claims.get("userId", String.class);
}

public String getRole(Claims claims) {
return claims.get("role", String.class);
}
}
3 changes: 3 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,9 @@ logging.level.com.devoops=DEBUG
logging.level.org.springframework.web=INFO
logging.level.org.springframework.cloud.gateway=INFO

# JWT
jwt.secret=${JWT_SECRET:dGhpcyBpcyBhIHZlcnkgc2VjdXJlIGtleSBmb3IgSFMyNTYgdGhhdCBpcyBhdCBsZWFzdCAyNTYgYml0cyBsb25n}

#Zipkin tracing configuration
management.tracing.sampling.probability=1.0
management.tracing.export.zipkin.endpoint=http://${ZIPKIN_HOST:zipkin}:${ZIPKIN_PORT:9411}/api/v2/spans
Expand Down