본 포스팅은 MSA 공부 간 작성한 포스팅 입니다.
아래 포스팅을 먼저 보고 오시면 이해에 도움이 되시니 참고 부탁드립니다.
https://byunsw4.tistory.com/34
1. 인증 처리를 수행하는 API Gateway
MSA 구조에서 API Gateway 는 마이크로 서비스 간의 routing 처리 외에도 마이크로 서비스에서 공통적으로 처리해야하는 기능을 수행하게 할 수 있습니다. 그 중 하나가 바로 "인증(Authorization)" 입니다.
API Gateway 에 인증 기능을 추가하면 아래와 같은 장점을 얻을 수 있습니다.
- 공통 인증 로직을 한 곳에서 처리할 수 있다.
- 마이크로 서비스에는 비지니스 로직만 구현하면 된다.
이번 포스팅에서 적용해볼 인증 방식은 JWT로 유명한 토큰 인증 방식 입니다.
API Gateway 에서 JWT 인증을 수행하는 경우, 인증 절차는 아래와 같이 처리됩니다.
모든 클라이언트의 요청은 API Gateway 를 통과하여 인증이 필요한지 필요 없는지, 필요하면 토큰을 통해 사용자 정보를 parsing 해서 마이크로 서비스로 요청을 다시 보내는 흐름으로 진행됩니다.
2. API Gateway 에 적용할 Filter 생성하기
Spring Cloud Gateway 에 Filter 를 적용하기 위해선 GatewayFilter interface 를 implements 받아 filter() 메서드를 override 합니다.
@Component
public class JwtAuthorizationFilter implements GatewayFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
return null;
}
}
새로 만든 Filter 를 Gateway Routing 에 적용하기 위해서는 아래와 같이 적용하고 싶은 route 규칙에 filter 를 추가하면 됩니다.
@RequiredArgsConstructor
@Configuration
public class GatewayConfiguration {
private final JwtAuthorizationFilter jwtAuthorizationFilter;
@Bean
public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
return builder.routes()
.route("user-service-1", predicateSpec -> predicateSpec
.path("/sign-up","/login")
.and().method(HttpMethod.POST)
.filters(gatewayFilterSpec -> gatewayFilterSpec
.removeRequestHeader(HttpHeaders.COOKIE)
)
.uri("lb://USER-SERVICE")
)
.route("user-service-2", predicateSpec -> predicateSpec
.path("/users/**")
.filters(gatewayFilterSpec -> gatewayFilterSpec
.removeRequestHeader(HttpHeaders.COOKIE)
.filter(jwtAuthorizationFilter)
)
.uri("lb://USER-SERVICE")
)
.route("main-service", predicateSpec -> predicateSpec
.path("/**")
.filters(gatewayFilterSpec -> gatewayFilterSpec
.removeRequestHeader(HttpHeaders.COOKIE)
.filter(jwtAuthorizationFilter)
)
.uri("lb://MAIN-SERVICE")
)
.build();
}
}
위 설정은 사용자 인증이 필요없는 /login(로그인) 과 /sign-up(회원가입) 은 인증 필터를 적용하지 않고, 나머지 요청에 대해서는 인증 필터를 적용하는 routing 설정입니다.
3. JWT 방식의 인증 정보 처리
그렇다면, 위에서 만든 GatewayFilter 에 JWT 방식을 추가해보겠습니다.
일단, jwt 구현을 위해 아래 의존성을 추가했습니다.
``` build.gradle
// ... 이상 생략
dependencies {
// -- 중략 -- //
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
}
// ... 이하 생략
Jwt 에 대한 자세한 내용은 아래 jwt 공식 홈페이지를 참고해주세요!
출처 - https://jwt.io/
위에서 추가해준 jwt 를 통해 구현된 JwtAuthorizationFilter 입니다.
import static shoppingmall.apigateway.AuthorizationConstants.*;
@RequiredArgsConstructor
@Component
public class JwtAuthorizationFilter implements GatewayFilter {
@Value("${auth.jwt.key}")
private String key;
private final ObjectMapper objectMapper;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
log.info("JwtAuthorizationFilter begin");
try{
List<String> authorizations = getAuthorizations(exchange);
if(isNotExistsAuthorizationHeader(authorizations)) {
throw new NotExistsAuthorization();
}
String authorization = authorizations.stream()
.filter(this::isBearerType)
.findFirst()
.orElseThrow(NotExistsAuthorization::new);
String jwtToken = parseAuthorizationToken(authorization);
if(isValidateExpire(jwtToken)) {
throw new AccessTokenExpiredException();
}
exchange.getRequest().mutate().header(X_GATEWAY_HEADER, getSubjectOf(jwtToken));
return chain.filter(exchange);
} catch(NotExistsAuthorization e1) {
return sendErrorResponse(exchange, 701, e1);
} catch(AccessTokenExpiredException e2) {
return sendErrorResponse(exchange, 702, e2);
} catch(Exception e3){
return sendErrorResponse(exchange, 999, e3);
}
}
private Mono<Void> sendErrorResponse(ServerWebExchange exchange, int errorCode, Exception e) {
try {
ErrorResponse errorResponse = new ErrorResponse(errorCode, e.getMessage());
String errorBody = objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(errorResponse);
ServerHttpResponse response = exchange.getResponse();
response.setStatusCode(HttpStatus.UNAUTHORIZED);
response.getHeaders().setContentType(MediaType.APPLICATION_JSON);
DataBuffer buffer = response.bufferFactory().wrap(errorBody.getBytes(StandardCharsets.UTF_8));
return response.writeWith(Flux.just(buffer));
} catch (JsonProcessingException ex) {
throw new RuntimeException(ex);
}
}
private boolean isBearerType(String authorization) {
return authorization.startsWith(AUTH_TYPE);
}
private List<String> getAuthorizations(ServerWebExchange exchange) {
ServerHttpRequest request = exchange.getRequest();
return request.getHeaders().get(HttpHeaders.AUTHORIZATION);
}
private String parseAuthorizationToken(String authorization) {
return authorization.replace(AUTH_TYPE, "").trim();
}
private boolean isNotExistsAuthorizationHeader(List<String> authorizations) {
return authorizations == null || authorizations.isEmpty();
}
private String getSubjectOf(String jwtToken) {
return Jwts.parser().verifyWith(secretKey())
.build()
.parseSignedClaims(jwtToken)
.getPayload()
.getSubject();
}
private boolean isValidateExpire(String jwtToken) {
Date expiration = Jwts.parser().verifyWith(secretKey())
.build()
.parseSignedClaims(jwtToken)
.getPayload()
.getExpiration();
return expiration.before(new Date());
}
private SecretKey secretKey() {
return Keys.hmacShaKeyFor(key.getBytes(StandardCharsets.UTF_8));
}
record ErrorResponse(int code, String message){}
}
Gateway에서 요청이 들어오면 마이크로 서비스로 요청을 routing 하기 전에 @Override 가 붙은 filter() 메서드를 실행하게 됩니다.
요청에 포함된 jwt 값이 정상적으로 parsing 되고 만료시간 전이라 사용이 가능하면, token 을 만들 때 사용한 사용자의 고유 정보를 parsing 하여 request header에 추가해줍니다. 그 후에 return chain.filter(exchange); 를 통해 다음에 실행될 filter 를 실행하게 되고, filter 를 모두 실행하게 되면 마이크로 서비스로 요청이 전달됩니다.
그 외에 어떠한 이유로 filter() 메서드에서 예외가 발생하면, sendErrorResponse() 를 통해 이후에 동작할 Filter 가 실행되지 않고, 클라이언트에게 예외 응답이 return 되는 형태입니다.
4. 결과 테스트
현재 구현된 API Gateway 는 3가지의 Routing Pattern 을 가집니다.
- /login, /sign-up -> 별도의 filter 가 적용되지 않는 사용자 마이크로 서비스에 대한 요청
- /users/** -> JwtAuthorizationFilter 가 적용되는 사용자 마이크로 서비스에 대한 요청
- /** -> JwtAuthorizationFilter 가 적용되는 메인 마이크로 서비스에 대한 요청
위 3가지 요청에 대한 테스트를 postman 으로 해보면 아래와 같은 결과를 얻게 됩니다.
4-1. /sign-up, /login -> JwtAuthorizationFilter 동작 안함
위 2가지의 요청에 대해서는 JwtAuthorizationFilter 를 처리하지 않았기 때문에, Filter 시작할 때 찍어놓은 log 가 출력되지 않습니다.
4-2. /users - JwtAuthorizationFilter 동작 함
위 요청에 대해서는 JwtAuthorizationFilter 가 동작할 것이기 때문에 Filter에 찍어놓은 log 가 출력됩니다.
4-3. 그 외 다른 요청 - JwtAuthorizationFilter 동작 함
앞선 두 분류의 요청은 사용자 마이크로 서비스에 대한 요청이었다면, 이번 요청은 그 외의 메인 마이크로 서비스 요청으로 테스트 해보겠습니다. 마찬가지로 JwtAuthorizationFilter 가 동작하기 때문에, 인증 정보를 추가한 상태로 요청을 보내봐야 합니다.
참고 및 출처.
https://github.com/dongha-byun/springboot-shoppingmall-api-gateway
https://spring.io/projects/spring-cloud-gateway
'DDD&MSA' 카테고리의 다른 글
[MSA] Feign Client 테스트 작성기 - wireMockServer 사용기 (0) | 2023.11.14 |
---|---|
[MSA] Feign Client - 마이크로 서비스 간 통신 구현하기 (0) | 2023.11.08 |
[MSA] API Gateway - 마이크로 서비스 Routing 처리하기(feat. Spring Cloud Gateway) (0) | 2023.09.28 |
[MSA] Micro Service Architecture(MSA) 시작하기 - Eureka Server & Client (0) | 2023.09.10 |
[DDD] CQRS - Command 와 Query 의 분리 (0) | 2023.09.07 |
댓글