[식구하자_MSA] Spring Cloud Eureka, Gateway를 통한 인증 처리

이민우·2024년 2월 6일
2

🍀 식구하자_MSA

목록 보기
3/21

지난 포스팅(Jenkins+Docker+MSA+Spring Boot CI/CD 적용)에 이어 MSA 적용을 하면서 CI/CD 파이프라인 구축 후, 해당 포스팅에서는 MSA 서비스를 위해 Spring Cloud Eureka,Gateway를 통한 인증/인가/예외 처리를 구현 할 예정입니다‼️

Spring Cloud, Gateway, Eureka에 대한 사전 지식과 Gateway와 Eureka 설정 방법에 대해서는 저의 이전 포스팅을 참고해주시면 될 것 같습니다.

[MSA]Spring Cloud Gateway& Eureka 개념 및 예제

백엔드 아키텍쳐

현재 MSA를 적용한 설계도에서 Spring Cloud Eureka, Gateway에 대해서 알아보자면

  • Spring Cloud Eureka 서버
    Service Discovery 각 Service 인스턴스 정보를 가지고 있어 Gateway에서 라우팅할 때 서버에 대한 정보가 필요할 때 사용함
  • Spring Cloud Gateway
    각 서비스에 바로 요청이 가지 않고 게이트 웨이 서버를 통해서 라우팅됨
    JWT 와 같은 인증 필터를 두어서 앞단에서 한번에 처리할 수 있음

Eureka Server 구축

1️⃣ 의존성 추가 (gradle 기준)

Spring Boot 2.7.6 버전

ext {
    set('springCloudVersion', "2021.0.4")
}
dependencies {
'''
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-server'
'''
dependencyManagement {
    imports {
        mavenBom "org.springframework.cloud:spring-cloud-dependencies:${springCloudVersion}"
    }
}

}

2️⃣ @EnableEurekaServer

의존성을 추가해줬으면, Spring 메인 클래스에 @EnableEurekaServer 어노테이션을 붙여 Eureka Server임을 알려준다.

@SpringBootApplication
@EnableEurekaServer
public class PlantEurekaApplication {

    public static void main(String[] args) {
        SpringApplication.run(PlantEurekaApplication.class, args);
    }

}

3️⃣ application.yml 설정

server:
  port: 8761 //default가 8761

spring:
  application:
    name: [application 이름]

eureka:
  client:
    register-with-eureka: false #eureka server를 registry에 등록할지 여부
    fetch-registry: false       #registry에 있는 정보들을 가져올지 여부
    service-url:
      defaultZone: http://[본인 ip 주소]:[eureka 포트 번호]/eureka

4️⃣ Eureka Server 기동

Eureka Server을 run 시키고 http://[ip주소]:[eureka 포트 번호]
에 접속해보면 정상적으로 기동이 되면 아래 사진과 같이 나온다.

✅ API Gateway Service 구축

1️⃣ 의존성 추가

spring cloud 의존성은 위의 eureka 서버에 추가한 의존성을 추가하면됩니다!

implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'

2️⃣ application.yml 설정

server:
  port: 8000 

eureka:
  client:
    register-with-eureka: true #eureka server를 registry에 등록할지 여부
    fetch-registry: true       #registry에 있는 정보들을 가져올지 여부
    service-url:
      defaultZone: http://[본인 ip 주소]:[eureka 포트 번호]/eureka
      

spring:
  application:
    name: [application 이름]
  cloud:
    gateway:
      routes:
        - id: plant-service #등록할 마이크로 서비스 이름
          uri: lb://PLANT-SERVICE
          predicates:
            - Path=/plant-service/api/login #엔드포인트
            - Method=POST
          filters:
            - RemoveRequestHeader=Cookie
            - RewritePath=/plant-service/(?<segment>. *) , /$\{segment}
        - id: plant-service
          uri: lb://PLANT-SERVICE
          predicates:
            - Path=/plant-service/**
            - Method=GET,POST,PUT,DELETE
          filters:
            - RemoveRequestHeader=Cookie
            - RewritePath=/plant-service/(?<segment>. *) , /$\{segment}
            - AuthorizationHeaderFilter

routes부분에 대해서 추가로 코드를 나눠서 설명드리자면,

        - id: plant-service
          uri: lb://PLANT-SERVICE
          predicates:
            - Path=/plant-service/**
            - Method=GET,POST,PUT,DELETE
          filters:
            - RemoveRequestHeader=Cookie
            - RewritePath=/plant-service/(?<segment>. *) , /$\{segment}
            - AuthorizationHeaderFilter

보통 Spring Cloud를 이용한 MSA에서는 인증 정보 유효성 검증, 예외를 필터를 통해 Gateway 서버에서 처리합니다. 그래서 gateway 서버에서 AuthorizationHeaderFilter를 만든후에, yml filters부분에 추가해주시면 /plant-service/* 해당 마이크로서비스 모든 엔드포인트에 필터를 거치게 해줘 인증 정보 유효성을 확인하여 진행합니다.

💡 하지만 로그인이나 회원가입, 토큰 재발급 받는 엔드포인트에 관해서는 사용자가 현재 인증된 상태가 아님에도 불구하고, 서비스를 이용해야하므로 필터에서 제외시켜줘야합니다.
 - id: plant-service #등록할 마이크로 서비스 이름
          uri: lb://PLANT-SERVICE
          predicates:
            - Path=/plant-service/api/login #필터에서 제외시킬 엔드포인트
            - Method=POST
          filters:
            - RemoveRequestHeader=Cookie
            - RewritePath=/plant-service/(?<segment>. *) , /$\{segment}

📌 여기서 주의할 점은 필터에서 제외시킬 엔드포인트의 설정을 필터를 적용시킬 설정정보보다 위에 위치시켜 주셔야 해당 사항이 적용됩니다!

3️⃣ AuthorizationHeaderFilter 코드 작성

@Component
@Slf4j
public class AuthorizationHeaderFilter extends AbstractGatewayFilterFactory<AuthorizationHeaderFilter.Config> {
    Environment env;
    public static class Config{
    }
    public AuthorizationHeaderFilter(Environment env) {
        super(Config.class);
        this.env = env;
    }

    @Override
    public GatewayFilter apply(Config config) {
        return ((exchange, chain) -> {
            ServerHttpRequest request = exchange.getRequest();

            if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION)) {
                return OnError(exchange, "로그인이 필요한 서비스입니다.", HttpStatus.UNAUTHORIZED);
            }

            String authorizationHeader = request.getHeaders().get(HttpHeaders.AUTHORIZATION).get(0);
            String jwt = authorizationHeader.replace("Bearer ", "");

            if (!isJwtValid(jwt)) {
                return OnError(exchange, "Jwt token is not vaild", HttpStatus.UNAUTHORIZED);
            }
            return chain.filter(exchange);

        });
    }

    private boolean isJwtValid(String jwt) {
        boolean returnValue = true;
        String username = null;

        try {
            //복호화
            Algorithm algorithm = Algorithm.HMAC256("secretKey".getBytes());
            JWTVerifier verifier = JWT.require(algorithm).build();

            DecodedJWT decodedJWT = verifier.verify(jwt);
            username = decodedJWT.getSubject();
            String[] roles = decodedJWT.getClaim("roles").asArray(String.class);
//            username = Jwts.parser().setSigningKey(env.getProperty("token.secret"))
//                    .parseClaimsJws(jwt).getBody()
//                    .getSubject();
        } catch (Exception e) {
            returnValue = false;
        }
        if (username == null || username.isEmpty()) {
            returnValue = false;
        }


        return returnValue;
    }
    //Mono, Flux => Spring WebFlux
    private Mono<Void> OnError(ServerWebExchange exchange, String err, HttpStatus httpStatus) {
        ServerHttpResponse response = exchange.getResponse();
        response.setStatusCode(httpStatus);

        log.error(err);
        return response.setComplete();
    }

}

위와 같이 모든 설정과 코드 구현을 마치고
postman을 통해 인증되지 않은 요청을 보내보면

정상적으로 401 ERROR가 발생한다.

다음 포스팅에서는 설정 정보 관리를 하기 위해 Spring Cloud, Bus, Rabbitmq를 통해서버 구축하는 과정을 진행해보겠습니다!

profile
백엔드 공부중입니다!

0개의 댓글

관련 채용 정보