
지난 포스팅(Jenkins+Docker+MSA+Spring Boot CI/CD 적용)에 이어 MSA 적용을 하면서 CI/CD 파이프라인 구축 후, 해당 포스팅에서는 MSA 서비스를 위해 Spring Cloud Eureka,Gateway를 통한 인증/인가/예외 처리를 구현 할 예정입니다‼️
Spring Cloud, Gateway, Eureka에 대한 사전 지식과 Gateway와 Eureka 설정 방법에 대해서는 저의 이전 포스팅을 참고해주시면 될 것 같습니다.

현재 MSA를 적용한 설계도에서 Spring Cloud Eureka, Gateway에 대해서 알아보자면
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}"
    }
}
}
의존성을 추가해줬으면, Spring 메인 클래스에 @EnableEurekaServer 어노테이션을 붙여 Eureka Server임을 알려준다.
@SpringBootApplication
@EnableEurekaServer
public class PlantEurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(PlantEurekaApplication.class, args);
    }
}
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
Eureka Server을 run 시키고 http://[ip주소]:[eureka 포트 번호]
에 접속해보면 정상적으로 기동이 되면 아래 사진과 같이 나온다.

spring cloud 의존성은 위의 eureka 서버에 추가한 의존성을 추가하면됩니다!
implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
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}
📌 여기서 주의할 점은 필터에서 제외시킬 엔드포인트의 설정을 필터를 적용시킬 설정정보보다 위에 위치시켜 주셔야 해당 사항이 적용됩니다!
@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를 통해서버 구축하는 과정을 진행해보겠습니다!