GlobalFilter 또는 GatewayFilter 인터페이스를 구현하고, filter 메서드를 오버라이드해야 합니다.GlobalFilter 또는 GatewayFilter 인터페이스를 구현하고, filter 메서드를 오버라이드해야 합니다.Mono는 리액티브 프로그래밍에서 0 또는 1개의 데이터를 비동기적으로 처리합니다.Mono<Void>는 아무 데이터도 반환하지 않음을 의미합니다.ServerWebExchange는 HTTP 요청과 응답을 캡슐화한 객체입니다.exchange.getRequest()로 HTTP 요청을 가져옵니다.exchange.getResponse()로 HTTP 응답을 가져옵니다.GatewayFilterChain은 여러 필터를 체인처럼 연결합니다.chain.filter(exchange)는 다음 필터로 요청을 전달합니다.then 메서드를 사용할 필요가 없습니다. @Component
public class PreFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
// 요청 로깅
System.out.println("Request: " + exchange.getRequest().getPath());
return chain.filter(exchange);
}
@Override
public int getOrder() { // 필터의 순서를 지정합니다.
return -1; // 필터 순서를 가장 높은 우선 순위로 설정합니다.
}
}
chain.filter(exchange)를 호출하여 다음 필터를 실행한 후, then 메서드를 사용하여 응답이 완료된 후에 실행할 작업을 정의합니다.@Component
public class PostFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
// 응답 로깅
System.out.println("Response Status: " + exchange.getResponse().getStatusCode());
}));
}
@Override
public int getOrder() {
return -1;
}
}
Spring Boot2에서는 Zuul을 사용한다고 한다. 기본적인 구조만 알아가자
build.gradle 파일 예시:
dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-zuul'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
}
@SpringBootApplication
@EnableZuulProxy
public class ApiGatewayApplication {
public static void main(String[] args) {
SpringApplication.run(ApiGatewayApplication.class, args);
}
}
application.yml 파일에서 라우팅 설정을 정의할 수 있습니다.
zuul:
routes:
users-service:
path: /users/**
serviceId: users-service
orders-service:
path: /orders/**
serviceId: orders-service
eureka:
client:
service-url:
defaultZone: http://localhost:8761/eureka/
@Component
public class PreFilter extends ZuulFilter {
@Override
public String filterType() {
return "pre";
}
@Override
public int filterOrder() {
return 1;
}
@Override
public boolean shouldFilter() {
return true;
}
@Override
public Object run() throws ZuulException {
RequestContext ctx = RequestContext.getCurrentContext();
HttpServletRequest request = ctx.getRequest();
// 요청 로깅
System.out.println(String.format("%s request to %s", request.getMethod(), request.getRequestURL().toString()));
return null;
}
}
💡 클라우드 게이트웨이 + 유레카 + Order 인스턴스(1개) + Product 인스턴스(2개) 로 진행해봅니다.

OrderController.java
@RestController
@RequestMapping("/order")
public class OrderController {
@GetMapping
public String getOrder() {
return "Order details";
}
}
Reactive Gateway, Spring Boot Actuator, Eureka Discovery Client, Lombok, Spring Web을 dependency에서 추가하자CustomPreFilter.java
@Component
public class CustomPreFilter implements GlobalFilter, Ordered {
private static final Logger logger = Logger.getLogger(CustomPreFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest response = exchange.getRequest();
logger.info("Pre Filter: Request URI is " + response.getURI());
return chain.filter(exchange);
}
@Override
public int getOrder() {
return Ordered.HIGHEST_PRECEDENCE;
}
}
CustomPostFilter.java
@Component
public class CustomPostFilter implements GlobalFilter, Ordered {
private static final Logger logger = Logger.getLogger(CustomPostFilter.class.getName());
@Override
public Mono<Void> filter(ServerWebExchange exchange, org.springframework.cloud.gateway.filter.GatewayFilterChain chain) {
return chain.filter(exchange).then(Mono.fromRunnable(() -> {
ServerHttpResponse response = exchange.getResponse();
logger.info("Post Filter: Response status code is " + response.getStatusCode());
}));
}
@Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
application.properties
spring.application.name=gateway-service
server.port=19091
spring.main.web-application-type=reactive
spring.cloud.gateway.routes[0].id=order-service
spring.cloud.gateway.routes[0].uri=lb://order-service
spring.cloud.gateway.routes[0].predicates[0]=Path=/order/**
spring.cloud.gateway.routes[1].id=product-service
spring.cloud.gateway.routes[1].uri=lb://product-service
spring.cloud.gateway.routes[1].predicates[0]=Path=/product/**
spring.cloud.gateway.routes[2].id=auth-service
spring.cloud.gateway.routes[2].uri=lb://auth-service
spring.cloud.gateway.routes[2].predicates[0]=Path=/auth/signIn
spring.cloud.gateway.discovery.locator.enabled=true
eureka.client.service-url.defaultZone=http://localhost:19090/eureka/
service.jwt.secret-key=401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1





CustomPreFilter는 게이트웨이로 들어오는 HTTP 요청의 URI를 기록하며, 이 경우 요청된 URI는 http://localhost:19091/order였습니다. 그 후 요청이 처리되고 난 뒤, CustomPostFilter는 반환된 응답의 상태 코드를 기록했으며, 이 로그에서는 응답이 성공적으로 처리되었음을 나타내는 200 OK 상태 코드가 반환되었습니다.
헤더, 페이로드, 서명클라우드 게이트웨이의 Pre 필터에서 JWT 인증을 진행해봅니다.
우선 “스프링 클라우드 게이트웨이”에서 학습한 모든 프로젝트를 복사하여 사용하겠습니다.
여기에 Auth Service 를 생성하여 로그인 기능을 아주 간단하게 구현하겠습니다.
클라우드 게이트웨이에 Pre 필터를 하나 더 생성하여 로그인을 체크 하겠습니다.

생성한 토큰을 전달받아 다시 로그인에 사용해야 하므로 Postman 준비하기
Spring Web, Eureka Discovery Client, Lombok, Spring Boot Actuator, Spring Security를 dependency로 추가하자implementation 'io.jsonwebtoken:jjwt:0.12.6' jwt는 build.gradle에 직접 추가하자!!application.properties
spring.application.name=auth-service
eureka.client.service-url.defaultZone=http://localhost:19090/eureka/
service.jwt.access-expiration=3600000
service.jwt.secret-key=401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1
server.port=19095
AuthConfig.java
@Configuration
@EnableWebSecurity
public class AuthConfig {
// SecurityFilterChain 빈을 정의합니다. 이 메서드는 Spring Security의 보안 필터 체인을 구성합니다.
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// CSRF 보호를 비활성화합니다. CSRF 보호는 주로 브라우저 클라이언트를 대상으로 하는 공격을 방지하기 위해 사용됩니다.
.csrf(csrf -> csrf.disable())
// 요청에 대한 접근 권한을 설정합니다.
.authorizeRequests(authorize -> authorize
// /auth/signIn 경로에 대한 접근을 허용합니다. 이 경로는 인증 없이 접근할 수 있습니다.
.requestMatchers("/auth/signIn").permitAll()
// 그 외의 모든 요청은 인증이 필요합니다.
.anyRequest().authenticated()
)
// 세션 관리 정책을 정의합니다. 여기서는 세션을 사용하지 않도록 STATELESS로 설정합니다.
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
);
// 설정된 보안 필터 체인을 반환합니다.
return http.build();
}
}
AuthService.java
@Service
public class Authservice {
@Value("${spring.application.name}")
private String issuer;
@Value("${service.jwt.access-expiration}")
private Long accessExpiration;
private final SecretKey secretKey;
// Base64 URL 인코딩된 비밀 키를 디코딩하여 HMAC-SHA 알고리즘에 적합한 SecretKey 객체를 생성합니다.
// @param secretKey는 Base64 URL 인코딩된 비밀 키
public Authservice(@Value("${service.jwt.secret-key}")String secretKey) {
this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
}
public String createAccessToken(String userId) {
return Jwts.builder()
.claim("user_id",userId)
.claim("role","ADMIN")
.issuer(issuer)
.issuedAt(new Date(System.currentTimeMillis()))
.expiration(new Date(System.currentTimeMillis()+accessExpiration))
// SecretKey를 사용하여 HMAC-SHA512 알고리즘으로 서명
.signWith(secretKey, SignatureAlgorithm.HS512)
.compact();
}
}
AuthController.java
@RestController
@RequiredArgsConstructor
public class AuthController {
private final Authservice authservice;
@GetMapping("/auth/signIn")
public ResponseEntity<?> createAuthenticationToken(@RequestParam String user_id){
return ResponseEntity.ok(new AuthResponse(authservice.createAccessToken(user_id)));
}
@Getter
@AllArgsConstructor
@NoArgsConstructor
static class AuthResponse {
private String access_token;
}
}
build.gradle에 jwt dependency 추가하기application.properties는 6.7.4 게이트웨이에 작성된 것으로 교체하기!!LocalJwtAuthenticationFilter.java
@Slf4j
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {
@Value("${service.jwt.secret-key}")
private String secretKey;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (path.equals("/auth/signIn")) {
return chain.filter(exchange); // /signIn 경로는 필터를 적용하지 않음
}
String token = extractToken(exchange);
if (token == null || !validateToken(token)) {
exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
return exchange.getResponse().setComplete();
}
return chain.filter(exchange);
}
private String extractToken(ServerWebExchange exchange) {
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return authHeader.substring(7);
}
return null;
}
private boolean validateToken(String token) {
try {
SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
Jws<Claims> claimsJws = Jwts.parser()
.verifyWith(key)
.build().parseSignedClaims(token);
log.info("#####payload :: " + claimsJws.getPayload().toString());
return true;
} catch (Exception e) {
return false;
}
}
}


게이트웨이에서 로그인을 요청하여 토큰을 발급받아보자 하지만 위처럼 503 에러가 나왔다. AuthService를 실행하지 않았거나 실행 순서가 다르면 서비스를 찾지 못하는 것이다. 순서를 잘 지키자~

순서를 지키니 올바른 access_token을 받게 되었다 이제 상품을 요청할때 header에 토큰을 넣어보자

gateway를 통해 jwt 토큰을 이용하여 로그인을 완료하였다.
오늘은 어제만큼 실패한 내용은 없었지만, jwt 토큰 생성과 검증 로직을 다시한번 살펴보고 나중에 jwt 토큰 구현할때는 deprecate 된 함수를 사용하지 않고 docs를 찾아보면서 새로운 함수들로 구현해보자는 생각을 가지게 되었습니다.