본문 바로가기
해피 코딩/Today I Learned

[TIL 4] 보안 구성 Gateway를 통한 Security이해하기

by happy-coding 2024. 8. 5.

Today I Learned

오늘은 보안을 구성하는 Security를 학습하였다. Security는 배워도 배워도 끝없이 어려운 거 같다 😭
🔥 나중에 다시 봐도 이해하기 쉽게 정리를 해보도록 하자

[ 보안의 중요성 ]

  • 마이크로서비스 아키텍처에서는 각 서비스가 독립적으로 배포되고 통신하기 때문에 보안이 매우 중요합니다.
  • 데이터 보호, 인증 및 권한 부여, 통신 암호화 등을 통해 시스템의 보안성을 확보해야 합니다.

[ OAuth2 ]

  • OAuth2는 토큰 기반의 인증 및 권한 부여 프로토콜입니다.
  • 클라이언트 애플리케이션이 리소스 소유자의 권한을 얻어 보호된 리소스에 접근할 수 있도록 합니다.'
  • OAuth2는 네 가지 역할을 정의합니다: 리소스 소유자, 클라이언트, 리소스 서버, 인증 서버

[ JWT의 주요 특징 ]

  • 자가 포함: 토큰 자체에 모든 정보를 포함하고 있어 별도의 상태 저장이 필요 없습니다.
  • 간결성: 짧고 간결한 문자열로, URL, 헤더 등에 쉽게 포함될 수 있습니다.
  • 서명 및 암호화: 데이터의 무결성과 인증을 보장합니다.

[ 동작 실습 ]

이번 강의에서는 실습을 통해 Cloud Gateway의 Pre 필터에서 JWT 인증을 진행해 보았습니다.
  1. 로그인을 담당하는 서비스 어플리케이션 Auth Service 를 생성하고 로그인 기능을 아주 간단하게 구현한다.
  2. Cloud Gateway에 Pre 필터를 하나 더 생성하여 로그인을 체크 한다.
  3. 동작 정리: 로그인을 진행하면 auth를 통하여 토큰을 발급받고, 이 토큰을 사용하여 Gateway를 호출한다.


[ Auth ]

Auth의 yaml 설정
spring:
  application:
    name: auth-service

eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/

service:
  jwt:
    access-expiration: 3600000 # 만료시간 1시간
    secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"

server:
  port: 19095
Dependencies

build.gradle에 jwt 추가
implementation 'io.jsonwebtoken:jjwt:0.12.6'
AuthService 
  • 사용자 ID를 받아 JWT 액세스 토큰을 생성합니다.
public String createAccessToken(String user_id){
  return Jwts.builder()
          .claim("user_id", user_id)
          .claim("role","ADMIN") // 권한
          .issuer(issuer) // 토큰 발급자
          .issuedAt(new Date(System.currentTimeMillis())) // 발행 날짜
          .expiration(new Date(System.currentTimeMillis() + accessExpiration)) // 만료 날짜
          .signWith(secretKey, SignatureAlgorithm.HS512) // 알고리즘 서명
          .compact();
}
AuthController
  • 사용자 ID를 받아 JWT 액세스 토큰을 생성하고 응답합니다.
@GetMapping("/auth/signIn")
public ResponseEntity<?> createAuthToken(
        @RequestParam("user_id")
        String user_id
){
  return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
}

[ Cloud Gateway ]

  • 기존 사용하던 Gateway 코드에 JWT인증 및 auth-service 라우팅 정보를 추가합니다.
build.gradle에 jwt 추가
implementation 'io.jsonwebtoken:jjwt:0.12.6'
yaml 수정
server:
  port: 19091  # 게이트웨이 서비스가 실행될 포트 번호

spring:
  main:
    web-application-type: reactive  # Spring 애플리케이션이 리액티브 웹 애플리케이션으로 설정됨
  application:
    name: gateway-service  # 애플리케이션 이름을 'gateway-service'로 설정
  cloud:
    gateway:
      routes:  # Spring Cloud Gateway의 라우팅 설정
        - id: order-service  # 라우트 식별자
          uri: lb://order-service  # 'order-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/order/**  # /order/** 경로로 들어오는 요청을 이 라우트로 처리
        - id: product-service  # 라우트 식별자
          uri: lb://product-service  # 'product-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/product/**  # /product/** 경로로 들어오는 요청을 이 라우트로 처리
        - id: auth-service  # 라우트 식별자
          uri: lb://auth-service  # 'auth-service'라는 이름으로 로드 밸런싱된 서비스로 라우팅
          predicates:
            - Path=/auth/signIn  # /auth/signIn 경로로 들어오는 요청을 이 라우트로 처리
      discovery:
        locator:
          enabled: true  # 서비스 디스커버리를 통해 동적으로 라우트를 생성하도록 설정

eureka:
  client:
    service-url:
      defaultZone: http://localhost:19090/eureka/  # Eureka 서버의 URL을 지정

# Auth에서 access_token을 만들때 사용한 동일한 secret-key가 있어야 사용자로 부터 요청이 들어왔을 때 Gateway에서 토큰 유효성 검사 가능
service:
  jwt:
    secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
LocalJwtAuthenticationFilter.java
  • 헤더에서 토큰을 가져오고, 토큰에 대한 유효성 검사를 해줍니다.
  • 로그인 페이지 접근에 대해서는 토큰을 검사할 필요가 없기 때문에 다음 필터로 이동합니다.
  • https://http.dog/ 에서 HTTP 에러를 강아지의 귀여운 사진으로 볼 수 있습니다.
@Slf4j
@Component
public class LocalJwtAuthenticationFilter implements GlobalFilter {

  @Value("${service.jwt.secret-key}")
  private String secretKey;

  @Override
  public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
    // signIn(로그인)페이지 접근에 대해서는 토큰을 검사를 하지 않고 넘겨준다.
    String path = exchange.getRequest().getURI().getPath();
    if (path.equals("/auth/signIn")){
      return chain.filter(exchange);
    }

    String token = extractToken(exchange);

    // 잘못된 토큰일 경우 / State: 401
    if (token == null || !validateToken(token)){
      exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
      return exchange.getResponse().setComplete();
    }
    // 제대로 된 토큰일 경우 다음 필터로 이동
    return chain.filter(exchange);
  }

  private String extractToken(ServerWebExchange exchange){
    // 헤더에서 토큰을 가져오기 / Authorization: {Key:Value}형식의 Key 값
    String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
    // 토큰이 null이 아니고, "Bearer "로 시작하는지
    if (authHeader != null && authHeader.startsWith("Bearer ")){
      return authHeader.substring(7); // 순수 토큰 값
    }
    return null;
  }

  // 토큰 유효성 검사
  private boolean validateToken(String token) {
    try {
      SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
      Jws<Claims> claimsJws = Jwts.parser()
              .verifyWith(key)
              .build()
              .parseSignedClaims(token);
      // payload에 들어있는 데이터 확인
      log.info("#####payload :: " + claimsJws.getPayload().toString());

      // 추가적인 검증 로직 추가 가능(예: 토큰 만료 여부 확인 등)을 여기에 추가할 수 있습니다.
      return true;
    } catch (Exception e) { // 토큰 검증 중 예외가 발생할 경우 false 반환
      return false;
    }
  }

[ Run ]

http://localhost:19090에 접속하여 각 인스턴스를 확인합니다.


[ 엑세스 토큰 발급 전 ]

Gateway '19091' 에서 상품을 요청해 봅니다. 401 에러가 발생하는 것을 볼 수 있습니다.
  • 엑세스 토큰을 발급받지 않고 접근


[ 엑세스 토큰 발급 ]

Gateway에서 로그인을 요청하여 토큰을 발급받아봅니다.

해당 토큰을 상품요청에 헤더에 넣어서 요청합니다.
  • Gateway를 통하여 상품 요청 성공, 헤더에 엑세스 토큰 넣어주기


보안 구성!! Gateway를 통한 Security의 동작을 알아 보았다. 아직 Security를 완벽하게 이해한다고는 못하지만, 확실한 것은 들을 때마다 조금씩 쉬워지는 느낌이 있는 것 같다.

 

읽어 주셔서 감사합니다 😊

 

Eureka 서버

😺 GitHub: https://github.com/mad-cost/Starta-MSA-study-eureka.server

 

Gateway Repository

😺 GitHub: https://github.com/mad-cost/Starta-MSA-study-client.gateway

 

Auth Repository

😺 GitHub: https://github.com/mad-cost/Starta-MSA-study-client.auth