개발노트: 현천 #9 관문 Part-2 API Gateway

Bookmark and Share

이 글은 앞선 글 "개발노트: 현천 #9 관문"에 이어지는 글로, "관문" 서비스를 통하여 뒤에 숨어있는 실제의 "업무 서비스"가 제공하는 API에 접근하기 위한 기능의 구현을 설명한다.

API Gateway

API Gateway는 다양한 서비스가 제공하는 API를 단일 창구를 통하게 함으로써 보안 및 인증/권한 관리, 기록 및 감사, 전반적인 서비스 품질 보장, API Aggregation 및 응답 형식 표준화 등의 다양한 영역에서의 품질을 하나의 창구를 통하여 보장해주는 역할을 한다.

API Gateway의 작성

API Gateway 구성의 바탕이 되는 Client 인증 기능과 요청 처리에 대한 Logging 기능은 이미 앞선 문서에서 그 틀을 작성하였다. 이 단계에서는, 구체적으로,

  • Gateway 서비스를 제공하기 위한 모델링과 인증방식의 정비,
  • 실제의 서비스 API 호출 및 응답 처리 기능을 구현

하는 단계로 진행된다.

서비스 클래스 작성

먼저, 관문 뒤에 숨어있는 서비스 정의를 위한 클래스와 사용자 또는 서버(통칭해서 클라이언트)의 접속 허가를 저장하기 위한 클래스를 만든다.

$ rails g scaffold service name description base_url is_public:boolean --no-javascripts --no-stylesheets      invoke  active_record
      create    db/migrate/20150308154558_create_services.rb
      create    app/models/service.rb
      invoke    test_unit
      create      test/models/service_test.rb
      create      test/fixtures/services.yml
      invoke  resource_route
       route    resources :services
      invoke  scaffold_controller
      create    app/controllers/services_controller.rb
      invoke    erb
      create      app/views/services
      create      app/views/services/index.html.erb
      create      app/views/services/edit.html.erb
      create      app/views/services/show.html.erb
      create      app/views/services/new.html.erb
      create      app/views/services/_form.html.erb
      invoke    test_unit
      create      test/controllers/services_controller_test.rb
      invoke    helper
      create      app/helpers/services_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/services/index.json.jbuilder
      create      app/views/services/show.json.jbuilder
      invoke  assets
      invoke    coffee
      invoke    scss
$ 
$ rails g scaffold access description client:references{polymorphic} service:references permissions:text --no-javascripts --no-stylesheets
      invoke  active_record
      create    db/migrate/20150308155815_create_accesses.rb
      create    app/models/access.rb
      invoke    test_unit
      create      test/models/access_test.rb
      create      test/fixtures/accesses.yml
      invoke  resource_route
       route    resources :accesses
      invoke  scaffold_controller
      create    app/controllers/accesses_controller.rb
      invoke    erb
      create      app/views/accesses
      create      app/views/accesses/index.html.erb
      create      app/views/accesses/edit.html.erb
      create      app/views/accesses/show.html.erb
      create      app/views/accesses/new.html.erb
      create      app/views/accesses/_form.html.erb
      invoke    test_unit
      create      test/controllers/accesses_controller_test.rb
      invoke    helper
      create      app/helpers/accesses_helper.rb
      invoke      test_unit
      invoke    jbuilder
      create      app/views/accesses/index.json.jbuilder
      create      app/views/accesses/show.json.jbuilder
      invoke  assets
      invoke    coffee
      invoke    scss
$

여기서 Access 모델은 "Service" - "Access" - "Client" 구도를 만들어 클라이언트의 사용 권한을 서비스 별로 관리할 수 있게 한다. 보다 세분화된 권한 관리가 필요하다면 "Service" - "Path/Resource" - "Access" - "Client" 단계로 구성을 잡는 것도 방법.

새 Migration을 적용하고, 변경 사항 Commit!

$ rake db:migrate
== 20150308154558 CreateServices: migrating ===================================
-- create_table(:services)
   -> 0.0056s
== 20150308154558 CreateServices: migrated (0.0057s) ==========================

== 20150308155815 CreateAccesses: migrating ===================================
-- create_table(:accesses)
   -> 0.0101s
-- add_foreign_key(:accesses, :services)
   -> 0.0016s
== 20150308155815 CreateAccesses: migrated (0.0118s) ==========================

$ git commit -m "scaffold for services/accesses"
$

인증 구조의 변경과 서버 등록 기능 복원

이 모듈의 최초 계획에서는 그 용도를 "중앙 집중식 원격 로그 기록"에 초점을 맞추고 있었고, Gateway로써의 역할은 서버 간 통신을 위주로 생각하고 있었다. 따라서 관리자의 노력을 최소화하기 위하여 서버의 등록은 자동으로 이루어지게 하였고, API 인증 역시 서버 중심으로 설계하였다.

그러나, 설계와 더불어 구현을 하는 과정에서, 중앙 집중식 인증 및 권한 관리의 효과를 보다 높이기 위해서(또는, 다른 모듈에 들어가는 인증을 줄이기 위해서) 사용자 브라우저가 개별 서비스 API에 접근하기 위해 이 모듈을 통하는 것으로 설계를 변경하였고, 이를 지원하기 위해 인증 기능을 수정하고 지워버렸던 서버 등록 기능을 복원하였다.

주요 변경 사항은 다음과 같다.

--- a/app/controllers/api/api_controller.rb
+++ b/app/controllers/api/api_controller.rb
@@ -38,27 +38,34 @@ class Api::ApiController < ActionController::Base
 
   protected
   def token_required
-    client_addr = env['HTTP_X_FORWARDED_FOR']
-    unless @client = Server.find_by_address(client_addr)
-      @client = Server.create(address: client_addr)
-      api_log(:info, 'server#create', @client.id, 'register', 'first request')
-    end
-
-    if authenticate_token
-      api_log(:debug, 'authenticate', @client.id, 'valid authentication.')
-    else
-      api_log(:error, 'authenticate', @client.id, 'invalid authentication.')
-      render_unauthorized
-    end
+    authenticate || render_unauthorized
   end
 
-  def authenticate_token
+  def authenticate
+    client_addr = env['HTTP_X_FORWARDED_FOR']
     authenticate_with_http_token do |token, options|
-      @client.is_authorized?(token)
+      if @client = User.find_by_api_key(token)
+        api_log(:verbose, nil, @client.id, 'user authenticated')
+      elsif client = Server.find_by_address(client_addr)
+        if client.is_authorized?(token)
+          @client = client
+          api_log(:verbose, nil, @client.id, 'server authenticated')
+        end
+      else
+        net = ENV['TRUSTED_NETWORK'].split('.')
+        if client_addr.split('.')[0..net.size - 1] == net
+          client = Server.create(address: client_addr)
+          api_log(:info, 'server#create', client.id, nil, 'trusted net')
+        end
+      end
     end
+    @client
   end
 
   def render_unauthorized
+    client_addr = env['HTTP_X_FORWARDED_FOR']
+    api_log(:error, nil, client_addr, 'unauthorized access')
+
     self.headers['WWW-Authenticate'] = 'Token realm="Kwanmun"'
     render json: error(401, :unauthorized), status: :unauthorized
   end
--- a/app/models/concerns/client_methods.rb
+++ b/app/models/concerns/client_methods.rb
@@ -22,7 +22,8 @@ module ClientMethods
   private
   def generate_api_key
     begin
-      self.api_key = Digest::SHA256.new.update(SecureRandom.base64).to_s
+      self.api_key = self.class.name[0].downcase
+      self.api_key += Digest::SHA256.new.update(SecureRandom.base64).to_s
     end while self.class.exists?(api_key: api_key)
   end
 end
--- a/config/application.yml.dist
+++ b/config/application.yml.dist
@@ -3,3 +3,5 @@ YEOKSA_DATABASE_USERNAME: @DATABASE_USERNAME@
 YEOKSA_DATABASE_PASSWORD: @DATABASE_PASSWORD@
 
 SECRET_KEY_BASE: @SECRET_KEY_BASE@
+
+TRUSTED_NETWORK: "192.168"

Gateway API 작성

다음과 같은 내용으로, API 아래에 Gateway 역할을 제어하기 위한 Controller를 만들어 준다. 이 Controller는

  • Client가 Service 사용권(Access)을 가지고 있는 경우,
  • 대신하여 실제의 Service를 호출한 후에
  • 그 결과를 JSON 포맷에 담아 Client에게 전달

하는 방식으로 작동한다.

--- a/app/controllers/api/api_controller.rb
+++ b/app/controllers/api/api_controller.rb
@@ -17,12 +18,20 @@ class Api::ApiController < ActionController::Base
     end
   end
 
+  def gateway_log(mesg, result, tags=nil)
+    where = env['HTTP_X_FORWARDED_FOR']
+    process = env['HTTP_USER_AGENT']
+    service = params[:service_name]
+    action = params[:path]
+    target = result
+    log(:gateway, :info, process, mesg, where, action, target, nil, tags)
+  end
+
   def api_log(level, action, target, mesg=nil, why=nil, tags=nil)
     category = 'api'
     where = env['HTTP_X_FORWARDED_FOR']
--- /dev/null
+++ b/app/controllers/api/v1/gateway_controller.rb
@@ -0,0 +1,64 @@
+class Api::V1::GatewayController < Api::ApiController
+
+  # GET /s/:service_name/*path
+  def get
+    if @service = @client.services.find_by_name(params[:service_name])
+      conn = Faraday.new(url: @service.base_url,
+                         ssl: {verify: false}) do |faraday|
+        faraday.adapter Faraday.default_adapter
+      end
+      @result = set_result conn.get params[:path] + '.json'
+      gateway_log('<%s>/%s served with %s' % [@service.name,
+                                              params[:path],
+                                              @result[:status]],
+                  @result[:extra])
+    else
+      @result = set_error 404, 'Not Found'
+      gateway_log('no service found for client', @result[:extra])
+    end
+  end
+
+  # POST /s/:service_name/*path
+  def post
+    render_unauthorized
+  end
+
+  # PUT /s/:service_name/*path
+  def put
+    render_unauthorized
+  end
+
+  # DELETE /s/:service_name/*path
+  def delete
+    render_unauthorized
+  end
+
+  private
+  def set_result response
+    {
+      service: params[:service_name],
+      status: response.env.status,
+      response: JSON.parse(response.body),
+      extra: {
+        base_url: @service.base_url,
+        path: params[:path],
+        url: response.env.url.to_s,
+        method: response.env.method.upcase,
+        headers: response.env.response_headers,
+      }
+    }
+  end
+
+  def set_error status, reason
+    {
+      service: params[:service_name],
+      status: status,
+      response: nil,
+      extra: {
+        path: params[:path],
+        message: reason,
+      }
+    }
+  end
+end
+# vim: set ts=2 sw=2 expandtab:
--- /dev/null
+++ b/app/views/api/v1/gateway/get.json.jbuilder
@@ -0,0 +1 @@
+json.extract! @result, :service, :status, :response, :extra
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -18,6 +18,10 @@ Rails.application.routes.draw do
   namespace :api, defaults: {format: 'json'} do
     namespace :v1 do
       resources :logs
+      get 's/:service_name/*path', to: 'gateway#get'
+      post 's/:service_name/*path', to: 'gateway#post'
+      put 's/:service_name/*path', to: 'gateway#put'
+      delete 's/:service_name/*path', to: 'gateway#delete'
     end
   end

대충 정리가 되었으면 확인을 하고, commit!

$ git commit -m "write gateway api controller"
$

Bookmark and Share


따로 명시하지 않는 한에서 이 사이트의 모든 콘텐츠는 다음의 라이선스를 따릅니다: Creative Commons Attribution-NonCommercial 3.0 License