지난 포스팅(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를 통해서버 구축하는 과정을 진행해보겠습니다!