최종 시연을 준비하면서 관리자 권한의 시나리오를 추가하게 되었는데, 갑자기 원래 기대하던 응답이 나타나지 않아 의아했다.
문제는 올바른 토큰을 주었음에도 권한 인증 처리가 안된 것이었는데, 원인은 게이트웨이에서 해당 경로를 public path로 취급해 인증 검증 필터를 우회하게 되었던 것이었다. (처음에 public path를 경로가 같을 때 요청 메서드에 따라 분기해줘야한다는 사실을 간과했다.)
인증 필터를 구현하면서 사용했던 Postman 요청을 복사해서 사용하면서, 게이트웨이에서는 토큰을 검증하면서 헤더에 주입해줘야했을 헤더 정보가 Postman 요청 헤더에 자동으로 포함되어있어서 파악이 늦어졌다.
코드래빗은 전 후 상황을 모르니 당연히 인가가 필요하지 않은 경로인 줄 알고 이에 대한 피드백을 주지 못했을 것이다.
실전이었으면 요청 헤더가 주장하는대로 모든 요청을 처리했을 것이라 생각하니 오싹했다.😱
그래도 지금이라도 발견해서 다행이라고 생각한다...
@ConfigurationProperties 를 통해 사용 위치에 주입된다.
jwt:
secret: ${JWT_SECRET}
# 생성 명령: openssl rand -base64 64
public-paths:
# - /api/v1/users/ <- 이거 여기 넣으면 바보
- /api/v1/auth/**
- /actuator/**
- /api/v1/payment/webhook
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
String path = request.getURI().getPath();
HttpMethod method = request.getMethod(); // 추가
// [1] Public Path 매칭 / 회원가입 경로 (POST /users/) → 인증 우회
if (isPublicPath(path) ||
"/api/v1/users".equals(path) && HttpMethod.POST.equals(method)) { // 추가
return chain.filter(exchange);
}
서비스에서만 사용하는 공통모듈을 만들고, 공통모듈에서 응답 형태를 맞춘 BusinessException을 커스텀해 만들어 사용했는데, Gateway는 서비스가 아닌 인프라 성격의 서버라서 공통응답 형태를 맞춰놓지 않았었다.
그래도 예외 처리를 할 때 같은 형태로 나와야한다고 생각해서 공통모듈처럼 예외를 처리하려고 했다.
그런데 생각보다 쉽진 않았다. SpringCloudGateway는 WebFlux 기반이라 기존 서비스 서버에서처럼 @RestControllerAdvice를 사용해 구현할 수 없었다.
간단하게 응답 형태만 고치려고 했는데 이 부분에만 오전 시간을 다 소모해버렸다.😂
WebFlux 방식에서는 다른 방식을 사용해야했는데, Mono 객체가 에러를 가진 채 리턴될 때, 이 에러를 처리할 Handler를 직접 만들어 처리해줘야했다.
해당 헨들러는 org.springframework.boot.web.reactive.error.ErrorWebExceptionHandler를 구현한다.
handle() 메서드를 재정의해 형식에 맞는 예외응답을 반환하도록 수정해야한다.
@RequiredArgsConstructor
public class GatewayExceptionHandler implements ErrorWebExceptionHandler {
private final ObjectMapper objectMapper;
@Override
public Mono<Void> handle(ServerWebExchange exchange, Throwable ex) {
ServerHttpResponse response = exchange.getResponse();
if (response.isCommitted()) {
return Mono.error(ex);
}
...
return response.writeWith(Mono.fromSupplier(() -> {
try {
// 이 부분이 정확히 apiResponse 변수를 직렬화하는지 다시 확인!
byte[] bytes = objectMapper.writeValueAsBytes(apiResponse);
return response.bufferFactory().wrap(bytes);
} catch (Exception e) {
log.error("Error writing response", e);
return response.bufferFactory()
.wrap("{\"success\":false,\"error\":{\"code\":\"INTERNAL_SERVER_ERROR\"}}".getBytes());
}
}));
...
}
}
처음엔 GatewayExceptionHandler를 @Component로 만들어 @Order(-2) 를 사용해서 기본으로 사용되는 예외 처리 핸들러보다 더 먼저 적용되도록 했는데, 응답 형태가 기본 예외처리 핸들러에서 만들어내는 예외 형식으로만 나오고 바뀌지 않았다.
그래서 검색해보니, @Config 설정으로 직접 Bean을 등록하는 방식이 필요할 수 있다고 해서 핸들러 적용 순서는 다음과 같이 적용했다.
@Configuration
public class ErrorConfig {
@Bean
@Primary // 같은 타입의 빈이 여러 개일 때 이 녀석을 1순위로!
@Order(-2) // 기본 핸들러(-1)보다 높은 순위
public ErrorWebExceptionHandler gatewayExceptionHandler(ObjectMapper objectMapper) {
return new GatewayExceptionHandler(objectMapper);
}
}