이번 포스팅은 API Gateway 부분을 구현할 차례다.
지난 포스팅에서 User와 Auth간의 소통을 구현했었는데
별개의 서버에서 API를 불러다 사용하는 방식으로 구현했었다.
그렇지만 MSA는 서버간의 직접 소통을 권하지는 않는다.
이 문제를 해결하기 위해서 API Gateway를 설정하고 모든 서비스는 API Gateway를 통하여 소통하도록 구현해야 한다.
먼저 gateway에 JwtUtil을 구현한다.
Auth-Service 에서 받아온 로그인을 위한 인증 토큰을 사용하기 위해 필요한 구성이다.
@Component
public class JwtUtil {
// TODO : Secret Key 환경 변수로 수정
private final String SECRET_KEY = "2~~";
public String extractEmail(String token) {
return Jwts.parser()
.setSigningKey(SECRET_KEY.getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token)
.getBody()
.getSubject();
}
public boolean validateToken(String token) {
try {
Jwts.parser()
.setSigningKey(SECRET_KEY.getBytes(StandardCharsets.UTF_8))
.parseClaimsJws(token);
return true;
} catch (JwtException | IllegalArgumentException e) {
return false;
}
}
}
지금은 간단한 구현이기 때문에 SECRET_KEY 자체를 하드코딩 해두었다.
그 다음으로는 정보 추출과 토큰 검증이다.
@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter implements WebFilter {
private final JwtUtil jwtUtil;
private static final List<String> EXCLUDED_PATHS = List.of("/v1/auth/login", "/v1/users/email", "/v1/users/signup");
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
String path = exchange.getRequest().getURI().getPath();
if (EXCLUDED_PATHS.contains(path)) {
return chain.filter(exchange);
}
String authHeader = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
return onError(exchange, "Missing or Invalid Authorization Header", HttpStatus.UNAUTHORIZED);
}
String token = authHeader.substring(7);
if (!jwtUtil.validateToken(token)) {
return onError(exchange, "Invalid JWT Token", HttpStatus.UNAUTHORIZED);
}
// userId 추출
String userId = jwtUtil.extractEmail(token);
// userId를 헤더에 추가해서 downstream service로 넘겨줌
ServerWebExchange mutatedExchange = exchange.mutate()
.request(builder -> builder.header("X-User-Id", userId))
.build();
return chain.filter(mutatedExchange);
}
private Mono<Void> onError(ServerWebExchange exchange, String err, HttpStatus status) {
exchange.getResponse().setStatusCode(status);
return exchange.getResponse().setComplete();
}
}
전체 코드는 이러하며 각 코드가 하는 일을 조금 살펴보도록 하겠다.
일단 MSA 관점에서 API Gateway 레이어에서 인증 처리는 대표적인 패턴이다.
WebFilter는 Spring WebFlux의 비동기 논블로킹 필터로서, Gateway의 필터 체인에서 모든 요청 전처리 역할을 수행한다.
이 말은 다음과 같이 정리할 수 있다.
ServerWebExchange 로 요청과 응답을 통째로 다룰 수 있어 헤더 수정, Body 조작 등 유연성 확보ServerWebExchange는 불변 객체로 mutate로 복제 후 헤더에 X-User-Id라는 헤더를 추가하여
다른 서비스로 넘겨주는 방식이다.
ServerWebExchange mutatedExchange = exchange.mutate()
.request(builder -> builder.header("X-User-Id", userId))
.build();
@Configuration
public class SecurityConfig {
private final JwtAuthenticationFilter jwtAuthenticationFilter;
public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
}
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) throws Exception {
http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.authorizeExchange(exchanges -> exchanges
.pathMatchers("/v1/auth/**").permitAll()
.pathMatchers("/v1/users/**").permitAll()
.pathMatchers("/logout").permitAll()
.pathMatchers("/error").permitAll()
.anyExchange().authenticated()
)
.addFilterAt(jwtAuthenticationFilter, SecurityWebFiltersOrder.AUTHENTICATION);
return http.build();
}
}
평소에 사용하던 Security Config 와 비슷하지만
authorizeExchange 라던가 pathMatchers 처럼 경로를 허용하는 방식등 기존의
Web MVC 패턴이 아닌 경로 형태로 진행된다.
위와 같이 구현하면 다른 서비스에서도 인증이 가능하다.
일단 X-User-Id 헤더와 같은 경우 Client에서의 직접적인 입력을 반드시 막아야 한다.
Gateway에서 입력해야 인증객체가 설정되는 것이기 때문에 외부에서 입력할 경우 정상적으로 작동하지 않을 가능성이 높으며 탈취와 같은 위험이 존재할 수 있다.
이번 포스팅은 조금 공부가 더 필요할 것 같기 때문에 다음 포스팅에서는 WebFlux, Mono등 조금 더 용어나 그 외의 주요 특징들을 다루는 글을 적도록 하겠다.