API Gateway를 통한 사용자 자격 증명 기반 동적 라우팅

Bobur Umurzokov

Bobur Umurzokov

April 9, 2023

Technology

JWT 클레임 기반 동적 라우팅: Apache APISIX와 Okta를 활용한 사례

동적 라우팅은 대부분의 현대 API 게이트웨이에서 제공하는 강력한 기능으로, HTTP 헤더, 쿼리 파라미터, 심지어 요청 본문과 같은 다양한 기준을 기반으로 실시간으로 들어오는 요청을 다른 백엔드 서비스로 라우팅할 수 있게 해줍니다.

Apache APISIX의 기존 내장 플러그인을 활용하면 개발자들은 액세스 토큰, API 키, 사용자 ID와 같은 다양한 사용자 자격 증명을 기반으로 동적 라우팅 규칙을 생성할 수도 있습니다. 이 글에서는 Apache APISIX를 사용하여 인증 속성 기반 동적 라우팅을 도입하는 이점을 탐구하고, JWT 토큰의 클레임을 기반으로 클라이언트 요청을 적절한 백엔드 서비스로 동적으로 라우팅하는 예제 구성을 보여드리겠습니다.

학습 목표

이 글을 통해 다음을 배우게 됩니다:

  • API 게이트웨이를 사용한 동적 트래픽 라우팅.
  • 사용자 자격 증명 기반 동적 라우팅이 필요한 이유.
  • Apache APISIX를 사용한 JWT 토큰 클레임 기반 동적 라우팅.

API 게이트웨이: 동적 트래픽 라우팅

API 게이트웨이를 사용한 동적 트래픽 라우팅은 성능 최적화, 보안 강화, 사용자가 적절한 리소스에 접근할 수 있도록 보장하기 위해 다양한 애플리케이션과 시나리오에서 사용될 수 있습니다.

동적 트래픽 라우팅을 통해 시스템은 다른 서버 간의 부하를 분산하거나 서비스 간의 부하를 분산할 수 있습니다. 이를 통해 사용 가능한 서비스나 서버로 트래픽을 라우팅하여 고가용성을 보장할 수 있습니다. 하나의 서비스나 서버가 실패하면 트래픽이 자동으로 다른 사용 가능한 서비스나 서버로 재라우팅됩니다.

Apache APISIX를 사용한 동적 트래픽 라우팅

동적 라우팅은 사용자의 지리적 위치를 기반으로 트래픽을 라우팅하는 데에도 사용될 수 있습니다. 이를 통해 사용자가 가장 가까운 서버나 서비스에 연결되도록 하여 응답 시간을 개선하고 지연 시간을 줄일 수 있습니다.

Apache APISIX를 사용한 지리적 위치 기반 트래픽 라우팅

API 게이트웨이: 사용자 신원 기반 동적 라우팅

종종 우리는 사용자가 제공한 신원을 기반으로 특정 서비스나 경로로 트래�을 라우팅하거나 사용자와 관련된 데이터만 표시하고 싶을 때가 있습니다. 예를 들어, 다중 테넌트 애플리케이션에서 다른 테넌트는 다른 서비스나 리소스에 접근할 수 있습니다. 이 경우 API 게이트웨이는 사용자 자격 증명을 기반으로 적절한 테넌트 리소스로만 트래픽을 라우팅할 수 있습니다. 또는 모바일 애플리케이션에서는 장치 유형이나 운영 체제에 따라 특정 서비스로 트래�을 라우팅할 수 있습니다.

일반적인 접근 방식 중 하나는 JWT 토큰을 사용하여 API에 대한 요청을 인증하고 권한을 부여하는 것입니다. 이는 API 게이트웨이에서 JWT 토큰에 포함된 클레임을 고려하여 요청을 어디로 전달할지 또는 어떤 데이터를 표시할지 결정하는 복잡한 라우팅 규칙을 생성할 수 있음을 의미합니다. 이 접근 방식은 시스템 내에서 다양한 수준의 접근 제어가 필요한 여러 사용자가 있을 때 특히 유용합니다.

Apache APISIX를 사용한 JWT 토큰 기반 동적 트래픽 라우팅

데모: JWT 토큰 클레임 기반 동적 라우팅

이 데모에서는 컨퍼런스 세션, 발표자 및 주제 정보를 제공하는 Conference API라는 기존의 공개 백엔드 API를 사용합니다. 실제로 이는 여러분의 백엔드 서비스가 될 수 있습니다. 우리는 JWT 토큰과 같은 자격 증명을 사용하여 시스템에 로그인한 특정 발표자에 속한 세션만 필터링하고 검색하려고 한다고 가정해 보겠습니다. 예를 들어, https://conferenceapi.azurewebsites.net/speaker/1/sessions

이 요청은 고유 ID를 가진 발표자의 세션만 표시하며, 이 고유 ID는 JWT 토큰 클레임의 일부로 페이로드에서 가져옵니다. 아래 디코딩된 토큰 페이로드 구조를 보면 speakerId 필드도 포함되어 있습니다:

사용자 정의 클레임이 포함된 JWT 토큰

이 시나리오에서 우리는 API 게이트웨이의 동일한 Route로 요청을 보내고, 게이트웨이는 인증 헤더에서 동적 URI를 계산하여 요청을 해당 URI로 전달합니다(아래 다이어그램을 참조하여 흐름을 이해하세요). 이를 위해 Apache APISIX API 게이트웨이 수준에서 JWT 토큰의 클레임을 기반으로 동적 라우팅을 구현할 것입니다. 이를 위해 다음 플러그인을 사용합니다:

  1. openid-connect 플러그인: 이 플러그인은 ID 공급자(IdP)와 상호 작용하며, 백엔드 애플리케이션에 대한 인증되지 않은 요청을 가로챌 수 있습니다. ID 공급자로는 Okta를 사용하며, 이는 사용자 정의 클레임이 포함된 JWT 토큰을 발급하고 JWT 토큰을 검증합니다. 또는 Keycloak, Ory Hydra와 같은 다른 IdP를 사용할 수도 있으며, jwt-plugin을 사용하여 JWT 토큰을 생성하고 요청을 인증 및 권한 부여할 수도 있습니다.
  2. serverless-pre-function 플러그인: 이 플러그인은 요청을 가로채고, JWT 토큰 클레임을 디코딩 및 파싱하여 클레임 값을 새로운 사용자 정의 헤더에 저장하여 추가적인 권한 결정을 내리는 사용자 정의 Lua 함수 코드를 작성합니다.
  3. proxy-rewrite 플러그인: 클레임이 헤더에 있으면 이 플러그인을 요청 전달 메커니즘으로 사용하여 Nginx 헤더 변수를 기반으로 어떤 URI 경로를 사용할지 결정합니다. 이 경우 speakerId는 동적으로 변경되어 다른 경로 /speaker/$http_speakerId/sessions를 생성합니다. 이 플러그인은 요청을 Conference API의 관련 리소스로 전달합니다.

이제 데모에서 다룰 내용을 이해했으므로, 위 시나리오를 구성하고 튜토리얼을 완료하기 위한 전제 조건을 확인해 보겠습니다.

전제 조건

  • Docker: 컨테이너화된 etcd와 APISIX를 설치하는 데 사용됩니다.
  • curl: APISIX에 요청을 보내어 라우트, 업스트림 및 플러그인 구성을 설정하는 데 사용됩니다. Postman과 같은 쉬운 도구를 사용하여 API와 상호 작용할 수도 있습니다.
  • Apache APISIX가 대상 환경에 설치되어 있습니다. APISIX는 다음 빠른 시작 가이드를 따라 쉽게 설치하고 시작할 수 있습니다.
  • OKTA 계정이 생성되었는지 확인하고, 새 앱을 등록했는지 확인하세요(이 가이드 Okta 구성를 따를 수 있음), Okta 대시보드를 사용하여 토큰에 사용자 정의 클레임 추가, 그리고 speakerId라는 사용자 정의 클레임이 포함된 토큰 요청을 했는지 확인하세요.

백엔드 서비스 구성 (업스트림)

Conference API에 대한 요청을 라우팅할 백엔드 서비스를 구성해야 합니다. 이는 Apache APISIX에서 Admin API를 통해 업스트림 서버를 추가하여 수행할 수 있습니다.

curl "http://127.0.0.1:9180/apisix/admin/upstreams/1" -X PUT -d '
{
  "name": "Conferences API upstream",
  "desc": "Register Conferences API as the upstream",
  "type": "roundrobin",
  "scheme": "https",
  "nodes": {
    "conferenceapi.azurewebsites.net:443": 1
  }
}'

플러그인 구성 생성

다음으로, 새로운 플러그인 구성 객체를 설정합니다. 우리는 앞서 논의한 각 플러그인의 사용 사례에 따라 openid-connect, serverless-pre-function, 그리고 proxy-rewrite 플러그인을 사용할 것입니다. curl 명령을 실행하기 전에 openid-connect 플러그인 속성(ClienID, Secret, Discovery 및 Introspection 엔드포인트)을 여러분의 Okta 세부 정보로 대체해야 합니다.

curl "http://127.0.0.1:9180/apisix/admin/plugin_configs/1" -X PUT -d '
{
    "plugins": {
        "openid-connect":{
            "client_id":"{YOUR_OKTA_CLIENT_ID}",
            "client_secret":"{YOUR_OKTA_CLIENT_SECRET}",
            "discovery":"https://{YOUR_OKTA_ISSUER}/oauth2/default/.well-known/openid-configuration",
            "scope":"openid",
            "bearer_only":true,
            "realm":"master",
            "introspection_endpoint_auth_method":"https://{YOUR_OKTA_ISSUER}/oauth2/v1/introspect",
            "redirect_uri":"https://conferenceapi.azurewebsites.net/"
        },
        "proxy-rewrite": {
            "uri": "/speaker/$http_speakerId/sessions",
            "host":"conferenceapi.azurewebsites.net"
        },
        "serverless-pre-function": {
            "phase": "rewrite",
            "functions" : ["return function(conf, ctx)

    -- 필요한 라이브러리 가져오기
    local core = require(\"apisix.core\")
    local jwt = require(\"resty.jwt\")

    -- Authorization 헤더에서 JWT 토큰 가져오기
    local jwt_token = core.request.header(ctx, \"Authorization\")
    if jwt_token ~= nil then
        -- JWT 토큰에서 "Bearer " 접두사 제거
        local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\")
        if jwt_token_only ~= nil then
           -- JWT 토큰 디코딩
           local jwt_obj = jwt:load_jwt(jwt_token_only)

           if jwt_obj.valid then
             -- JWT 토큰에서 "speakerId" 클레임 값 가져오기
             local speakerId_claim_value = jwt_obj.payload.speakerId

             -- "speakerId" 클레임 값을 헤더 변수에 저장
             core.request.set_header(ctx, \"speakerId\", speakerId_claim_value)
           end
         end
     end
   end
    "]}
    }
}'

위 구성에서 가장 이해하기 어려운 부분은 serverless-pre-function 플러그인 내부에 작성한 Lua 사용자 정의 함수 코드일 수 있습니다:

return function(conf, ctx)
    -- 필요한 라이브러리 가져오기
    local core = require(\"apisix.core\")
    local jwt = require(\"resty.jwt\")

    -- Authorization 헤더에서 JWT 토큰 가져오기
    local jwt_token = core.request.header(ctx, \"Authorization\")
    if jwt_token ~= nil then
        -- JWT 토큰에서 "Bearer " 접두사 제거
        local _, _, jwt_token_only = string.find(jwt_token, \"Bearer%s+(.+)\")
        if jwt_token_only ~= nil then
           -- JWT 토큰 디코딩
           local jwt_obj = jwt:load_jwt(jwt_token_only)

           if jwt_obj.valid then
             -- JWT 토큰에서 "speakerId" 클레임 값 가져오기
             local speakerId_claim_value = jwt_obj.payload.speakerId

             -- "speakerId" 클레임 값을 헤더 변수에 저장
             core.request.set_header(ctx, \"speakerId\", speakerId_claim_value)
           end
         end
   end
end

기본적으로 이 플러그인은 다른 두 플러그인보다 먼저 실행되며 다음을 수행합니다:

  1. Authorization 헤더에서 JWT 토큰을 가져옵니다.
  2. JWT 토큰에서 "Bearer " 접두사를 제거합니다.
  3. resty.jwt 라이브러리를 사용하여 JWT 토큰을 디코딩합니다.
  4. 디코딩된 JWT 토큰에서 "speakerId" 클레임 값을 가져옵니다.
  5. 마지막으로, "speakerId" 클레임 값을 speakerId 헤더 변수에 저장합니다.

새로운 라우트 구성

이 단계에서는 플러그인 구성을 사용하는 새로운 라우트를 설정하고, 이전 단계에서 생성한 업스트림과 함께 작동하도록 라우트를 구성합니다(이들의 ID를 참조하여):

curl "http://127.0.0.1:9180/apisix/admin/routes/1"  -H 'X-API-KEY: edd1c9f034335f136f87ad84b625c8f1' -X PUT -d '
{
    "name":"Conferences API speaker sessions route",
    "desc":"Create a new route in APISIX for the Conferences API speaker sessions",
    "methods": ["GET"],
    "uri": "/sessions",
    "upstream_id":"1",
    "plugin_config_id":1
}'

위 구성에서 우리는 HTTP GET 요청만 URI /sessions로 라우팅되도록 라우트 매칭 규칙을 정의했습니다.

Okta에서 토큰 가져오기

APISIX 측에서 업스트림, 플러그인 및 라우트를 구성한 후, 이제 우리는 speakerId 사용자 정의 클레임이 포함된 토큰을 Okta에서 요청합니다. Okta와 함께 토큰 요청 URL을 구축하는 방법에 대한 정보가 포함된 가이드를 따르거나, 아래 결과 URL을 Okta 발급자 및 클라이언트 ID와 함께 사용할 수 있습니다:

https://{YOUR_OKTA_ISSUER}/oauth2/default/v1/authorize?client_id={YOUR_OKTA_CLIENT_ID}
&response_type=id_token
&scope=openid
&redirect_uri=https%3A%2F%2Fconferenceapi.azurewebsites.net
&state=myState
&nonce=myNonceValue

이 요청을 브라우저에 붙여넣으면 브라우저는 Okta의 로그인 페이지로 리디렉션되고 ID 토큰이 생성됩니다.

https://conferenceapi.azurewebsites.net/#id_token={TOKEN_WILL_BE_HERE}

다른 ID 공급자에서 토큰을 검색하는 과정은 다를 수 있습니다.

반환된 ID 토큰을 확인하려면 값을 복사하여 https://token.dev와 같은 JWT 디코더에 붙여넣을 수 있습니다.

동적 라우팅 테스트

마지막으로, 이제 요청이 매칭 기준과 JWT 토큰 클레임을 기반으로 올바른 URI 경로(발표자별 세션)로 라우팅되는지 확인할 수 있습니다. 이를 위해 간단한 curl 명령을 실행합니다:

curl -i -X "GET [http://127.0.0.1:9080/sessions](http://127.0.0.1:9080/sessions)" -H "Authorization: Bearer {YOUR_OKTA_JWT_TOKEN}"

여기서 우리가 예상한 대로 결과가 나옵니다. Okta JWT 클레임에서 speakerId를 1로 설정하면 Apisix는 요청을 관련 URI 경로로 라우팅하고 이 발표자의 모든 세션을 응답으로 반환합니다.

{
  "collection": {
    "version": "1.0",
    "links": [],
    "items": [
      {
        "href": "https://conferenceapi.azurewebsites.net/session/114",
        "data": [
          {
            "name": "Title",
            "value": "\r\n\t\t\tIntroduction to Windows Azure Part I\r\n\t\t"
          },
          {
            "name": "Timeslot",
            "value": "04 December 2013 13:40 - 14:40"
          },
          {
            "name": "Speaker",
            "value": "Scott Guthrie"
          }
        ],
        "links": [
          {
            "rel": "http://tavis.net/rels/speaker",
            "href": "https://conferenceapi.azurewebsites.net/speaker/1"
          },
          {
            "rel": "http://tavis.net/rels/topics",
            "href": "https://conferenceapi.azurewebsites.net/session/114/topics"
          }
        ]
      },
      {
        "href": "https://conferenceapi.azurewebsites.net/session/121",
        "data": [
          {
            "name": "Title",
            "value": "\r\n\t\t\tIntroduction to Windows Azure Part II\r\n\t\t"
          },
          {
            "name": "Timeslot",
            "value": "04 December 2013 15:00 - 16:00"
          },
          {
            "name": "Speaker",
            "value": "Scott Guthrie"
          }
        ],
      }
    ]
   }
}

요약

  • API 게이트웨이를 사용하면 다양한 기준을 기반으로 트래픽을 다른 백엔드 서비스로 라우팅할 수 있습니다.
  • 동적 라우팅은 요청 헤더, 쿼리 또는 본문에 지정된 사용자 속성에 따라 달라질 수 있습니다.
  • JWT 토큰에 포함된 클레임을 고려하여 복잡한 라우팅 규칙을 생성할 수 있으며, 권한이 있는 요청만 API에 접근할 수 있도록 보장할 수 있습니다.
Tags: