이 글은 앞선 글 "개발노트: 현천 #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"
$