Rails에서 Swagger를 이용하여 API Docs 사용 시 인증 처리

Swagger는 HTTP 기반의 API에 대한 문서 및 호출 환경 등을 웹 기반으로 제공해주는 솔루션입니다. 최근 모바일 앱의 서버 사이드 단이나  마이크로 서비스 아키텍처를 구성하면서 API 개발을 많이 하게 되면서 API 스펙에 대한 정의 및 API 호출(테스트) 환경이 더욱 중요해졌습니다.  이번 글에서는 Swagger에 대해서 간단하게 살펴보고 Rails에서 Swagger를 적용하면서 swagger-ui에 Basic Auth를 추가한 내용을 소개합니다.

Swagger 간단한 소개

이번 글은 Swagger에 대한 소개 글은 아니지만 간단하게 살펴보면 다음과 같습니다.

  • Swagger는 API 정의를 위한 스펙을 제공
    • Spec 정의는 주로 JSON 또는 YML 형태로 정의
  • Swagger에서 정의한 스펙대로 API를 정의하는 것은 프로그램 언어마다 또는 도구마다 다른 형태로 제공
    • 예를 들어 Java Spring의 경우 Controller의 Action 메소드에 @ApiImplicitParams, @ApiResponses 등과 같은 어노테이션으로 API를 정의할 수 있으며
    • Rails의 경우 controller 에 다음과 같은 별도 정의된 DSL을 이용하여 정의할 수 있습니다.
      1
      2
      3
      4
      5
      6
      7
      8
      9
      swagger_api :signin do
      summary 'Sing in'
      notes 'Sing in'
      param :form, :username, :string, :required, 'username'
      param :form, :password, :string, :required, 'password'
      end
      def signin
      # sign in logic
      end
    • 즉, 프로그램 언어와 무관하게 사용할 수 있습니다.
  • swagger-ui를 이용하여 웹 화면에서 API 스펙 정의를 조회하고 직접 호출 할 수 있음

즉 Swagger는 프로그램 언어와 상관없이 Swagger 에서 정의한 형태로 API 스펙을 제공해주면 swagger-ui 를 이용하여 이를 웹에서 조회할 수 있는 도구입니다.

SwaggerHP_Build

화면 상단에 ../swagger.json 으로 보이는 부분에 Swagger 형태의 파일을 지정해주는 방식입니다.

Rails에서 Swagger 사용

Rails에서 Swagger를 사용하기 위해서는 Swagger 관련 Gem을 사용하는데 구글 검색을 해보면 하나가 아닌 것을 알 수 있습니다. 어느 것이 좋은지 판단하기 애매해서 필자의 경우 "swagger-docs"를 사용했습니다. Gemfile에 swagger-docs를 추가합니다.

gem 'swagger-docs'

Gem을 추가한 다음 bundle을 install을 합니다. 그리고 initializers/swagger_docs.rb 파일을 생성하여 다음과 같은 코드를 추가합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Swagger::Docs::Config.register_apis({
  "1.0" => {
    # the extension used for the API
    :api_extension_type => :json,
    # the output location where your .json files are written to
    :api_file_path => "public",
    # the URL base path to your API
    :base_path => "http://api.somedomain.com",
    # if you want to delete all .json files at each generation
    :clean_directory => false,
    # Ability to setup base controller for each api version. Api::V1::SomeController for example.
    :parent_controller => Api::V1::SomeController,
    # add custom attributes to api-docs
    :attributes => {
      :info => {
        "title" => "Swagger Sample App",
        "description" => "This is a sample description.",
        "termsOfServiceUrl" => "http://helloreverb.com/terms/",
        "contact" => "apiteam@wordnik.com",
        "license" => "Apache 2.0",
        "licenseUrl" => "http://www.apache.org/licenses/LICENSE-2.0.html"
      }
    }
  }
})

위 코드는 swagger-docs gem 공식 사이트에 있는 코드입니다. 'api_file_path' 로 설정된 패스에 json 형태의 api doc 파일이 생성됩니다. 따라서 위 설정의 경우 rails에서 public한 static 파일 영역인 public 디렉토리로 지정해 주었습니다. 'base_path'는 API 호출할 때 API path 앞에 추가할 도메인이나 호스트에 대한 정보를 작성합니다.

여기까지 한 다음에 controller에 API 정의를 작성하는데 swagger-docs gem에 정의된 DSL을 이용하여 작성합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Api::V1::UsersController < ApplicationController
  swagger_controller :users, "User Management"
  swagger_api :index do
    summary "Fetches all User items"
    notes "This lists all the active users"
    param :query, :page, :integer, :optional, "Page number"
    param :path, :nested_id, :integer, :optional, "Team Id"
    response :unauthorized
    response :not_acceptable, "The request you made is not acceptable"
    response :requested_range_not_satisfiable
  end
  swagger_api :show do
    summary "Fetches a single User item"
    param :path, :id, :integer, :optional, "User Id"
    response :ok, "Success", :User
    response :unauthorized
    response :not_acceptable
    response :not_found
  end

Swagger docs를 사용하면 controller에 앞에서 설명한 API 스펙 정의 코드를 추가해주면 됩니다. 예제에서 처럼 controller의 앞 부분에 전체 action에 대해서 모두 정의해도 되고 각 action 별로 정의를 해도 됩니다.

이렇게 정의된 API는 다음 rake 명령을 이용하여 swagger api 문서를 생성합니다.

SD_LOG_LEVEL=1 rake swagger:docs

이 명령을 실행하면 swagger 환경 설정(swagger-docs.rb)에서 정의한  api_file_path 디렉토리에 'api-docs.json' 파일과 'api/v1/users.json' 파일이 생성됩니다.

이렇게 생성된 파일은 swagger-ui 를 이용하여 조회할 수 있는데 swagger-ui를 다운로드 받은 후 dist 디렉토리에 있는 내용 모두를 api_file_path에 지정된 곳으로 복사합니다. 위 예제의 경우에는 'public/api' 디렉토리에 복사합니다. 그런 다음 웹 브라우저에서 'http://localhost:3000/api/index.html' 로 접근하면 다음과 같은 API 관련 화면이 조회됩니다.

swagger-first

Swagger에 권한 설정

위와 같은 구성에서 문제는 API 정의 문서가 public 영역에 있어 누구나 접근할 수 있다는 것입니다. 이런 상태로 운영에 배포되면 구글 검색 엔진 등에 노출될 수도 있기 때문에 문제의 소지가 있습니다. 이런 이유 때문에 어떤 서비스들은 API 문서는 운영환경에 배포하지 않는 경우도 있습니다. 그렇다 하더라도 테스트를 하기 위해서 최소한 스테이징 환경에는 배포해야 하는데 스테이징 환경이 인터넷에 노출된 상황에서는 동일한 문제가 있습니다.

기본적으로 Swagger에도 Authorization에 대한 정의는 되어 있지만 실제로 swagger-ui에서는 이것을 사용하지 않습니다. API 호출시에는 이Authorization을 사용하지만 필자가 원했던 것은 API 정의 자체도 볼 수 없는 환경을 원했습니다. 앞의 swagger-ui 예제 화면을 보면 기본 화면에서 이미 API 정의는 모두 볼 수 있습니다. 이런 API 정의가 노출되는 것도 보안에 문제가 될 수 있어 필자의 경우 이 화면에 나타나기 전에 인증을 먼저 거친 후 swagger-ui  초기 화면으로 넘어가는 방식이 필요 했습니다. Spring의 경우 기본 보안 설정에 해당 URL에 대해 보안 설정을 하면 되지만 Rails의 경우 public 영역에 있는 파일에 대해서는 보안 설정을 할 수 없기 때문에 별도의 작업으로 이 문제를 해결 했습니다.

(이 문제를 해결하는 gem이 있으면 댓글로 알려주세요.)

  • api 스펙이 정의된 문서 생성 디렉토리를  public 영역이 아닌 view 영역으로 설정하였습니다(initializers/swagger_docs.rb).
    1
    2
    3
    4
    5
    6
    Swagger::Docs::Config.register_apis({
    "1.0" => {
    # the extension used for the API
    :api_extension_type => :json,
    # the output location where your .json files are written to
    :api_file_path => "app/views/swagger",
  • 이렇게 설정한 다음 api doc을 generate 하였습니다.
    • rake swagger:docs
  • swagger-ui에 있는 index.html 파일을 view 영역으로 옮겼습니다.
    • mv public/api/index.html app/view/swagger/index.html.erb
  • app/controller/swagger_controller.rb를 생성하여 index.html을 호출하는 action을 추가합니다.
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    class SwaggerController < ApplicationController
    before_action :basic_auth!
    def index
    response.headers["Cache-Control"] = "no-cache, no-store"
    response.headers["Pragma"] = "no-cache"
    response.headers["Expires"] = "Fri, 01 Jan 1990 00:00:00 GMT"
    render layout: false
    end
    def api_docs
    @@data = File.read("app/views/swagger/api-docs.json")
    render :json => @@data
    end
    def api_spec
    version = params[:version]
    name = params[:api_name]
    @@data = File.read("app/views/swagger/api/#{version}/#{name}.json")
    render :json => @@data
    end
    private:
    def basic_auth!
    if request.headers['Authorization'].blank?
    headers['WWW-Authenticate'] = "Basic realm=\"Swagger API Docs\""
    render :layout => false, :status => :unauthorized and return
    end
    userAndPassword = decode64_url(request.headers['Authorization'].split(' ')[1])
    if userAndPassword != "api_user:password1234"  #user와 password는 환경 설정을 이용하는 것이 좋다.
    headers['WWW-Authenticate'] = "Basic realm=\"Swagger API Docs\""
    render :layout => false, :status => :unauthorized and return
    end
    end
    def decode64_url(str)
    return '' if str.blank?
    # add '=' padding
    str = case str.length % 4
    when 2 then str + '=='
    when 3 then str + '='
    else
    str
    end
    Base64.decode64(str.tr('-_', '+/'))
    end
    end
    Controller의 구현 내부에 기본 basic auth를 이용하여 인증하는 코드를 추가 하였습니다.
  • 다음 routes.rb에 API 관련 URL을 추가합니다.
    1
    2
    3
    get 'api' => 'swagger#index'
    get 'api-docs' => 'swagger#api_docs'
    get 'api/:version/:api_name' => 'swagger#api_spec'

이렇게 구성하면 브라우저에서 api 를 조회하면 다음과 같이 기본 인증 화면이 나타납니다.

swagger_basic_auth

마치며

API 관련 문서 및 웹 기반 호출 환경을 구성하기 위해 Swagger를 사용하면서 발생한 보안 이슈에 대해 살펴보았고 이 문제를 Rails에서 필자가 자체적으로 해결한 방법에 대해 소개해드렸습니다.  시스템을 구성하면서 다양한 오픈소스를 활용하게 되는데 이때 여러가지 사항을 고려해야 합니다. 보안 관련 부분도 아주 중요한 영역입니다. 이렇게 사소한 부분이라도 적극적으로 개선하거나 자신의 시스템 환경에 맞게 구성하는 것이 시스템의 품질을 높이는 방법이지 않나 생각합니다.


Popit은 페이스북 댓글만 사용하고 있습니다. 페이스북 로그인 후 글을 보시면 댓글이 나타납니다.