티스토리 뷰

프로그래밍 공부/TIL : Rails Tutorial

Chapter 8 : Action Controller Overview

마음 따뜻한 개발자, 나른한 하루 2020. 7. 12. 03:24
이 글은 Rails 5.0 Guide 기준으로 작성됩니다.
https://guides.rubyonrails.org/v5.0/action_controller_overview.html

 

 

  • Action Controller Overview intro

컨트롤러의 작동 방식과 컨트롤러가 애플리케이션의 요청주기에 어떻게 적용되는지 배웁니다.

 

 

  • What Does a Controller Do?

Action Controller는 MVC 패턴 중 C를 지칭합니다. 라우팅이 요청에 사용할 컨트롤러를 결정한 후, 컨트롤러는 요청을 이해한 후 적절한 출력을 생성합니다.

 

대부분의 Restful 어플리케이션에서는 컨틀로러가 요청을 받고, 데이터를 처리 후 HTML 페이지 렌더링을 통해 결과를 보여줍니다.

만약 컨트롤러의 작동 방식이 조금 다르더라도 이에 문제되지는 않습니다.

 

컨트롤러는 Model과 View 사이를 이어주는 미들웨어 역할을 맡습니다. Model에 있는 데이터를 View에 띄어주거나, 저장 혹은 수정을 다룰수도 있습니다.

 

 

  • Controller Naming Convention

Controller 이름은 마지막 문장에 대해 복수명(pluralization) 표기가 권유됩니다. (엄격히 요구되는건 아님; ApplicationController)

 

레일즈의 CoC 규칙에 의거하여 컨트롤러를 생성 시, 라우터에도 기본적으로 URI 및 컨트롤러#액션이 자동으로 셋팅이 됩니다. (명칭 규칙만 잘 지킨다면 라우터에 :path 혹은 :controller 옵션에 대해서 따로 지정하지 않아도 됩니다.)

 

 

  • Methods and Actions

controller는 class 형태로 되어있고, controller 안에는 다양한 메소드들이 있습니다.

Application에서 요청을 받아낸다면, 라우터는 어떤 컨트롤러#액션으로 안내할건지 판단 후, 요청을 해당 Controller 내 액션으로 안내할 것입니다. 그리고 레일즈에서는 해당 이벤트에 대한 인스턴스를 만들어 내고, 메소드가 작동될 것입니다.

class ClientsController < ApplicationController
  def new
  end
end

위 코드를 예를 들어 만약 유저가  http://.../clients/new  로 접속 시, 레일즈에서는 ClientsController 컨트롤러의 인스턴스를 만들어내고, new 라는 이름의 메소드를 만들어 낼 것입니다.

 

참고로 위 코드에는 new 메소드에는 아무것도 없긴 하나, 정상적으로 잘 동작됩니다.

만약에 Controller 내 메소드에 아무 내용이 없다면 그냥 View(예시에서는 기본적으로 app/views/clients/new.html.erb)에 적힌 코드를 토대로 화면에 페이지가 보여집니다.

Controller 내 액션에는 접근 지정자가 나뉘어져 있는데, 기본으로는 public 지정자가 정해져 있고, 라우터에서 접속을 시도 시 public에 대해서만 접근이 가능합니다.

 

 

  • Parameters

Controller에서 유저 혹은 다른 parameters에 데이터를 보내고 싶을 경우가 있을겁니다. 웹에서는 parameter을 보내는 방법이 2가지가 존재합니다. :

1) URL을 통한 방법 (query string parameters) : 주로 URL의 ? 부터 시작하는 문법으로서 파라미터 표현이 시작됩니다.

2) POST parameters : POST 메소드 방식으로 데이터를 표현할 때에 있어 유저로부터 HTML form에서 받은 데이터가 넘어갈 때 볼 수 있는 표현법입니다.

class ClientsController < ApplicationController
  # HTTP의 GET 요청으로 부터 전달받은 query string parameters 을 이용하여 쓰이는 Action 입니다.
  # 이 Action의 URL 표현 방식은 아래와 같습니다 :
  # /clients?status=activated
  def index
    if params[:status] == "activated"
      @clients = Client.activated
    else
      @clients = Client.inactivated
    end
  end
 
  # POST parameters를 요청받아 쓰이는 Action 입니다.
  # 유저에게 form을 입력받고, 서버로 전송됩니다. 
  # URL 표현 방식은 "/clients" 이고, 데이터는 request body 형태로 보내질겁니다.
  def create
    @client = Client.new(params[:client])
    if @client.save
      redirect_to @client
    else
      # This line overrides the default rendering behavior, which
      # would have been to render the "create" view.
      render "new"
    end
  end
end

 

1. Hash and Array Parameters

1) 배열(Array)

params는 일차원 적인 키와 값에 한정되진 않을 뿐더러, 중첩 배열과 해시를 포함 할 수 있습니다. 값의 배열을 보내려면 키 쌍에 빈 대괄호 "[]"쌍을 추가하면 됩니다.

GET /clients?ids[]=1&ids[]=2&ids[]=3

 참고  원래 URL에서는 '/' 및 ']'과 같은 문자는 URL 표현에서 자동으로 인코딩이 되어 "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5d=3" 같이 표현됩니다.

그러나 Rails에서는 이를 자동으로 디코딩(해석)하기 때문에 인코딩/디코딩에 대한 문제는 걱정하지 않아도 됩니다. 하지만 수동으로 서버에 요청 시에는 이 부분에 유의해야 합니다.

 

2) 해시(Hash)

해시 표현 방식은 웹 페이지 내 form을 활용한 요청 등이 들어와야 합니다.

<form accept-charset="UTF-8" action="/clients" method="post">
  <input type="text" name="client[name]" value="Acme" />
  <input type="text" name="client[phone]" value="12345" />
  <input type="text" name="client[address][postcode]" value="12345" />
  <input type="text" name="client[address][city]" value="Carrot City" />
</form>

 

만약 위 form이 전송될 경우, 서버에서는 아래와 같은 Hash 형태의 parameter을 받게됩니다.

{ "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }

여기서 params[:client][:address] 는 Nested hash의 개념이 쓰였습니다.

 

2. JSON parameters

요청의 "Content-Type"헤더가 "application/json"으로 설정된 경우, Rails는 자동으로 파라미터를 params 해시에 로드하여 이전 방법과 다르지 않게 액세스 할 수 있습니다.

 

만약, 아래와 같은 JSON 내용을 서버에 요청을 보낸다면, 서버는 params[:company] = { "name" => "acme", "address" => "123 Carrot Street" } 와 같은 JSON 타입의 내용을 받게됩니다.

{ "company": { "name": "acme", "address": "123 Carrot Street" } }

 

만약 JSON Parameter에서 루트 객체이름(위 예시에서는 company)을 숨기고 싶을 경우, initializer에 config.wrap_parameters 정의 혹은 controller에서 wrap_parameters 을 호출하면 됩니다.

{ "name": "acme", "address": "123 Carrot Street" }

wrap_parameters 설정 및 JSON 형식을 조금 바꾸어, 위의 형식을 서버에 요청하게 되면 매개 변수는 컨트롤러 이름 및 컨트롤러가 가진 에트리뷰트에 따라 복제/랩핑됩니다.

{ "name": "acme", "address": "123 Carrot Street", "company"=> { "name": "acme", "address": "123 Carrot Street" } }

 

3. 라우팅 파라미터

params 해시는 항상 :controller 및 :action 키를 포함하지만 이러한 값에 액세스하려면 controller_name 및 action_name 메소드를 대신 사용해야 합니다. 라우팅에 의해 정의 된 다른 매개 변수 (:id 등)도 사용할 수 있습니다.

 

라우터에서 아래와 같이 :status 매개변수를 받는 URI를 정의할 수 있습니다. 

get '/clients/:status' => 'clients#index', foo: 'bar'

위 URL에 규칙에 따라, 만약 유저가 /clients/active 라고 접근 시, params[:status]는 active 라는 매개변수가 담겨집니다. 또한 params[:foo]는 "bar"라는 값을 가집니다. 또한 컨트롤러에서는 params[:action] = "index" 및 params[:controller] = "clients" 라는 매개변수를 받게 됩니다.

 

4. default_url_options

컨틀로러 내에서 default_url_options 메소드를 통해 글로벌 parameters을 생성할 수 있습니다.

Key는 반드시 symbol 형태여야 합니다.

class ApplicationController < ActionController::Base
  def default_url_options
    { locale: I18n.locale }
  end
end

위의 예시 메소드인 ApplicationController에서 default_url_options와 같이 정의하면 이 기본값이 모든 URL 생성에 사용됩니다. 이 방법은 특정 컨트롤러에서 정의 할 수도 있습니다.

 

5. Strong Parameters

외부로 부터 Controller에 데이터를 요청받을 때 있어, strong parameter에 명시된 에트리뷰트만이 컨트롤러에서 받아내어 처리할 때 사용됩니다. 이 기법은 데이터베이스에 create 및 update 시, 외부에서 의도치 않은(악의적인) 요청으로 인한 취약점 이슈에 대해 예방을 해야할 때 많이 쓰입니다.

class PeopleController < ActionController::Base
  # This will raise an ActiveModel::ForbiddenAttributes exception
  # because it's using mass assignment without an explicit permit
  # step.
  def create
    Person.create(params[:person])
  end
 
  # This will pass with flying colors as long as there's a person key
  # in the parameters, otherwise it'll raise a
  # ActionController::ParameterMissing exception, which will get
  # caught by ActionController::Base and turned into that 400 Bad
  # Request reply.
  def update
    person = current_account.people.find(params[:id])
    person.update!(person_params)
    redirect_to person
  end
 
  private
    # Using a private method to encapsulate the permissible parameters
    # is just a good pattern since you'll be able to reuse the same
    # permit list between create and update. Also, you can specialize
    # this method with per-user checking of permissible attributes.
    def person_params
      params.require(:person).permit(:name, :age)
    end
end

params에 해당되는 에트리뷰트들에 대해 만이 서버에서 요청 들어온 작업을 처리합니다, 이를 화이트리스트 라고 합니다.

 

1) Permitted Scalar Values

params.permit(:id)

만약 위와 같이 :id 키가 permit 내에 있을 경우, 화이트리스트에 추가됩니다.

 

화이트리스트 추가에 있어 array, hashes 등과 같은 객체 표현은 제한됩니다.

 

scalar type에 있어 허용되는 변수 타입은 String, Symbol, NilClass, Numeric, TrueClass, FalseClass, Date, Time, DateTime, StringIO, IO, ActionDispatch::Http::UploadedFile, and Rack::Test::UploadedFile 입니다.

 

params 값이 배열이어야 함을 선언하려면 키를 빈 배열에 매핑하십시오.

params.permit(id: [])

 

매개변수의 전체 해시를 허용하려면 permit! 메소드를 활용하면 됩니다.

# Parameters: {"log_entry"=>{"title"=>"hello", "content"=>"world"}}

params.require(:log_entry).permit!

 

2) 중첩 매개 변수

아래 permit과 같이 중첩 매개변수에 활용할 수 있습니다.

params.permit(:name, { emails: [] },
              friends: [ :name,
                         { family: [ :name ], hobbies: [] }])

name, emails, friends에 대해 화이트리스트가 추가된 예시입니다.

 

- friends는 특정 속성을 가진 자원의 배열이 되고, email은 배열을 허용하는 스칼라 값이 됩니다.

- hobbies 속성은 Array 형태의 scalar values에 대해 허용됩니다.

- family 속성은 name 만을 갖는 것으로 제한됩니다.

 

3) More Example

:new Action에 허용 된 속성을 사용할 수도 있습니다.

여기서 발생하는 raise 에러 문제는 루트 키에서 require를 사용할 :new Action이 존재하지 않기 때문입니다.

# using `fetch` you can supply a default and use
# the Strong Parameters API from there.
params.fetch(:blog, {}).permit(:title, :author)

 

Model에서 지원하는 메소드인 accepts_nested_attributes_for를 통해 연계된 데이터의 삭제(destroy) 혹은 수정(update)도 함께 해낼 수 있습니다.

# permit :id and :_destroy
params.require(:author).permit(:name, books_attributes: [:title, :id, :_destroy])

 

정수 키가있는 해시는 다르게 취급되며 속성을 마치 자식 에트리뷰트 인 것처럼 선언 할 수 있습니다. Model에서 has_many로 정의된 타 모델과 연계시키는 accepts_nested_attributes_for과 함께 사용할 수 있습니다.

# To whitelist the following data:
# {"book" => {"title" => "Some Book",
#             "chapters_attributes" => { "1" => {"title" => "First Chapter"},
#                                        "2" => {"title" => "Second Chapter"}}}}
 
params.require(:book).permit(:title, chapters_attributes: [:title])

 

6. Outside the Scope of Strong Parameters

Strong Parameter는 API에서 가장 일반적으로 사용되는 사례를 염두하여 설계되었습니다. 그렇다보니 모든 화이트리스트 문제를 처리할 수는 없습니다. 그러나 상황에 맞게 API를 자신의 코드와 쉽게 혼합 할 수 있습니다.

 

 

  • Session

Application에는 각 사용자에 대한 세션이있어, 요청간에 유지 될 소량의 데이터를 저장할 수 있습니다. 세션은 Controller 및 View 에서만 사용 가능하며, 여러 가지 다른 스토리지 메커니즘 중 하나를 사용할 수 있습니다.

  • ActionDispatch::Session::CookieStore  유저의 모든 것을 저장합니다.
  • ActionDispatch::Session::CacheStore  Rails cache 내 데이터를 저장합니다.
  • ActionDispatch::Session::ActiveRecordStore  Active Record(Database)가 가진 데이터를 저장합니다.
  • ActionDispatch::Session::MemCacheStore  memcached cluster을 저장합니다. (이는 레거시 로서, CacheStore 사용을 고려한다.)

모든 세션이 가진 쿠키를 통해 세션이 저장되고, 각 세션에는 Unique한 ID를 가집니다. (레일즈에서는 보안적인 이슈 때문에 URL에 있는 session ID를 사용하는걸 권장하지 않습니다.)

 

대부분의 세션들은 서버가 관리하고 이 ID들을 통해 세션을 찾아냅니다.

 

만약 유저 세션에 있어 장시간 보관을 할 필요 없거나, 중요하지 않은 데이터를 가지고 있다면 ActionDispatch::Session::CacheStore 을 사용할 것이 권유됩니다. 이는 캐시 구현을 사용하여 세션이 저장됩니다. 추가 설정이나 관리없이 세션을 저장하기 위해 기존 캐시 인프라를 사용할 수 있다는 장점이 있습니다. 물론 단점은 세션이 일시적이며 언제든지 사라질 수 있다는 것입니다.

 

세션에 대한 설정은  config/initializers/session_store.rb  에서 해낼 수 있습니다.

## config/initializers/session_store.rb

# Use the database for sessions instead of the cookie-based default,
# which shouldn't be used to store highly confidential information
# (create the session table with "rails g active_record:session_migration")
# Rails.application.config.session_store :active_record_store

 

세션 키에 대해서도 설정할 수 있고, 설정 위치 역시 위와 동일합니다.

## config/initializers/session_store.rb

# Be sure to restart your server when you modify this file.
Rails.application.config.session_store :cookie_store, key: '_your_app_session'

 

특정 Domain Key에 대한 설정도 동일한 위치에서 해낼 수 있습니다.

## config/initializers/session_store.rb

# Be sure to restart your server when you modify this file.
Rails.application.config.session_store :cookie_store, key: '_your_app_session', domain: ".example.com"

 

세션 데이터 축척에 있어  config/secrets.yml  에서 secret key를 통해 세션 파일을 복호화 해낼 수 있습니다.

## config/secrets.yml

# Be sure to restart your server when you modify this file.
 
# Your secret key is used for verifying the integrity of signed cookies.
# If you change this key, all old signed cookies will become invalid!
 
# Make sure the secret is at least 30 characters and all random,
# no regular words or you'll be exposed to dictionary attacks.
# You can use `rails secret` to generate a secure secret key.
 
# Make sure the secrets in this file are kept private
# if you're sharing your code publicly.
 
development:
  secret_key_base: a75d...
 
test:
  secret_key_base: 492f...
 
# Do not keep production secrets in the repository,
# instead read values from the environment.
production:
  secret_key_base: <%= ENV["SECRET_KEY_BASE"] %>

 

1. Accessing the Session

Conteroller에서 인스턴스 메소드를 통해 접근을 해볼 수 있습니다.

 참고  세션은 느리게(lozily) load 됩니다. Action 코드에서 세션에 접근하지 않으면 세션이 load되지 않습니다. 따라서 세션을 비활성화 할 필요가 없으며, 액세스하지 않으면 세션에 접근하지 않는 채로 작업이 수행됩니다.

 

세션은 hash와 같이 key/value 를 가진 채 저장되어 있습니다.

class ApplicationController < ActionController::Base
 
  private
 
  # Finds the User with the ID stored in the session with the key
  # :current_user_id This is a common way to handle user login in
  # a Rails application; logging in sets the session value and
  # logging out removes it.
  def current_user
    @_current_user ||= session[:current_user_id] &&
      User.find_by(id: session[:current_user_id])
  end
end

 

세션에 무언가를 저장하고자 한다면, 아래와 같이 key에 value를 초기화 해주세요.

class LoginsController < ApplicationController
  # "Create" a login, aka "log the user in"
  def create
    if user = User.authenticate(params[:username], params[:password])
      # Save the user ID in the session so it can be used in
      # subsequent requests
      session[:current_user_id] = user.id
      redirect_to root_url
    end
  end
end

 

세션에서 무언가를 지우고자 한다면, session Key에 대해 nil(NULL) 처리를 해주면 됩니다.

class LoginsController < ApplicationController
  # "Delete" a login, aka "log the user out"
  def destroy
    # Remove the user id from the session
    @_current_user = session[:current_user_id] = nil
    redirect_to root_url
  end
end

 

2. The Flash

어떤 요청에 대해 처리 후, 다음 요청이 왔을 때 오류메세지 등을 전달할 때 유용한 기능합니다.

만약 로그아웃에 대한 작업을 할 경우, 다음 요청에 있어 아래와 같이 flash 메세지가 띄어집니다.

class LoginsController < ApplicationController
  def destroy
    session[:current_user_id] = nil
    flash[:notice] = "You have successfully logged out."
    redirect_to root_url
  end
end

 

어떤 작업이 이루어 진 후, 다른 페이지로 넘어갈 때에 있어서도 flesh 기능을 사용할 수 있으며, flesh에는 아래와 같은 기본 명칭이 있습니다 :

:notice, :alert, :flash

redirect_to root_url, notice: "You have successfully logged out."
redirect_to root_url, alert: "You're stuck here!"
redirect_to root_url, flash: { referral_code: 1234 }
<% flash.each do |name, msg| -%>
  <%= content_tag :div, msg, class: name %>
<% end -%>

 

위에 설명한 3가지(notice, alert, flash) 외에도 아래와 같이 커스터마이징 해서 쓸 수 있습니다.

<% if flash[:just_signed_up] %>
  <p class="welcome">Welcome to our site!</p>
<% end %>

 

만약 flash 메세지를 다음 액션 작업으로 넘길 때 유지시키고 싶으면 아래와 같이 keep 메소드를 통해 보존해낼 수 있습니다.

class MainController < ApplicationController
  # Let's say this action corresponds to root_url, but you want
  # all requests here to be redirected to UsersController#index.
  # If an action sets the flash and redirects here, the values
  # would normally be lost when another redirect happens, but you
  # can use 'keep' to make it persist for another request.
  def index
    # Will persist all flash values.
    flash.keep
 
    # You can also use a key to keep only some kind of value.
    # flash.keep(:notice)
    redirect_to users_url
  end
end

 

본래 flash는 다음 Action에서 메세지를 띄우는 문법이나, 동일한 Action에서 사용하고 싶을 경우, now 메소드를 사용하면 됩니다.

class ClientsController < ApplicationController
  def create
    @client = Client.new(params[:client])
    if @client.save
      # ...
    else
      flash.now[:error] = "Could not save client"
      render action: "new"
    end
  end
end

 

 

  • Cookies

Application에는 클라이언트로 부터 받은 소규모 데이터인 Cookie를 가지고 있는데, 이는 요청과 세션에 걸쳐 지속됩니다.

Rails에서는 Cookie 메소드를 통해 쉽게 쿠키에 접근할 수 있도록 도와주는 메소드가 있으며, 이는 해쉬와 비슷합니다. (동작 역시 hash와 유사하게 작동됩니다.)

class CommentsController < ApplicationController
  def new
    # Auto-fill the commenter's name if it has been stored in a cookie
    @comment = Comment.new(author: cookies[:commenter_name])
  end
 
  def create
    @comment = Comment.new(params[:comment])
    if @comment.save
      flash[:notice] = "Thanks for your comment!"
      if params[:remember_name]
        # Remember the commenter's name.
        cookies[:commenter_name] = @comment.author
      else
        # Delete cookie for the commenter's name cookie, if any.
        cookies.delete(:commenter_name)
      end
      redirect_to @comment.article
    else
      render action: "new"
    end
  end
end

하지만 세션과 다른 부분이 있다면, 세션을 지우기 위해서는 value에 nil이라고 표현하면 됐지만, 쿠키는 cookies.delete(:key) 라고 해줘야 합니다.

 

Rails에서는 민감한 데이터를 저장함에 있어 cookie jar과 encrypted cookie jar가 사용됩니다.

생성된 cookie jar에 암호화 서명을 추가하여 무결성을 보호합니다. 암호화 된 cookie jar은 value 또한 암호화 되고, 그로인해 사용자는 해당 Cookie 값을 읽어내지 못합니다.

 

 

  • Rendering XML and JSON data

ActionController를 사용하면 XML 또는 JSON 데이터를 매우 쉽게 렌더링 할 수 있습니다. scaffold 기능을 사용하여 컨트롤러를 생성 한 경우 다음과 같습니다.

class UsersController < ApplicationController
  def index
    @users = User.all
    respond_to do |format|
      format.html # index.html.erb
      format.xml  { render xml: @users}
      format.json { render json: @users}
    end
  end
end

위의 기능을 보면, xml 및 json 같은 경우는 단순히 @users 객체를 넘김으로서 표현이 된다는걸 볼 수 있습니다.

(원래는 .to_xml 혹은 .to_json 메소드를 활용해야 합니다.)

 

 

  • Filters

Filter는 컨트롤러 작업 전(before), 후(after), 매번(around) 실행되는 메소드입니다. (마치 콜백과 같은?)

 

필터는 상속이 된다는 특징이 있는데, 상속 특징을 이용해서 ApplicationController에서 필터를 설정하면 모든 컨트롤러에서 필터가 실행됩니다.

 

또한, 아래와 같이 filter을 통해 일부 Action 내 코드 작업을 건너뛰게 할 수 있습니다.

class LoginsController < ApplicationController
  skip_before_action :require_login, only: [:new, :create]
end

 

Before filter는 중간에 요청주기(request cycle)를 중단시킬 수 있습니다.

아래 코드의 Before Filter는 작업을 실행하기 위해 사용자가 로그인해야하는 필터입니다. 다음과 같이 필터 방법을 정의 할 수 있습니다.

class ApplicationController < ActionController::Base
  before_action :require_login
 
  private
 
  def require_login
    unless logged_in?
      flash[:error] = "You must be logged in to access this section"
      redirect_to new_login_url # halts request cycle
    end
  end
end

 

1. After Filters and Around Filters

before_action filter 외에도 Action 작업이 실행된 후 혹은 작업 전/후 둘다에 대해서도 filter을 정의내릴 수 있습니다.

 

앞전에 before에 대해 알아봤는데, before 외에도 이와 비슷한 after filter 이 존재합니다. 그런데 after filter은 Action 내 코드가 실행된 후에 작동되기 때문에 클라이언트로 보내려는 응답 데이터에 접근할 수 있는 특징이 있습니다.

 

around filter은 Action 작업 전/후 둘다에 대해 작업이 이루어지는데, around filter 내 코드에는 yield 문법을 통해 코드 작동을 정의내려야 합니다.

 

예를 들어, 웹사이트에서 트랜잭션 응답 구분을 통해 데이터를 사전에 쉽게 볼 수 있는 예시의 코드입니다.

class ChangesController < ApplicationController
  around_action :wrap_in_transaction, only: :show
 
  private
 
  def wrap_in_transaction
    ActiveRecord::Base.transaction do
      begin
        yield
      ensure
        raise ActiveRecord::Rollback
      end
    end
  end
end

 

2. Other Ways to Use Filters

필터를 사용하는 가장 일반적인 방법은 private 메소드를 작성하고,  *_action 코드 추가를 통해 filter을 사용하는 것인데, 이에대한 방법이 두 가지가 있습니다.

 

1) 첫 번째는 *_action 메소드와 함께 직접 블록을 사용하는 것입니다. 블록은 컨트롤러를 인수로 받습니다.

class ApplicationController < ActionController::Base
  before_action do |controller|
    unless controller.send(:logged_in?)
      flash[:error] = "You must be logged in to access this section"
      redirect_to new_login_url
    end
  end
end

이 경우 필터는 logs_in? 메소드는 private 이며 필터가 컨트롤러 범위에서 실행되지 않습니다. 이 특정 필터를 구현하는 데 권장되는 방법은 아니지만, 보다 간단한 경우에는 유용 ​​할 수 있습니다.

 

2) 두 번째 방법은 필터링을 처리하기 위해 클래스를 사용하는 것입니다 (실제로 일치하는 메소드에 응답하는 모든 객체가 수행함).

이는 더 복잡하고 다른 두 가지 방법을 사용하여 읽기 쉽고 재사용 가능한 방식으로 구현할 수없는 경우에 유용합니다. 예를 들어, 클래스를 사용하기 위해 로그인 필터를 다시 작성할 수 있습니다.

class ApplicationController < ActionController::Base
  before_action LoginFilter
end
 
class LoginFilter
  def self.before(controller)
    unless controller.send(:logged_in?)
      controller.flash[:error] = "You must be logged in to access this section"
      controller.redirect_to controller.new_login_url
    end
  end
end

이 필터는 컨트롤러 범위에서 실행되지 않지만 컨트롤러를 인수로 전달하기 때문에 이 필터에 좋은 예는 아닙니다.

필터 클래스는 필터와 이름이 같은 메소드를 구현해야하므로 before_action 필터의 경우 클래스는 before Method 등을 구현해야합니다. around 메소드는 코드를 실행하기 위해 yield 문법이 명시되어져야 합니다.

 

 

  • Request Forgery Protection

Cross-site request forgery(이하 CSRF; 사이트 간 요청 위조) 란 다른 사이트에서 요청을 하도록 사용자를 속여 사용자의 권한 없이 해당 사이트의 데이터베이스에 무단으로 접근해서 데이터를 추가, 수정 또는 삭제할 수 있는 보안 취약점을 노린 공격입니다.

이를 피하기위한 첫 번째 단계는 모든 삭제작업 (만들기, 업데이트 및 삭제)이 GET이 아닌 타 Method요청으로만 액세스 할 수 있도록하는 것입니다. (RESTful 규칙을 따르는 경우 이미 명시된 Method 요청에 대해서만 작업을 하도록 설계되어 있을겁니다.)

그러나 악의적 인 사이트는 여전히 GET 이외의 요청을 귀하의 사이트로 매우 쉽게 보낼 수 있으며, 이는 위조 방지 요청이 들어오는 곳입니다. 이름에서 알 수 있듯이 위조 된 요청으로부터 보호합니다.

 

만약 Rails 내 View 파일에서 아래와 같이 코드를 짠 후 :

<%= form_for @user do |f| %>
  <%= f.text_field :username %>
  <%= f.text_field :password %>
<% end %>

 

크롬 태그분석기(F12)를 통해 홈페이지 태그를 보면 아래와 같은 결과를 볼 수 있습니다.

<form accept-charset="UTF-8" action="/users/1" method="post">
<input type="hidden"
       value="67250ab105eb5ad10851c00a5621854a23af5489"
       name="authenticity_token"/>
<!-- fields -->
</form>

레일즈에서는 모든 폼헬퍼 태그에 있어, 자동으로 token 이란게 생성됩니다.

token을 통해 CSRF 취약점을 예방합니다.

 

form_authenticity_token은 유효한 인증 토큰을 생성합니다. 이는 커스텀 Ajax 호출과 같이 Rails가 자동으로 추가하지 않는 곳에서 유용합니다.

 

 

 

  • The Request and Response Objects

모든 컨트롤러에는 현재 실행중인 요청주기와 관련된 응답 객체와 요청을 가리키는 두 가지 접근 자 메서드가 있습니다. 요청 메소드는 ActionDispatch :: Request의 인스턴스를 포함하고, 응답 메소드는 클라이언트로 다시 전송 될 내용을 나타내는 응답 오브젝트를 리턴합니다.

 

1. The response Object

The request object contains a lot of useful information about the request coming in from the client. To get a full list of the available methods, refer to the API documentation. Among the properties that you can access on this object are:

 

요청 객체에는 클라이언트에서 들어오는 요청에 대한 유용한 정보가 많이 있습니다. 사용 가능한 메소드의 전체 목록을 얻으려면 API 문서를 참조하십시오. 이 개체에서 액세스 할 수있는 속성은 다음과 같습니다.

request 속성 특징
host 요청에 사용된 hostname
domain(n=2) 오른쪽에서 시작하여 호스트 이름의 첫 번째 n 세그먼트
format 클라이언트가 요청한 컨텐츠 유형
method 요청에 사용된 HTTP Method
get?, post?, patch?, put?, delete?, head? HTTP 메소드가 GET / POST / PATCH / PUT / DELETE / HEAD 인 경우 true 리턴 (Method 유형검사)
headers 요청과 관련된 헤더가 포함 된 해시를 반환
port 요청에 사용된 Port 번호
protocol 사용 된 프로토콜에 "://"를 더한 문자열 (예 : "http://")을 반환
query_string URL의 검색어 문자열 부분 (예: "?"뒤의 모든 것)
remote_ip 사용자의 IP주소
url 요청에 사용된 모든 URL 주소

 

path_parameters, query_parameters, and request_parameters

Rails는 query string 또는 post body의 일부로 전송되는 여부에 관계없이 매개 변수 Hash에서 요청과 함께 전송 된 모든 매개 변수를 수집합니다. 요청 객체에는 세 가지 접근자(path, query, request parameters)가 있으며 이러한 접근자는 사용자가 어디에서 왔는지에 따라 이러한 매개 변수에 액세스 할 수 있습니다.

query_parameters 해시는 쿼리 문자열의 일부로 전송 된 매개 변수를 포함하고 request_parameters 해시는 포스트 본문의 일부로 전송 된 매개 변수를 포함합니다.

path_parameters 해시는 라우팅에 의해 특정 컨트롤러 및 동작으로 이어지는 경로의 일부로 인식 된 매개 변수를 포함합니다.

 

2. The request Object

응답 오브젝트는 일반적으로 직접 사용되지 않지만, 조치 실행 및 사용자에게 다시 전송되는 데이터 렌더링 중 빌드되지만 때로는 after filter에서와 같이 응답에 접근하는 것이 유용 합니다.

직접. 이러한 접근 자 메소드 중 일부에는 setters가 있으므로 값을 변경할 수 있습니다.

response 속성 특징
body 클라이언트로 다시 전송되는 데이터 문자열, 대부분 표현방식은 HTML
status HTTP 응답코드 (200: 성공, 300: URL Redirection, 400: 클라이언트 오류, 500: 서버 에러)
location 클라이언트가 리디렉션되는 URL
content_type 응답의 컨텐츠 유형
charset 응답에 사용되는 문자 설정, 기본값은 "utf-8"
headers 서버 자체에 대한 정보, 응답에 대한 부가적인 정보를 포함하는 해더

 

Setting Custom Headers

응답에 대한 사용자 정의 헤더를 설정하려면 response.headers를 사용하십시오. headers 속성은 헤더 이름과 값을 서로 매핑해주는 Hash이며, Rails는 이를 자동으로 설정해줍니다. 헤더를 추가하거나 변경하려면 다음과 같이 response.headers에 초기화 해주면 됩니다.

response.headers["Content-Type"] = "application/pdf"

 

 

  • HTTP Authentications

Rails에는 두 가지 내장 HTTP 인증 메커니즘이 제공됩니다.

 

1. HTTP Basic Authentication

HTTP 기본 인증은 대부분의 브라우저 및 기타 HTTP 클라이언트에서 지원되는 인증 체계입니다.

브라우저의 HTTP 기본 대화 창에 사용자 이름과 비밀번호를 입력해야만 사용할 수 있는 페이지를 예로들 수 있습니다. 내장 인증 사용은 매우 쉽고, 아래 코드와 같이 Controller 내에서 하나의 메소드만 정의내려주면 됩니다.

class AdminsController < ApplicationController
  http_basic_authenticate_with name: "humbaba", password: "5baa61e4"
end

2. HTTP Digest Authentication

네트워크를 통해 암호화되지 않은 암호를 보내지 않아도 되므로 기본 인증보다 우수합니다 (HTTP 기본 인증은 HTTPS를 통해 안전하지만).

class AdminsController < ApplicationController
  USERS = { "lifo" => "world" }
 
  before_action :authenticate
 
  private
 
    def authenticate
      authenticate_or_request_with_http_digest do |username|
        USERS[username]
      end
    end
end

위의 예에서 볼 수 있듯이 authenticate_or_request_with_http_digest 블록은 사용자 이름(username) 이라는 하나의 인수 만 취합니다. 그리고 블록은 암호를 반환합니다.

authenticate_or_request_with_http_digest 에서 false또는 nil에서 반환하면 인증이 실패합니다.

 

 

  • Streaming and File Downloads

HTML 페이지를 렌더링하는 대신 파일을 사용자에게 보내려고 할 수 있습니다. Rails의 모든 컨트롤러에는 send_data및 send_file 메소드 가 존재하며, 이 메소드들은 모두 클라이언트로 데이터를 스트리밍합니다.

send_file 메소드를 통해 서버에서 파일 이름 및 내용을 스트리밍 할 수 있습니다.

 

send_data 메소드를 통해 클라이언트에 파일을 보내는 예시코드 입니다 :

require "prawn"
class ClientsController < ApplicationController
  # Generates a PDF document with information on the client and
  # returns it. The user will get the PDF as a file download.
  def download_pdf
    client = Client.find(params[:id])
    send_data generate_pdf(client),
              filename: "#{client.name}.pdf",
              type: "application/pdf"
  end
 
  private
 
    def generate_pdf(client)
      Prawn::Document.new do
        text client.name, align: :center
        text "Address: #{client.address}"
        text "Email: #{client.email}"
      end.render
    end
end

위 예제의 download_pdf Action은 실제로 PDF 문서를 생성하고, 이를 문자열로 반환하는 private 접근지정자가 설정된 generate_pdf Action을 호출합니다. 이 문자열은 사용자에게 스트리밍 되면서 파일 다운로드가 이루어지고, 파일 이름이 사용자에게 제안됩니다. 때떄로, 파일을 사용자에게 스트리밍 할 때 파일 다운로드를 원치않을 수 있는데, 파일을 다운로드를 비활성화 하려면 :disposition 옵션을 "inline"으로 설정하면 됩니다. 이 옵션의 반대 및 기본값은 "attachment" 입니다.

 

1. Sending File

만약 클라이언트에 보내고자 하는 파일이 서버에 이미 존재할 경우, send_file 메소드를 활용하면 됩니다.

class ClientsController < ApplicationController
  # Stream a file that has already been generated and stored on disk.
  def download_pdf
    client = Client.find(params[:id])
    send_file("#{Rails.root}/files/clients/#{client.id}.pdf",
              filename: "#{client.name}.pdf",
              type: "application/pdf")
  end
end

This will read and stream the file 4kB at the time, avoiding loading the entire file into memory at once. You can turn off streaming with the :stream option or adjust the block size with the :buffer_size option.

 

전체 파일을 한 번에 메모리에 load하지 않아도 파일을 4kb를 읽고 스트리밍 할 수 있습니다. :stream 옵션으로 스트리밍을 끄거나 :buffer_size 옵션으로 블록 크기를 조정할 수 있습니다.

 

:type 옵션을 지정하지 않으면 :filename 에 지정된 파일 확장자에서 추측됩니다. 컨텐츠 유형이 extension에 등록되지 않은 경우, application/octet-stream이 사용됩니다.

 

 참고 1  클라이언트에서 들어오는 데이터 (매개 변수, 쿠키 등)를 사용하여 디스크에서 파일을 찾을 때 의도치 않은 파일에 액세스 할 수 있는 보안 위험이 있습니다.

 참고 2  정적 파일은 Rails 프로젝트 내 public 폴더에 보관할 수 있으며, Rails를 통해 정적 파일을 스트리밍하지 않는 것이 좋습니다. 사용자가 Apache 또는 다른 웹 서버를 사용하여 직접 파일을 직접 다운로드하여 요청이 불필요하게 전체 Rails 스택을 거치지 않도록하는 것이 훨씬 더 효율적입니다.

 

2. RESTful Downloads

send_data 메소드는 는 정상적으로 작동하지만, 파일 다운로드에 대해 별도의 조치를 갖는 RESTful 애플리케이션을 작성하는 경우엔 사용하지 않아도 됩니다. REST 용어에서, 위 예제의 PDF 파일은 클라이언트 자원의 다른 표현으로 인식될 수 있습니다. Rails는 "RESTful downloads"를 수행하는 쉽고 세련된 방법을 제공합니다. 다음은 스트리밍없이 PDF 다운로드가 이루어지는 show Action 코드 예시입니다.

class ClientsController < ApplicationController
  # The user can request to receive this resource as HTML or PDF.
  def show
    @client = Client.find(params[:id])
 
    respond_to do |format|
      format.html
      format.pdf { render pdf: generate_pdf(@client) }
    end
  end
end

 

이 예제가 작동하려면 PDF MIME 유형을 Rails에 추가해야합니다.  config/initializers/mime_types.rb  파일에 아래 코드를 추가하면 됩니다.

 부록  MIME TYPE 개념

Mime::Type.register "application/pdf", :pdf

 

mime_types.rb에 내용을 입력 후, config/routes.rb 에서 Client에 보낼 PDF 파일 버전에 대해 아래와 같이 URL로 표현하면 됩니다.

GET /clients/1.pdf

 

3. Live Streaming of Arbitrary Data

Rails에서는 응답 객체에서 원하는 파일을 스트리밍 할 수 있습니다. ActionController::Live 모듈을 사용하면 브라우저와 지속적인 연결을 만들 수 있습니다. 이 모듈을 사용하면 특정 시점에 임의의 데이터를 브라우저로 보낼 수 있습니다.

 

1) Incorporating Live Streaming

Controller 내에 ActionController::Live 을 추가 시, 해당 Controller 내 모든 Action에서 파일을 스트리밍 할 수 있습니다.

class MyController < ActionController::Base
  include ActionController::Live
 
  def stream
    response.headers['Content-Type'] = 'text/event-stream'
    100.times {
      response.stream.write "hello world\n"
      sleep 1
    }
  ensure
    response.stream.close
  end
end

다만, 위 코드에 보시다 싶이 주의할 점이 있다면, 스트리밍 작업이 끝날 경우 작업을 끝마치는 코드가 있어야 합니다.

또한 스트리밍 전 content type을 text/event-stream 로 설정해줘야 합니다.

 

2) Streaming Considerations

임의의 데이터 스트리밍은 매우 강력한 기능입니다. 이전 예에서와 같이 응답 스트림을 통해 언제, 무엇을 전송할 것인지 선택할 수 있습니다. 그러나 다음 사항도 참고해야 합니다.

  • 각 응답 스트림은 새 스레드를 작성하고 기존의 스레드에서 스레드 로컬 변수를 copy 합니다. 스레드 로컬 변수가 너무 많으면 성능이 떨어질 수 있습니다. 마찬가지로 많은 수의 스레드도 성능을 방해 할 수 있습니다.
  • 응답 스트림을 닫지 않으면 해당 소켓이 영원히 열린 상태로 유지됩니다. response 스트림을 사용할 때마다, 작업이 끝날 시 close 메소드를 호출하세요.
  • WEBrick 서버는 모든 응답을 버퍼링하므로, ActionController::Live 이 작동되지 않습니다. 응답을 자동으로 버퍼링하지 않는 웹 서버를 사용해야합니다.

 

 

 

  • Log Filtering

Rails는 각 환경에 대한 로그 파일을 log폴더에 보관합니다. 이들은 실제로 응용 프로그램에서 진행중인 작업을 디버깅 할 때 매우 유용하지만 실제 응용 프로그램에서는 모든 정보가 로그 파일에 저장되는 것을 원하지 않을 수 있습니다.

 

1. Parameters Filtering

Application configuration 에서 config.filter_parameters에 추가하여 로그 파일에서 민감한 요청 매개 변수를 필터링 할 수 있습니다. 이 매개 변수는 로그에 [FILTERED]로 표시됩니다.

config.filter_parameters << :password

 

2. Redirects Filtering

Sometimes it's desirable to filter out from log files some sensitive locations your application is redirecting to. You can do that by using the config.filter_redirect configuration option:

 

때로는 응용 프로그램이 리디렉션되는 민감한 위치를 로그 파일에서 필터링 해야합니다.

config.filter_redirect 구성 옵션을 사용하여 이를 수행 할 수 있습니다.

## 일반적인 방식
config.filter_redirect << 's3.amazonaws.com'

## string, 정규식 둘 다를 활용한 방식 
config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]

위에 일치하는 URL은 로그에 '[FILTERED]' 로 표시됩니다.

 

 

  • Rescue

Application에서 버그가 포함되는 등으로 인해 예외처리가 발생할 때가 있습니다. 예를 들어, 사용자가 데이터베이스에 더 이상 존재하지 않는 리소스에 대한 링크를 따라 가면 Active Record에서 ActiveRecord::RecordNotFound 예외가 발생합니다.

 

Rails 기본 예외 처리는 모든 예외에 대해 response code로 500(서버 오류) 메시지를 표시합니다. 요청이 Local로 이루어진 경우 추적 및 (에러에 대해) 일부 정보가 표시되므로, 무엇이 잘못되었는지 파악하고 처리 할 수 ​​있습니다.

 

원격 요청인 경우, Rails는 사용자에게 간단한 "500 Server Error" 메시지를 표시하거나, 라우팅 에러인 경우(페이지 없음 등) "404 Not Found"를 표시합니다.

 

때때로 이러한 오류가 발생하는 방식과 사용자에게 표시되는 방식을 사용자 정의 할 수 있습니다. Rails 애플리케이션에서 사용 가능한 몇 가지 레벨의 예외 처리가 있습니다.

 

1. The Default 500 and 404 Templates

400 및 500 에러에 대해 사용자에게 화면에 띄어져서 보여줘야 할 경우, 이 화면에 보여질 내용에 대해 개발자가 따로 꾸며줄 수 있습니다.

public 폴더 내에 존재하는 404.html, 500.html 파일을 통해 꾸밀 수 있습니다.

다만, public 폴더 내 존재하는 html 파일들은 정적 화면을 띄어주는 파일이고, .erb 와 같은 확장자를 사용을 못하며, assets에 설정된 scss 및 coffeescript 사용, layouts 양식 사용이 제한됩니다.

 

2. rescue_from

오류를 잡을 때 좀 더 정교하게 작업 하려면 전체 컨트롤러와 하위 클래스에서 특정 유형 (또는 여러 유형)의 예외를 처리하는 rescue_from 사용할 수 있습니다.

 

rescue_from 에 의해 발견 된 예외가 발생하면 예외 오브젝트가 핸들러로 전달됩니다. 핸들러는 메소드 또는 :with 옵션에 따라 Proc 객체에 전달됩니다. Proc 대신 블록을 직접 사용할 수도 있습니다 .

 

다음은 rescue_from이 모든 ActiveRecord::RecordNotFound 오류를 catch 후, 무언가를 수행 하는  사용할 수 있는 방법 입니다.

class ApplicationController < ActionController::Base
  rescue_from ActiveRecord::RecordNotFound, with: :record_not_found
 
  private
  def record_not_found
    render plain: "404 Not Found", status: 404
  end
end

 

위 예제는 상세한 예외 처리를 기반으로 코드가 짜여진게 아니지만, 일단 예외를 포착 할 수 있으면 에러에 대한 작업을 할 수 있습니다.

예를 들어, 에러 코드는 모든 Controller에서 작동하는 ApplicationController에 작성하고, 사용자가 특정 영역에 접속 할 수 없을 때 발생하는 사용자 정의 예외 클래스를 작성할 수 있습니다.

class ApplicationController < ActionController::Base
  rescue_from User::NotAuthorized, with: :user_not_authorized
 
  private
 
  def user_not_authorized
    flash[:error] = "You don't have access to this section."
    redirect_back(fallback_location: root_path)
  end
end
 
class ClientsController < ApplicationController
  # Check that the user has the right authorization to access clients.
  before_action :check_authorization
 
  # Note how the actions don't have to worry about all the auth stuff.
  def edit
    @client = Client.find(params[:id])
  end
 
  private
  # If the user is not authorized, just throw the exception.
  def check_authorization
    raise User::NotAuthorized unless current_user.admin?
  end
end

 참고 1  심각한 부작용을 일으킬 수있는 특별한 이유가 없는 한(예 : 개발 중에 예외 세부 사항 및 추적을 볼 수 없음) rescue_from Exception 또는 rescue_from StandardError 를 수행하지 않아야합니다.

 참고 2  View를 사용하는 Production Environment 에서는 ActiveRecord::RecordNotFound 에러 시, 404 에러 페이지를 띄우는데, 추가적인 동작을 하는게 아닐 경우 이에 대해선 따로 exception 처리를 하지 않아도 됩니다.

 참고 3  특정 exception에 대해선 모든 컨트롤러 내 초기화 및 작업이 실행되기 전에 있어, ApplicationController 에서 raise 처리를 할 수 있습니다.

 

컨트롤러가 초기화되고 작업이 실행되기 전에 발생하는 일부 예외는 ApplicationController 클래스에서만 구할 수 있습니다.

 

 

  • Force HTTPS protocol

때로는 보안상의 이유로 HTTPS 프로토콜을 통해서만 특정 컨트롤러에 액세스 할 수 있도록 할 수 있습니다. 컨트롤러에서 force_ssl 메소드를 사용하여 다음을 시행 할 수 있습니다.

class DinnerController
  force_ssl
end

 

Filter와 마찬가지로 :only:except 옵션을 통해 특정 Action 내에서만 보안 연결을 적용 할 수 있습니다.

class DinnerController
  force_ssl only: :cheeseburger
  # or
  force_ssl except: :cheeseburger
end

 

많은 컨트롤러에 force_ssl을 추가하다 보면, 나중에는 전체 응용 프로그램이 HTTPS를 대신 사용하도록 하는게 나을 수 있습니다. 이 경우 환경 파일(config/environments)에서 config.force_ssl 옵션을 설정할 수 있습니다.

 

 

  • 자료 참고

1. How Rails Sessions Work

2. HTTP 서버 응답 코드 (Response Code) 정리

3. [개념잡기] HTTP 기본 개념

 

 

댓글
댓글쓰기 폼
공지사항
Total
37,630
Today
22
Yesterday
231
링크
«   2020/08   »
            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          
글 보관함