[MSA] OAuth 2.0

박원준·2026년 4월 13일

MSA

목록 보기
7/9

1️⃣ 보안 개요

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


2️⃣ OAuth 2.0

OAuth2는 사용자의 비밀번호를 직접 공유하지 않고도 제3자 애플리케이션이 사용자 자원에 접근할 수 있도록 해주는 인가(Authorization) 프레임워크입니다.

예를 들어 웹 서핑을 하다 보면 Google과 Naver 등의 외부 소셜 계정을 기반으로 간편히 회원가입 및 로그인할 수 있는 웹 어플리케이션을 쉽게 찾아볼 수 있습니다.

  • OAuth2는 토큰 기반의 인증 및 권한 부여 프로토콜입니다.
  • 클라이언트 애플리케이션이 리소스 소유자의 권한을 얻어 보호된 리소스에 접근할 수 있도록 합니다.
  • OAuth2는 다음 네 가지 역할을 정의합니다
    1. Resource Owner (리소스 소유자) : 실제 데이터를 소유한 사용자
    2. Client (애플리케이션) : 사용자 대신 리소스에 접근하려는 서비스
    3. Authorization Server (인증 서버) : 사용자 인증 후 토큰 발급
    4. Resource Server (API 서버) : 실제 데이터 제공 서버

🔹 구성요소와 역할

  1. 리소스 소유자(Resource Owner)
    접근에 인증이 필요한 리소스(Resource)에 대해서 접근하고자 하는 Entity 입니다. 만약 유저가 이를 이용하고 싶다고 한다면 end-user가 될 수도 있고 만약 다른 서버가 사용을 원한다면 리소스 소유자는 해당 서버가 될 수도 있습니다. 즉, 이는 리소스를 사용하고자 요청을 보내는 주체입니다.

  2. 인증 서버(Authorization Server)
    리소스에 대한 접근 권한 등을 관리하고 있는 서버입니다. 리소스 서버에 리소스 소유자가 클라이언트를 이용하여 접근할 수 있도록 Access Token을 만들어주는 서버이기도 하며 Grant Type을 관리하고 인증 정보를 관리하기도 합니다.

  3. 리소스 서버(Resource Server)
    리소스가 존재하고 있는 서버입니다. 이 서버는 리소스를 가지고 있고 해당 리소스를 접근 권한이 있는 사용자에게만 접근할 수 있도록 허용합니다. 대표적으로 JWT를 Access Token으로 이용한다면 리소스 서버는 이를 verify하여 인증과 인과를 확인하여 접근 여부를 판단합니다. 간단히 말해서 정보를 가지고 있는 API 서버라고 볼 수 있습니다.

  4. 클라이언트(Client)
    리소스 소유자를 대신하여 리소스 서버에 리소스를 요청하는 어플리케이션입니다. 웹, 모바일 앱, 데스크 탑 어플리케이션이라고 봐주시면 됩니다.

참고로 인증 서버와 리소스 서버는 구현에 따라 동일한 서버가 될 수도 있고 나뉘어질수도 있습니다.


🔹 Auth2.0 Flow

위의 이미지는 OAuth 2.0에서 4가지의 Role의 추상화된 상호작용을 flow로 나타낸 것입니다. 순서는 위에서 아래의 순서를 가집니다.

  1. 클라이언트는 리소스 소유자에게 권한얻기 위해서 인증을 요청합니다. 권한 부여는 리소스 소유자가 직접 할수도 있습니다. 하지만 대체적으로 로그인 등과 같이 인증 서버에 위임하여 간접적으로 요청하는것이 일반적입니다.

  2. 인증이 완료되면 Client는 authorization grant중 한가지와 이에 해당하는 정보들을 받습니다. (authorization grant의 종류는 아래에서 추가적으로 알아보겠습니다.) 권한 부여 유형은 각 서비스마다 다를 수 있습니다.

  3. 2번에서 얻은 정보와 Client가 기존에 가지고 있던 정보를 가지고 인증 서버(Authorization Server)에 Access Token을 요청합니다.

  4. 인증 서버는 클라이언트가 전달한 정보 및 Authorization Granth의 유효성을 검증합니다. 검증이 정상적으로 완료되면 Access Token을 발급해줍니다.

  5. 클라이언트는 Access Token을 이용하여 리소스 서버에 리소스에 접근한다는 요청을 합니다.

  6. 적절한 Access Token인지 Resource Server에서 검증한 후 검증이 완료되었다면 서버에서 리소스를 반환합니다.

위의 Flow를 보았을 때 실제로 리소스 소유자가 본인을 인증하는 것은 1번밖에 없습니다. 나머지는 시스템끼리 주고 받는 flow입니다. 1번의 인증 이후에 실제로 리소스에 접근하기 위해서 필요한것은 Access Token입니다. 결과적으로 리소스 접근에 대해서 유저의 Access Token이 만약 탈취되더라도 인증 정보는 무사하기 때문에 Access Token만 만료 시키면 안전성을 유지할 수 있습니다.


🔹 Authorization Grant의 종류

Authorization Grant란 Access Token을 얻는 방법입니다. OAuth2는 여러 방법으로 Access Token을 얻는 방법을 제공하고 있습니다. 먼저 간단하게 목록을 보겠습니다.

  • Authorization Code: 인증 코드를 사용하여 액세스 토큰을 얻는 방식
  • Implicit: 클라이언트 애플리케이션에서 직접 액세스 토큰을 얻는 방식
  • Resource Owner Password Credentials: 사용자 이름과 비밀번호를 사용하여 액세스 토큰을 얻는 방식
  • Client Credentials: 클라이언트 애플리케이션이 자신의 자격 증명을 사용하여 액세스 토큰을 얻는 방식
    • 서비스 → 서비스 인증 (사용자 없이 서비스 간 인증)
    • MSA 내부 통신에서 사용

가장 안전하고 일반적으로 사용하는 방식인 Authorization Code에 대해 자세히 알아보겠습니다.


🔹 Authorization Code

클라이언트와 리소스 소유자 사이에 인증 서버를 두고 access_token를 얻는 flow를 가집니다. 클라이언트는 인증 서버를 통해서 웹 사이트가 열리고 이곳에서 리소스 오너라는 것을 인증합니다. 그 이후 code를 받고 이를 access_token으로 교환하는데 이러한 인증 flow에는 클라이언트가 개입하는 부분이 없는 Authorization Grant 입니다.

이러한 Authorization Code grant type은 웹과 모바일 앱에서 주로 사용됩니다. 해당 type이 다른 grant type과 크게 다른 점은 클라이언트에서 인증하는 것이 아니라 인증 서버를 통해서 이동된 웹 사이트를 통해 유저가 인증하고 이 정보가 사용된다는 점에 있습니다.


위 flow를 4단계로 구분하면 아래와 같습니다.

  1. 클라이언트 어플리케이션은 Browser를 열고 사용자는 OAuth Server로 접속합니다.

  2. 사용자는 App의 요청을 승인하도록 새롭게 뜬 prompt 창을 보게 되고 로그인, 승락 또는 거절을 할 수 있습니다.

  3. 승낙하게 되면 클라이언트 어플리케이션으로 redirection 되고 authorization code를 받습니다.

  4. 클라이언트 어플리케이션은 이를 access token과 바꾸고 유저에게 반환합니다.

각 단계에서 주고 받는 정보는 아래와 같습니다.


리소스 소유자가 인증 서버에 권한을 요청할 때 호출하는 API

1번에서 클라이언트가 인증 서버에 요청하는 내용입니다. 이는 아래와 같습니다.

https://{{인증 서버 주소}}/auth
 ?response_type=code
 &client_id=29352915982374239857
 &redirect_uri=https%3A%2F%2Fexample-app.com%2Fcallback
 &scope=create+delete
 &state=xcoiv98y2kd22vusuye3kch
  • response_type=code : 인증 서버에 어플리케이션은 authorization code flow로 인증을 할것임을 전달
  • client_id : 클라이언트를 지칭하기 위한 key로 인증 서버에 등록되어있는 client_id
  • redirect_uri : 인증이 승인 되었을 때 user를 되돌리는 url 주소
  • scope : 요청할 권한 리스트
  • state : 클라이언트에서 생성한 random string 값. 이를 포함함으로써 승인 되었을 때 redirect_uri에 state를 그대로 반환 받습니다. 이를 이용하면 client에서는 정상적인 인증 서버에서 값이 반환되었다고 알 수 있습니다. 이를 통해서 CSRF 공격을 막을 수 있습니다.

클라이언트 URL로 돌아오는 Redirection API

유저가 인증 서버에서 본인 인증을 완료하고 나면, 인증 서버는 redirect_uri로 사이트를 이동시킵니다. 이때 아래의 파라미터를 함께 전달한다.

  • state : client가 request에 함께 보낸 state와 동일한 값입니다. client는 이값을 이용해서 정상적인 곳에서 해당 redirect_uri를 호출했는지 확인해야합니다. 이렇게 함으로써 CSRF 공격을 막을 수 있습니다.
  • code : 인증 서버가 생성한 authorization code입니다. 이 code는 1분 ~ 10분 정도의 짧은 TTL을 가지며 이후 Access Token과 교환이 가능합니다.
https://{{클라이언트 주소}}/redirect
 ?code=g0ZGZmNjVmOWIjNTk2NTk4ZTYyZGI3
 &state=xcoiv98y2kd22vusuye3kch  

인증 코드(Authorization Code)와 Access Token을 교환하는 API

이제 클라이언트는 위에서 받은 code를 이용하여 access token을 얻습니다.

아래의 파라미터로 access_token에 요청합니다. 이 request는 POST method를 이용합니다.

  • grant_type=authorization_code : grant type을 Authorization Code로 이용하고 있다고 전달합니다.
  • code : authorization server에서 받은 code를 전달합니다.
  • redirect_uri(optional) : access_token을 반환받고 이동할 곳의 url 입니다.
  • client_id : client를 지칭하기 위한 key로 인증 서버에 등록되어있는 client_id
  • client_secret : client의 secret. 유저와는 상호작용 없이 클라이언트와 인증 서버간의 뒷단에서 통신이 이루어집니다. 따라서 이렇게 하면 인증 코드(authorization code)를 가로챘을 수 있는 잠재적 공격자가 아닌 애플리케이션에서만 액세스 토큰을 요청할 수 있습니다.

이런 과정을 거쳐 결과적으로 클라이언트는 access_token을 얻게 되며 리소스 서버는 이를 검증하고 정상적인 token이면 유저에게 해당 리소스에 대한 접근을 허락해줍니다. 인증 과정의 마지막 결과로 얻게 되는 response는 아래와 같습니다.

HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store
Pragma: no-cache

{
  "access_token":"MTQ0NjJkZmQ5OTM2NDE1ZTZjNGZmZjI3",
  "token_type":"bearer",
  "expires_in":3600,
  "refresh_token":"IwOGYzYTlmM2YxOTQ5MGE3YmNmMDFkNTVk",
  "scope":"create delete"
}

3️⃣ JWT

  • JWT(JSON Web Token)는 JSON 형식의 자가 포함된 토큰으로, 클레임(claim)을 포함하여 사용자에 대한 정보를 전달합니다.
  • JWT는 헤더, 페이로드, 서명 총 세 부분으로 구성됩니다.
  • JWT는 암호화를 통해 데이터의 무결성과 출처를 보장합니다.

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


🔹 Bearer

  • Bearer는 OAuth 2.0 프로토콜에서 사용하는 인증 토큰 유형 중 하나로, 액세스 토큰을 통해 보호된 리소스에 접근할 수 있도록 합니다. Bearer 토큰은 요청 헤더에 포함되어 서버에 전달되며, 서버는 이를 검증하여 요청이 유효한지 확인합니다.
  • 간단한 사용법: 클라이언트는 서버에서 받은 Bearer 토큰을 HTTP 요청 헤더에 포함시키기만 하면 됩니다.
  • 서버 측 검증: 서버는 이 토큰을 검증하여 요청이 인증된 사용자의 요청인지 확인합니다. 일반적으로 토큰의 유효성, 만료 시간 등을 확인합니다.
  • 보안: Bearer 토큰은 HTTPS를 통해 전달되어야 합니다. 이를 통해 토큰이 전송 중에 도난당하지 않도록 보호할 수 있습니다

4️⃣ 실습

❗저번에 진행했던 API Gateway부분 코드에서 이어서 진행하겠습니다.
게이트웨이의 Pre 필터에서 JWT 인증을 진행해보겠습니다.

여기에 Auth Service 를 생성하여 로그인 기능을 아주 간단하게 구현하겠습니다.
클라우드 게이트웨이에 Pre 필터를 하나 더 생성하여 로그인을 체크 하겠습니다.

위 이미지의 점선만 실행하여 확인하겠습니다.

PostMan 또는 크롬 익스텐션 중 Talend API Tester를 설치하여 진행해보겠습니다.


🔹 Auth Service

로그인을 담당하는 서비스 어플리케이션을 생성합니다. 로그인을 진행하면 토큰을 발급받고 이 토큰을 사용하여 Gateway를 호출 합니다.

  1. start.spring.io 를 사용하여 프로젝트를 생성합니다.

  2. build.gradle 파일의 디펜던시를 아래와 같이 수정합니다. ( jwt 추가 )

    dependencies {
    	implementation 'io.jsonwebtoken:jjwt:0.12.6'
    	implementation 'org.springframework.boot:spring-boot-starter-actuator'
    	implementation 'org.springframework.boot:spring-boot-starter-security'
    	implementation 'org.springframework.boot:spring-boot-starter-web'
    	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    	compileOnly 'org.projectlombok:lombok'
    	annotationProcessor 'org.projectlombok:lombok'
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    	testImplementation 'org.springframework.security:spring-security-test'
    	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    	testImplementation 'io.projectreactor:reactor-test'
    	
    }
  3. application.yml

    spring:
      application:
        name: auth-service
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/
    
    service:
      jwt:
        access-expiration: 3600000
        secret-key: "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
    
    server:
      port: 19095
  4. AuthConfig.java

    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.web.SecurityFilterChain;
    
    @Configuration
    @EnableWebSecurity
    public class AuthConfig {
    
        // SecurityFilterChain 빈을 정의합니다. 이 메서드는 Spring Security의 보안 필터 체인을 구성합니다.
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                // CSRF 보호를 비활성화합니다. CSRF 보호는 주로 브라우저 클라이언트를 대상으로 하는 공격을 방지하기 위해 사용됩니다.
                .csrf(csrf -> csrf.disable())
                // 요청에 대한 접근 권한을 설정합니다.
                .authorizeRequests(authorize -> authorize
                    // /auth/signIn 경로에 대한 접근을 허용합니다. 이 경로는 인증 없이 접근할 수 있습니다.
                    .requestMatchers("/auth/signIn").permitAll()
                    // 그 외의 모든 요청은 인증이 필요합니다.
                    .anyRequest().authenticated()
                )
                // 세션 관리 정책을 정의합니다. 여기서는 세션을 사용하지 않도록 STATELESS로 설정합니다.
                .sessionManagement(session -> session
                    .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                );
    
            // 설정된 보안 필터 체인을 반환합니다.
            return http.build();
        }
    }
  5. AuthService.java

    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.io.Decoders;
    import io.jsonwebtoken.security.Keys;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.stereotype.Service;
    
    import javax.crypto.SecretKey;
    import java.util.Date;
    
    @Service
    public class AuthService {
    
      @Value("${spring.application.name}")
      private String issuer;
    
      @Value("${service.jwt.access-expiration}")
      private Long accessExpiration;
    
      private final SecretKey secretKey;
    
      /**
       * AuthService 생성자.
       * Base64 URL 인코딩된 비밀 키를 디코딩하여 HMAC-SHA 알고리즘에 적합한 SecretKey 객체를 생성합니다.
       *
       * @param secretKey Base64 URL 인코딩된 비밀 키
       */
      public AuthService(@Value("${service.jwt.secret-key}") String secretKey) {
          this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
      }
    
      /**
       * 사용자 ID를 받아 JWT 액세스 토큰을 생성합니다.
       *
       * @param user_id 사용자 ID
       * @return 생성된 JWT 액세스 토큰
       */
      public String createAccessToken(String user_id) {
          return Jwts.builder()
                  // 사용자 ID를 클레임으로 설정
                  .claim("user_id", user_id)
                  .claim("role", "ADMIN")
                  // JWT 발행자를 설정
                  .issuer(issuer)
                  // JWT 발행 시간을 현재 시간으로 설정
                  .issuedAt(new Date(System.currentTimeMillis()))
                  // JWT 만료 시간을 설정
                  .expiration(new Date(System.currentTimeMillis() + accessExpiration))
                  // SecretKey를 사용하여 HMAC-SHA512 알고리즘으로 서명
                  .signWith(secretKey, io.jsonwebtoken.SignatureAlgorithm.HS512)
                  // JWT 문자열로 컴팩트하게 변환
                  .compact();
      }
    }
  6. AuthController.java

    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import lombok.RequiredArgsConstructor;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequiredArgsConstructor
    public class AuthController {
    
      private final AuthService authService;
    
      /**
       * 사용자 ID를 받아 JWT 액세스 토큰을 생성하여 응답합니다.
       *
       * @param user_id 사용자 ID
       * @return JWT 액세스 토큰을 포함한 AuthResponse 객체를 반환합니다.
       */
      @GetMapping("/auth/signIn")
      public ResponseEntity<?> createAuthenticationToken(@RequestParam String user_id){
          return ResponseEntity.ok(new AuthResponse(authService.createAccessToken(user_id)));
      }
    
      /**
       * JWT 액세스 토큰을 포함하는 응답 객체입니다.
       */
      @Data
      @AllArgsConstructor
      @NoArgsConstructor
      static class AuthResponse {
          private String access_token;
    
      }
    }

🔹 Gateway

기존 게이트웨이 코드에 JWT인증 및 auth-service 라우팅 정보를 추가합니다.

  1. build.gradle 파일에 필요한 의존성을 추가합니다. ( jwt 추가 )

    dependencies {
    	implementation 'io.jsonwebtoken:jjwt:0.12.6'
    	implementation 'org.springframework.boot:spring-boot-starter-actuator'
    	implementation 'org.springframework.cloud:spring-cloud-starter-gateway'
    	implementation 'org.springframework.cloud:spring-cloud-starter-netflix-eureka-client'
    	compileOnly 'org.projectlombok:lombok'
    	annotationProcessor 'org.projectlombok:lombok'
    	testImplementation 'org.springframework.boot:spring-boot-starter-test'
    	testImplementation 'io.projectreactor:reactor-test'
    	testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
    }
  2. application.yml 을 다음과 같이 수정합니다.

    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을 지정
       
    service:
     jwt:
       secret-key:   "401b09eab3c013d4ca54922bb802bec8fd5318192b0a75f201d8b3727429080fb337591abd3e44453b954555b7a0812e1081c39b740293f765eae731f5a65ed1"
  3. LocalJwtAuthenticationFilter.java

    import io.jsonwebtoken.Claims;
    import io.jsonwebtoken.Jws;
    import io.jsonwebtoken.Jwts;
    import io.jsonwebtoken.io.Decoders;
    import io.jsonwebtoken.security.Keys;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.cloud.gateway.filter.GatewayFilterChain;
    import org.springframework.cloud.gateway.filter.GlobalFilter;
    import org.springframework.http.HttpStatus;
    import org.springframework.stereotype.Component;
    import org.springframework.web.server.ServerWebExchange;
    import reactor.core.publisher.Mono;
    
    import javax.crypto.SecretKey;
    @Slf4j
    @Component
    public class LocalJwtAuthenticationFilter implements GlobalFilter {
    
      @Value("${service.jwt.secret-key}")
      private String secretKey;
    
      @Override
      public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
          String path = exchange.getRequest().getURI().getPath();
          if (path.equals("/auth/signIn")) {
              return chain.filter(exchange);  // /signIn 경로는 필터를 적용하지 않음
          }
    
          String token = extractToken(exchange);
    
          if (token == null || !validateToken(token)) {
              exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
              return exchange.getResponse().setComplete();
          }
    
          return chain.filter(exchange);
      }
    
      private String extractToken(ServerWebExchange exchange) {
          String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
          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);
              log.info("#####payload :: " + claimsJws.getPayload().toString());
    
              // 추가적인 검증 로직 (예: 토큰 만료 여부 확인 등)을 여기에 추가할 수 있습니다.
              return true;
          } catch (Exception e) {
              return false;
          }
      }
    }

🔹 RUN

유레카 서버 ⇒ 게이트웨이⇒ 인증 ⇒ 상품 순으로 어플리케이션을 실행합니다.

  1. http://localhost:19090에 접속하여 각 인스턴스를 확인합니다.
  2. 게이트웨이에서 상품을 요청해 봅니다. 401 에러가 발생하는 것을 볼 수 있습니다.
  3. 게이트웨이에서 로그인을 요청하여 토큰을 발급받아봅니다.
  4. 해당 토큰을 상품요청에 헤더에 넣어서 요청합니다. 이미지를 확인해주세요.

0개의 댓글