Обзор Action Controller

По этому руководству вы изучите, как работают контроллеры, и как они вписываются в цикл запроса к вашему приложению.

После его прочтения, вы узнаете:

  • Как следить за ходом запроса через контроллер.
  • Как ограничить параметры, переданные в контроллер.
  • Как и зачем хранятся данные в сессии или куки.
  • Как работать с фильтрами для исполнения кода в течение обработки запроса.
  • Как использовать встроенную в Action Controller HTTP аутентификацию.
  • Как направлять потоковые данные прямо в браузер пользователя.
  • Как отфильтровывать деликатные параметры, чтобы они не появлялись в логах приложения.
  • Как работать с исключениями, которые могут порождаться в течение обработки запроса.

1. Что делает контроллер?

Action Controller это C в аббревиатуре MVC. После того, как роутинг определит, какой контроллер использовать для обработки запроса, контроллер ответственен за осмысление запроса и генерацию подходящего ответа. К счастью, Action Controller делает за вас большую часть грязной работы и использует элегантные соглашения, чтобы сделать это по возможности максимально просто.

Для большинства приложений, основанных на RESTful, контроллер получает запрос (это невидимо для вас, как для разработчика), извлекает или сохраняет данные в модели и использует вьюху для создания результирующего HTML. Если контроллеру необходимо работать немного по другому, не проблема, это всего лишь обычный способ работы контроллера.

Таким образом, можно представить себе контроллер как посредника между моделями и вьюхами. Он делает данные модели доступными вьюхе, так что она может отображать эти данные пользователю, и он сохраняет или обновляет данные от пользователя в модель.

Более детально о процессе маршрутизации смотрите Роутинг в Rails.

2. Соглашение по именованию контроллеров

Соглашение по именованию контроллеров в Rails устанавливает предпочтение множественного числа в последнем слове имени контроллера, хотя строго это не требуется (например, ApplicationController). К примеру, ClientsController более предпочтителен, чем ClientController, SiteAdminsController более предпочтителен, чем SiteAdminController или SitesAdminsController, и так далее.

Следование этому соглашению позволяет вам использовать генераторы маршрутов по умолчанию (например, resources и т.п.) без необходимости определять каждый :path или :controller, и сохраняет последовательным использование хелперов URL и путей во всем вашем приложении. Подробнее смотрите в Руководстве по макетам и рендерингу.

Соглашение по именованию контроллеров отличается от соглашения по именованию моделей, которое ожидается в единственном числе.

3. Методы и экшны

Контроллер - это класс Ruby, унаследованный от ApplicationController и имеющий методы, как и любой другой класс. Когда ваше приложение получает запрос, роутинг определит, какой контроллер и экшн запустить, затем Rails создаст экземпляр этого контроллера и запустит метод с именем, как у экшна.

class ClientsController < ApplicationController
  def new
  end
end

В качестве примера, если пользователь перейдет в /clients/new в вашем приложении, чтобы добавить нового пользователя, Rails создаст экземпляр ClientsController и вызовет метод new. Отметьте, что пустой метод из вышеописанного примера будет прекрасно работать, так как Rails по умолчанию отрендерит вьюху new.html.erb, если экшн не сообщит иное. Метод new может сделать доступной для вьюхи переменную экземпляра @client для создания нового Client:

def new
  @client = Client.new
end

Руководство Макеты и рендеринг в Rails объясняет это более детально.

ApplicationController наследуется от ActionController::Base, который определяет несколько полезных методов. Это руководство раскроет часть из них, но если вы любопытны, можете увидеть их все сами в документации по API.

Только public методы могут быть вызваны как экшны. Хорошей практикой является уменьшение области видимости методов (при помощи private или protected), не предназначенных быть экшнами, таких как вспомогательные методы и фильтры.

4. Параметры

Возможно, вы хотите получить доступ к данным, посланным пользователем, или к другим параметрам в экшнах вашего контроллера. Имеются два типа параметров, возможных в веб приложениях. Первый - это параметры, посланные как часть URL, называемые параметрами строки запроса. Строка запроса всегда следует после "?" в URL. Второй тип параметров обычно упоминаются как данные POST. Эта информация обычно приходит из формы HTML, заполняемой пользователем. Они называются данными POST, так как могут быть посланы только как часть HTTP запроса POST. Rails не делает каких-либо различий между строковыми параметрами и параметрами POST, и они оба доступны в хэше params в вашем контроллере:

class ClientsController < ApplicationController
  # Этот экшн использует параметры строки запроса, потому, что он
  # запускается HTTP запросом GET, но это не делает каких-либо
  # различий в способе, с помощью которого можно получить доступ к
  # ним. URL для этого экшна выглядит как этот, запрашивающий список
  # активированных клиентов: /clients?status=activated
  def index
    if params[:status] == "activated"
      @clients = Client.activated
    else
      @clients = Client.inactivated
    end
  end

  # Этот экшн использует параметры POST. Они, скорее всего, пришли от
  # формы HTML, которую подтвердил пользователь. URL для этого
  # RESTful запроса будет /clients, и данные будут посланы
  # как часть тела запроса.
  def create
    @client = Client.new(params[:client])
    if @client.save
      redirect_to @client
    else
      # Эта строчка переопределяет поведение рендеринга по умолчанию, который
      # отрендерил бы вьюху "create".
      render "new"
    end
  end
end

4.1. Параметры в хэше и в массиве

Хэш params не ограничен одномерными ключами и значениями. Он может содержать вложенные массивы и хэши. Чтобы послать массив значений, добавьте пустую пару квадратных скобок "[]" к имени ключа:

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

Фактический URL в этом примере будет перекодирован как "/clients?ids%5b%5d=1&ids%5b%5d=2&ids%5b%5b=3", так как "[" и "]" не допустимы в URL. В основном, вам не придется беспокоиться об этом, так как браузер позаботится об этом за вас, а Rails декодирует это обратно, когда получит, но если вы когда-нибудь будете отправлять эти запросы вручную, имейте это в виду.

Значение params[:ids] теперь будет ["1", "2", "3"]. Отметьте, что значения параметра всегда строковое; Rails не делает попыток угадать или предсказать тип.

Значения, такие как [nil] или [nil, nil, ...] в params по умолчанию заменяются на nil по причине безопасности. Подробнее смотрите в Руководстве по безопасности.

Чтобы послать хэш, следует заключить имя ключа в скобки:

<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>

Когда эта форма будет подтверждена, значение params[:client] будет { "name" => "Acme", "phone" => "12345", "address" => { "postcode" => "12345", "city" => "Carrot City" } }. Обратите внимание на вложенный хэш в params[:client][:address].

Объект params ведет себя как хэш, но позволяет взаимозаменяемо использовать символы и строки как ключи.

4.2. Параметры JSON

Если вы пишете приложение веб-сервиса, возможно вам более комфортно принимать параметры в формате JSON. Если заголовок "Content-Type" вашего запроса установлен в "application/json", Rails автоматически загружает ваши параметры в хэш params, к которому можно получить доступ обычным образом.

Так, к примеру, если вы пошлете такое содержимое JSON:

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

Ваш контроллер будет получать params[:company] как { "name" => "acme", "address" => "123 Carrot Street" }.

Также, если включите config.wrap_parameters в своем инициализаторе или вызовете wrap_parameters в своем контроллере, можно безопасно опустить корневой элемент в параметре JSON. Параметры будут клонированы и обернуты в ключ, соответствующий по умолчанию имени вашего контроллера. Таким образом, вышеупомянутый JSON POST может быть записан как:

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

И предположим, что мы посылаем данные в CompaniesController, тогда он будет обернут в ключ :company следующим образом:

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

Вы сможете настроить имя ключа или определенные параметры, которые вы хотите обернуть, ознакомившись с документацией по API.

Поддержка парсинга параметров XML была извлечена в гем actionpack-xml_parser.

4.3. Параметры роутинга

Хэш params будет всегда содержать ключи :controller и :action, но следует использовать методы controller_name и action_name вместо них для доступа к этим значениям. Любой другой параметр, определенный роутингом, такой как :id, также будет доступен. Например, рассмотрим перечень клиентов, где список может быть показан либо для активных, либо для неактивных клиентов. Мы можем добавить маршрут, который перехватывает параметр :status в "красивом" URL:

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

В этом случае, когда пользователь откроет URL /clients/active, params[:status] будет установлен в "active". Когда использован этот маршрут, params[:foo] также будет установлен в "bar", как будто он был передан в строке запроса. Ваш контроллер также получит params[:action] как "index" и params[:controller] как "clients".

4.4. default_url_options

Можно установить глобальные параметры по умолчанию для создания URL, определив в контроллере метод по имени default_url_options. Этот метод должен возвращать хэш с желаемыми значениями по умолчанию, ключи которого должны быть символами:

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

Эти опции будут использованы как начальная точка при генерации URL, поэтому, они могут быть переопределены опциями, переданными в url_for.

Если определить default_url_options в ApplicationController, как это показано в примере, эти дефолтные настройки будут использованы для создания всех URL. Метод также может быть определен в одном отдельном контроллере, в этом случае он влияет только на URL, создаваемые в нем.

В данном запросе, на самом деле метод не будет вызываться для каждого сгенерированного URL; для повышения производительности, возвращаемый хэш кэшируется, метод выполняется не более одного раза за запрос.

4.5. Strong Parameters

С помощью strong parameters параметры Action Controller запрещены к использованию в массовом назначении Active Model, кроме случая, когда они есть в белом листе. Это означает, что вы должны сделать сознательное решение, какие атрибуты будут доступны для массового обновления. Это лучший способ предотвратить случайную уязвимость, позволяющую пользователям обновлять конфиденциальные атрибуты модели.

Кроме того, параметры могут быть помечены как обязательные, отсутствие которых приведет к 400 Bad Request с помощью предопределенных raise/rescue.

class PeopleController < ActionController::Base
  # Это вызовет исключение ActiveModel::ForbiddenAttributes так как используется
  # массовое назначение без явного шага permit.
  def create
    Person.create(params[:person])
  end

  # Это будет проходить, пока в параметрах есть ключ person, иначе будет
  # вызвано исключение ActionController::ParameterMissing, которое будет
  # поймано в ActionController::Base и превращено в отклик 400 Bad Request.
  def update
    person = current_account.people.find(params[:id])
    person.update!(person_params)
    redirect_to person
  end

  private
    # Использование приватного метода для инкапсуляции разрешенных параметров -
    # это всего лишь хороший паттерн, с помощью которого можно повторно
    # использовать тот же самый список разрешений при создании и обновлении
    # Этот метод также можно адаптировать к проверке разрешенных атрибутов для
    # определенных пользователей.
    def person_params
      params.require(:person).permit(:name, :age)
    end
end

4.5.1. Разрешенные скалярные величины

Для данного

params.permit(:id)

ключ :id пройдет проверку белым списком, если он появится в params и будет иметь разрешенную скалярную величину. В ином случае ключ будет отфильтрован, таким образом, массивы, хэши и любые другие объекты не смогут быть переданы.

Разрешенные скалярные типы следующие String, Symbol, NilClass, Numeric, TrueClass, FalseClass, Date, Time, DateTime, StringIO, IO, ActionDispatch::Http::UploadedFile и Rack::Test::UploadedFile.

Чтобы объявить, что значение в params должно быть массивом разрешенных скалярных величин, свяжите ключ с пустым массивом:

params.permit(id: [])

Чтобы добавить в белый список полный хэш параметров, может быть использован метод permit!

params.require(:log_entry).permit!

Это пометит хэш параметров :log_entry и любые вложенные хэши как разрешенные. Следует соблюдать предельную осторожность при использовании permit!, так как он позволит массовое назначение всех текущих и будущих атрибутов модели.

4.5.2. Вложенные параметры

Также можно использовать permit c вложенными параметрами, например:

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

Это объявление поместит в белый список атрибуты name, emails и friends. Ожидается, что emails будет массивом разрешенных скалярных величин, и что friends будет массивом ресурсов с определенными атрибутами: у них будет атрибут name (допустимо любое скалярное значение), атрибут hobbies как массив разрешенных скалярных величин, и атрибут family, который может иметь только name (также допустимо любое скалярное значение).

4.5.3. Дополнительные примеры

Возможно вы захотите использовать разрешенные атрибуты в экшне new. Это вызывает проблему, что нельзя использовать require на корневом ключе, так как обычно он не существует при вызове new:

# используя `fetch`, можно предоставить значение по умолчанию и использовать
# далее Strong Parameters API.
params.fetch(:blog, {}).permit(:title, :author)

Метод класса модели accepts_nested_attributes_for позволяет обновлять и удалять связанные записи. Это основывается на параметрах id и _destroy:

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

Хэши с числовыми ключами трактуются по другому, можно объявить атрибуты так, как будто они являются прямыми детьми. Такой тип параметров вы получите при использовании accepts_nested_attributes_for с комбинацией со связью has_many:

# Чтобы пропустить следующие данные:
# {"book" => {"title" => "Some Book",
#             "chapters_attributes" => { "1" => {"title" => "First Chapter"},
#                                        "2" => {"title" => "Second Chapter"}}}}

params.require(:book).permit(:title, chapters_attributes: [:title])

4.5.4. За пределами Strong Parameters

Strong parameter API был разработан для наиболее общих вариантов использования. Это не панацея от всех ваших проблем белого списка. Однако можно легко смешивать API с вашим собственным кодом для адаптации к вашей ситуации.

Представим сценарий, когда у вас есть параметры, представляющие имя продукта и хэш произвольных данных, связанных с этим продуктом, и вы хотите поместить в белый список атрибут имени продукта, а также весь хэш данных. API в strong parameters не позволит вам напрямую разрешить весь вложенный хэш с любыми ключами, но можно использовать ключи вложенного хэша для указания, что добавить в белый список:

def product_params
  params.require(:product).permit(:name, data: params[:product][:data].try(:keys))
end

5. Сессия

У вашего приложения есть сессия для каждого пользователя в которой можно хранить небольшие порции данных, которые будут сохранены между запросами. Сессия доступна только в контроллере и во вьюхе, и может использовать один из нескольких механизмов хранения:

  • ActionDispatch::Session::CookieStore - Хранит все на клиенте.
  • ActionDispatch::Session::CacheStore - Хранит данные в кэше Rails.
  • ActionDispatch::Session::ActiveRecordStore - Хранит данные в базе данных с использованием Active Record. (требует гем activerecord-session_store).
  • ActionDispatch::Session::MemCacheStore - Хранит данные в кластере memcached (эта реализация - наследие старых версий, вместо нее рассмотрите использование CacheStore).

Все сессии используют куки для хранения уникального ID каждой сессии (вы обязаны использовать куки, Rails не позволяет передавать ID сессии в URL, так как это не безопасно).

Для большинства способов хранения этот ID используется для поиска данных сессии на сервере, в т.ч. в таблице базы данных. Имеется одно исключение, это дефолтное и рекомендуемое хранение сессии - CookieStore - которое хранит все данные сессии в куки (ID остается доступным, если он вам нужен). Преимущества этого в легкости, отсутствии настройки для нового приложения в порядке использования сессий. Данные в куки криптографически подписаны, что делает их защищенными от взлома. И они также зашифрованы, таким образом любой получивший к ним доступ, не сможет прочитать их содержимое (Rails не примет их, если они были отредактированы).

CookieStore могут хранить около 4kB данных - намного меньше, чем остальные - но этого обычно хватает. Хранение большего количества данных в сессии не рекомендуется, вне зависимости от того, как хранятся они в приложении. Следует специально избегать хранения в сессии сложных объектов (ничего, кроме простых объектов Ruby, например экземпляры моделей), так как сервер может не собрать их между запросами, что приведет к ошибке.

Если пользовательские сессии не хранят критичные данные или нет необходимости в ее сохранности на долгий период (скажем, если вы используете ее для сообщений во flash), можете рассмотреть использование ActionDispatch::Session::CacheStore. Он сохранит сессии с использованием реализации кэша, которую вы настроили для своего приложения. Преимущество этого в том, что для хранения сессий можно использовать существующую инфраструктуру кэширования без необходимости дополнительных настроек или администрирования. Минус, разумеется, в том, что сессии будут недолговечными и время от времени исчезать.

Читайте подробнее о хранении сессий в Руководстве по безопасности.

Если вы нуждаетесь в другом механизме хранения сессий, измените его в файле 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

Rails настраивает ключ сессии (имя куки) при подписании данных сессии. Он также может быть изменен в 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 и определить имя домена для куки:

# 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"

Rails настраивает (для CookieStore) секретный ключ для подписания данных сессии. Он может быть изменен в 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 `rake 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"] %>

Изменение секретного ключа при использовании CookieStore делает все предыдущие сессии невалидными.

5.1. Доступ к сессии

В контроллере можно получить доступ к сессии с помощью метода экземпляра session.

Сессии лениво загружаются. Если вы не получаете доступ к сессиям в коде экшна, они не будут загружаться. Следовательно, никогда не нужно отключать сессии, просто не обращайтесь к ним, чтобы они не работали.

Значение сессии хранится, используя пары ключ/значение, подобно хэшу:

class ApplicationController < ActionController::Base

  private

  # Находим пользователя с ID, хранящимся в сессии с ключом
  # :current_user_id Это обычный способ обрабатывать вход пользователя
  # в приложении на Rails; вход устанавливает значение сессии, а
  # выход убирает его.
  def current_user
    @_current_user ||= session[:current_user_id] &&
      User.find_by(id: session[:current_user_id])
  end
end

Чтобы что-то хранить в сессии, просто присвойте это ключу, как в хэше:

class LoginsController < ApplicationController
  # "Создаем" логин (при входе пользователя)
  def create
    if user = User.authenticate(params[:username], params[:password])
      # Сохраняем ID пользователя в сессии, так что он может быть использован
      # в последующих запросах
      session[:current_user_id] = user.id
      redirect_to root_url
    end
  end
end

Чтобы убрать что-то из сессии, присвойте этому ключу nil:

class LoginsController < ApplicationController
  # "Удаляем" логин (при выходе пользователя)
  def destroy
    # Убираем id пользователя из сессии
    @_current_user = session[:current_user_id] = nil
    redirect_to root_url
  end
end

Для сброса существующей сессии, используйте reset_session.

5.2. Flash

Flash - это специальная часть сессии, которая очищается с каждым запросом. Это означает, что значения хранятся там доступными только до следующего запроса, что полезно для хранения сообщений об ошибке и т.п.

Доступ к нему можно получить так же, как к сессии, подобно хэшу (это экземпляр FlashHash).

Давайте посмотрим действие логаута как пример. Контроллер может послать сообщение, которое будет отображено пользователю при следующем запросе:

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

Отметьте, что также возможно назначить сообщение флэш как часть перенаправления. Можно назначить :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 }

Экшн destroy перенаправляет на root_url приложения, где будет отображено сообщение. Отметьте, что от следующего экшна полностью зависит решение, будет ли он или не будет что-то делать с тем, что предыдущий экшн вложил во flash. Принято отображать любые сообщения об ошибке или уведомления из flash в макете приложения:

<html>
  <!-- <head/> -->
  <body>
    <% flash.each do |name, msg| -%>
      <%= content_tag :div, msg, class: name %>
    <% end -%>

    <!-- дальнейшее содержимое -->
  </body>
</html>

В этом случае, если экшн установил сообщения уведомления или предупреждения, макет отобразит их автоматически.

Можно передать все, что только сессия может хранить; вы не ограничены уведомлениями или предупреждениями:

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

Если хотите, чтобы значение flash было перенесено для другого запроса, используйте метод keep:

class MainController < ApplicationController
  # Давайте скажем этому экшну, соответствующему root_url, что хотим
  # все запросы сюда перенаправить на UsersController#index. Если
  # экшн установил flash и направил сюда, значения в нормальной ситуации
  # будут потеряны, когда произойдет другой редирект, но Вы можете
  # использовать 'keep', чтобы сделать его доступным для другого запроса.
  def index
    # Сохранит все значения flash.
    flash.keep

    # Можете также использовать ключ для сохранения определенных значений.
    # flash.keep(:notice)
    redirect_to users_url
  end
end

5.2.1. flash.now

По умолчанию, добавление значений во flash делает их доступными для следующего запроса, но иногда хочется иметь доступ к этим значениям в том же запросе. Например, если экшн create проваливается в сохранении ресурса, и вы рендерите макет new непосредственно тут, то не собираетесь передавать результат в новый запрос, но хотите отобразить сообщение, используя flash. Чтобы это сделать, используйте flash.now так же, как использовали нормальный flash:

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

6. Куки

Ваше приложение может хранить небольшое количество данных у клиента - в так называемых куки - которое будет сохранено между запросами и даже сессиями. Rails обеспечивает простой доступ к куки посредством метода cookies, который - очень похоже на session - работает как хэш:

class CommentsController < ApplicationController
  def new
    # Автозаполнение имени комментатора, если оно хранится в куки.
    @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]
        # Запоминаем имя комментатора.
        cookies[:commenter_name] = @comment.author
      else
        # Удаляем из куки имя комментатора, если оно есть.
        cookies.delete(:commenter_name)
      end
      redirect_to @comment.article
    else
      render action: "new"
    end
  end
end

Отметьте, что если для удаления сессии устанавливался ключ в nil, то для удаления значения куки следует использовать cookies.delete(:key).

Rails также представляет подписанные куки и зашифрованные куки для хранения конфиденциальных данных. В подписанные куки добавляется криптографическая подпись значений куки для защиты их целостности. Зашифрованные куки шифруют значения в дополнение к их подписи, поэтому они не могут быть прочитаны пользователем. За подробностями обратитесь к документации API.

Эти специальные куки используют сериализатор для сериализации назначенных значений в строку и десериализации их в объекты Ruby при чтении.

Можно определить, какой сериализатор использовать:

Rails.application.config.action_dispatch.cookies_serializer = :json

Для новых приложений сериализатором по умолчанию является :json. Для совместимости со старыми приложениями со существующими куки, используется :marshal, когда не определена опция serializer.

Также можно установить этой опции :hybrid, в этом случае Rails десериализует существующие (сериализованные Marshal) куки при чтении и перезапишет их в формате JSON. Это полезно при миграции существующих приложений на сериализатор :json.

Также возможно передать произвольный сериализатор, откликающийся на load и dump:

Rails.application.config.action_dispatch.cookies_serializer = MyCustomSerializer

При использовании сериализатора :json или :hybrid, следует знать, что не все объекты Ruby могут быть сериализованы как JSON. Например, объекты Date и Time будут сериализованы как строки, и у хэшей ключи будут преобразованы в строки.

class CookiesController < ApplicationController
  def set_cookie
    cookies.encrypted[:expiration_date] = Date.tomorrow # => Thu, 20 Mar 2014
    redirect_to action: 'read_cookie'
  end

  def read_cookie
    cookies.encrypted[:expiration_date] # => "2014-03-20"
  end
end

Желательно, чтобы в куки хранились только простые данные (строки и числа). Если храните сложные объекты, вам необходимо преобразовывать вручную при чтении значений в соответствующих запросах.

Если вы храните сессию в куки, все перечисленное также применяется к хэшам session и flash.

7. Рендеринг данных XML и JSON

ActionController позволяет очень просто рендерить данные XML или JSON. Если создадите контроллер с помощью скаффолдинга, то он будет выглядеть следующим образом.

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

Отметьте, что в вышеописанном коде использован render xml: @users, а не render xml: @users.to_xml. Если объект не String, то Rails автоматически вызовет to_xml.

8. Фильтры

Фильтры - это методы, которые запускаются "до", "после" или "вокруг" экшна контроллера.

Фильтры наследуются, поэтому, если вы установите фильтр в ApplicationController, он будет запущен в каждом контроллере вашего приложения.

Фильтры "before" могут прерывать цикл запроса. Обычный фильтр "before" это, например, тот, который требует, чтобы пользователь был авторизован для запуска экшна. Метод фильтра можно определить следующим образом:

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 # прерывает цикл запроса
    end
  end
end

Метод просто записывает сообщение об ошибке во flash и перенаправляет на форму авторизации, если пользователь не авторизовался. Если фильтр "before" рендерит или перенаправляет, экшн не запустится. Если есть дополнительные фильтры в очереди, они также прекращаются.

В этом примере фильтр добавлен в ApplicationController, и поэтому все контроллеры в приложении наследуют его. Это приводит к тому, что всё в приложении требует, чтобы пользователь был авторизован, чтобы пользоваться им. По понятным причинам (пользователь не сможет зарегистрироваться в первую очередь!), не все контроллеры или экшны должны требовать его. Вы можете не допустить запуск этого фильтра перед определенными экшнами с помощью skip_before_action:

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

Теперь, экшны LoginsController new и create будут работать как раньше, без требования к пользователю быть зарегистрированным. Опция :only используется для пропуска фильтра только для этих экшнов, также есть опция :except, которая работает наоборот. Эти опции могут также использоваться при добавлении фильтров, поэтому можете добавить фильтр, который запускается только для выбранных экшнов в первую очередь.

8.1. Последующие фильтры и охватывающие фильтры

В дополнение к фильтрам "before", можете запустить фильтры после того, как экшн был запущен, или и до, и после.

Фильтр "after" подобен "before", но, поскольку экшн уже был запущен, у него есть доступ к данным отклика, которые будут отосланы клиенту. Очевидно, фильтр "after" не сможет остановить экшн от запуска.

Фильтры "around" ответственны за запуск экшна с помощью yield, подобно тому, как работают промежуточные программы Rack.

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

Отметьте, что фильтры "around" также оборачивают рендеринг. В частности, если в вышеуказанном примере вьюха сама начнет считывать из базы данных (например через скоуп), она это осуществит внутри транзакции, предоставив, таким образом, данные для предварительного просмотра.

Можно не вызывать yield и создать отклик самостоятельно, в этом случае экшн не будет запущен.

8.2. Другие способы использования фильтров

Хотя наиболее распространенный способ использование фильтров - это создание private методов и использование *_action для их добавления, есть два других способа делать то же самое.

Первый - это использовать блок прямо в методах *_action. Блок получает контроллер как аргумент. Фильтр require_login может быть переписан с использованием блока:

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

Отметьте, что фильтр в этом случае использует метод send, так как logged_in? является private, и фильтр не запустится в области видимости контроллера. Это не рекомендуемый способ применения такого особого фильтра, но в простых задачах он может быть полезен.

Второй способ - это использовать класс (фактически, подойдет любой объект, реагирующий правильными методами) для управления фильтрацией. Это полезно для более сложных задач, которые не могут быть осуществлены предыдущими двумя способами по причине трудности читаемости и повторного использования. Как пример, можете переписать фильтр авторизации снова, использовав класс:

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

Опять же, это - не идеальный пример для этого фильтра, поскольку он не запускается в области видимости контроллера, а получает контроллер как аргумент. Класс фильтра имеет метод класса filter, который запускается до или после экшна, в зависимости от того, определен ли он предварительным или последующим фильтром. Классы, используемые как охватывающие фильтры, могут также использовать тот же метод filter, и они будет запущены тем же образом. Метод должен иметь yield для исполнения экшна. Альтернативно, он может иметь методы и before, и after, которые запускаются до и после экшна.

9. Защита от подделки запросов

Подделка межсайтовых запросов - это тип атаки, в которой сайт обманом заставляет пользователя сделать запрос на другой сайт, возможно добавляя, изменяя или удаляя данные на этом сайте без ведома или разрешения пользователя.

Первый шаг в избегании этого - убедиться, что все "разрушительные" экшны (создание, обновление и уничтожение) могут быть доступны только не-GET запросам. Если вы следуете соглашениям RESTful, то уже делаете это. Однако, сайт злоумышленника может также легко послать не-GET запрос на ваш сайт, поэтому и необходима защита от подделки запросов. Как следует из ее имени, она защищает от подделанных запросов.

Это можно сделать, добавив неугадываемый токен, известный только вашему серверу, в каждый запрос. При этом способе, если запрос приходит без подходящего токена, ему будет отказано в доступе.

Если вы генерируете подобную форму:

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

то увидите, как токен будет добавлен в скрытое поле:

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

Rails добавит этот токен в каждую форму, генерируемую с помощью хелперов форм, таким образом, большую часть времени можете об этом не беспокоиться. Если вы пишете формы вручную или хотите добавить токен по другой причине, это можно сделать, используя метод form_authenticity_token:

form_authenticity_token создает валидный аутентификационный токен. Его полезно помещать в места, куда Rails не добавляет его автоматически, например в произвольные вызовы Ajax.

В Руководстве по безопасности имеется более подробная информация об этом, и множество других вопросов, посвященных безопасности, которые вы должны принимать во внимание при разработке веб приложения.

10. Объекты Request и Response

В каждом контроллере есть два accessor-метода, указывающих на объекты запроса и отклика, связанные с циклом запроса, находящегося в текущее время на исполнении. Метод request содержит экземпляр ActionDispatch::Request, а метод response возвращает объект отклика, представляющий то, что собирается быть отправлено обратно на клиента.

10.1. Объект request

Объект request содержит множество полезной информации о запросе, полученном с клиента. Чтобы получить полный перечень доступных методов, обратитесь к документации по API. В числе свойств, доступных для этого объекта, следующие:

Свойство request Назначение
host Имя хоста, используемого для этого запроса.
domain(n=2) Первые n сегментов имени хоста, начиная справа (домен верхнего уровня).
format Тип содержимого, запрошенного с клиента.
method Метод HTTP, использованного для запроса.
get?, post?, patch?, put?, delete?, head? Возвращает true, если метод HTTP - это GET/POST/PATCH/PUT/DELETE/HEAD.
headers Возвращает хэш, содержащий заголовки, связанные с запросом.
port Номер порта (число), использованного для запроса.
protocol Возвращает строку, содержащую использованный протокол плюс "://", например "http://".
query_string Часть URL со строкой запроса, т.е. все после "?".
remote_ip Адрес IP клиента.
url Полный URL, использованный для запроса.
10.1.1. path_parameters, query_parameters и request_parameters

Rails собирает все параметры, посланные вместе с запросом, в хэше params, были ли они посланы как часть строки запроса, либо в теле запроса post. У объекта request имеется три метода доступа, которые дают доступ к этим параметрам в зависимости от того, откуда они пришли. Хэш query_parameters содержит параметры, посланные как часть строки запроса, а хэш request_parameters содержит параметры, посланные как часть тела post. Хэш path_parameters содержит параметры, распознанные роутингом как часть пути, ведущего к определенному контроллеру и экшну.

10.2. Объект response

Объект response обычно не используется напрямую, но он создается в течение исполнения экшна и рендеринга данных, которые посылаются обратно пользователю, но иногда - например, в последующем фильтре - бывает полезно иметь доступ к отклику напрямую. Некоторые из этих методов доступа имеют настройки, позволяющие изменять их значения.

Свойство response Назначение
body Это строка данных, которая будет возвращена клиенту. Часто это HTML.
status Код статуса HTTP для отклика, например 200 для успешного отклика или 404 для ненайденного файла.
location URL, по которому клиент будет перенаправлен, если указан.
content_type Тип содержимого отклика.
charset Кодировка, используемая для отклика. По умолчанию это "utf-8".
headers Заголовки, используемые для отклика.
10.2.1. Установка пользовательских заголовков

Если хотите установить произвольные заголовки для отклика, то response.headers - как раз то место, что нужно. Атрибут заголовков - это хэш, который связывает имена заголовков с их значениями, и Rails устанавливает некоторые из них автоматически. Если хотите добавить или изменить заголовок, назначьте его response.headers следующим образом:

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

Note: в вышеприведенном случае более очевидным было бы использование сеттера content_type.

11. Аутентификации HTTP

Rails поставляется с двумя встроенными механизмами аутентификации HTTP:

  • Простая аутентификация
  • Digest аутентификация

11.1. Простая аутентификация HTTP

Простая аутентификация HTTP - это аутентификационная схема, поддерживаемая большинством браузеров и других клиентов HTTP. Как пример, рассмотрим административный раздел, который доступен только при вводе имени пользователя и пароля в основном диалоговом окне браузера. Использование встроенной аутентификации достаточно простое, и требует использования одного метода http_basic_authenticate_with.

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

Разместив это, можете создавать именованные контроллеры, наследуемые от AdminsController. Таким образом, предварительный фильтр будет запущен для всех экшнов в этих контроллерах, защищая их с помощью основной аутентификации HTTP.

11.2. Digest аутентификация HTTP

Digest аутентификация HTTP превосходит простую аутентификацию, так как она не требует от клиента посылать незашифрованный пароль по сети (хотя простая аутентификация HTTP безопасна через HTTPS). Использовать digest аутентификацию с Rails просто, и это потребует только один метод authenticate_or_request_with_http_digest.

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 принимает только один аргумент - имя пользователя. И блок возвращает пароль. Возврат false или nil из authenticate_or_request_with_http_digest вызовет провал аутентификации.

12. Потоки и загрузка файлов

Иногда хочется послать пользователю файл вместо рендеринга страницы HTML. Все контроллеры в Rails имеют методы send_data и send_file, которые направляют данные на клиента. send_file - это удобный метод, который позволяет указать имя файла на диске, а он направит содержимое этого файла вам.

Чтобы направить данные на клиента, используйте send_data:

require "prawn"
class ClientsController < ApplicationController
  # Создает документ PDF с информацией на клиента и возвращает
  # его. Пользователь получает PDF как загрузку файла.
  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 в примере вызовет private метод, который фактически создаст документ PDF и возвратит его как строку. Эта строка будет направлена клиенту как загрузка файла, и пользователю будет предложено имя файла. Иногда при передаче файлов пользователю, вы можете не захотеть, чтобы их скачивали. Принятие изображений, к примеру, которое может быть внедрено в страницы HTML. Чтобы сказать браузеру, что файл не подразумевает быть скачанным, нужно установить опцию :disposition как "inline". Противоположное и дефолтное значение этой опции - "attachment.

12.1. Отправка файлов

Если хотите отправить файл, уже существующий на диске, используйте метод send_file.

class ClientsController < ApplicationController
  # Передача файла, который уже был создан и сохранен на диск.
  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

Это прочтет и передаст файл блоками в 4kB за раз, что избегает загрузки целого файла в память единовременно. Можете отключить потоковость с помощью опции :stream или отрегулировать размер блока с помощью опции :buffer_size.

Если не указан :type, он будет угадан по расширению файла, указанного в :filename. Если для расширения не зарегистрирован тип содержимого, будет использован application/octet-stream.

Будьте осторожны, когда используете данные, пришедшие с клиента (params, куки и т.д.), для обнаружения файла на диске, так как есть риск безопасности в том, что кто-то может получить доступ к файлам, к которым они не должны.

Не рекомендуется передавать статичные файлы через Rails, если можно вместо этого разместить их в папке public на Вашем веб сервере. Более эффективно разрешить пользователям скачивать файлы напрямую, используя Apache или другой веб сервер, сохраняя запрос от ненужного прогона через весь стек Rails.

12.2. Загрузка RESTful

Хотя send_data работает прекрасно, если вы создаете приложение на принципах RESTful, наличие отдельных экшнов для загрузок файла обычно не рекомендовано. В терминологии REST, файл PDF из примера выше можно считать еще одним представлением ресурса client. Rails предоставляет простой и наглядный способ осуществления загрузок в стиле RESTful. Вот как можно переписать пример так, что загрузка PDF является частью экшна show, без какой-либо потоковости:

class ClientsController < ApplicationController
  # Пользователь может запросить получение этого ресурса как HTML или 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.register "application/pdf", :pdf

Конфигурационные файлы не перезагружаются с каждым запросом, поэтому необходимо перезапустить сервер для того, чтобы изменения вступили в силу.

Теперь пользователь может запрашивать получение версии в PDF, просто добавив ".pdf" в URL:

GET /clients/1.pdf

12.3. Live Streaming произвольных данных

Rails позволяет отдавать в потоке не только файлы. Фактически в объекте отклика можно отдать все, что хотите. Модуль ActionController::Live позволяет создать постоянное соединение с браузером. Используя этот модуль, можно послать в браузер произвольные данные в определенные моменты времени.

Сервер идущий по умолчанию с Rails (WEBrick) является буферизирующим веб сервером и не поддерживает потоковую передачу данных. Для того, чтобы использовать эту функцию, вам необходимо использовать не буферизирующий сервер, такой как Puma, Rainbows или Passenger.

12.3.1. Подключение Live Streaming

Включение ActionController::Live в класс вашего контроллера представит всем экшнам контроллера возможность отдавать данные в потоке. Этот модуль можно включить следующим образом:

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

Вышеприведенный код будет поддерживать постоянное соединение с браузером, и пошлет 100 сообщений "hello world\n", раз в секунду каждое.

В вышеприведенном примере нужно обратить внимание на ряд вещей. Необходимо убедиться, что потоковый отклик будет закрыт. Если забыть закрыть, поток оставит навсегда открытым сокет. Также необходимо установить тип содержимого text/event-stream до записи в поток отклика. Это так, потому что заголовки не могут быть записаны после того, как отклик был подтвержден (когда response.committed? возвращает истинное значение), что случается, когда вы вызывает write или commit для потокового отклика.

12.3.2. Пример использования

Предположим, мы создаем машину караоке, и пользователь хочет получить слова для определенной песни. В каждом Song имеется определенное количество строчек, и у каждой строчки есть время num_beats для завершения пения.

Если мы хотим возвращать слова в манере караоке (посылая строчку, только когда певец закончил предыдущую), можно использовать ActionController::Live следующим образом:

class LyricsController < ActionController::Base
  include ActionController::Live

  def show
    response.headers['Content-Type'] = 'text/event-stream'
    song = Song.find(params[:id])

    song.each do |line|
      response.stream.write line.lyrics
      sleep line.num_beats
    end
  ensure
    response.stream.close
  end
end

Вышеприведенный код посылает следующую строчку только после того, как певец завершил предыдущую строчку.

12.3.3. Обсуждение Streaming

Streaming произвольных данных – чрезвычайно мощный инструмент. Как показано в предыдущих примерах, вы можете выбирать, когда и что посылать в потоковом отклике. Однако, также необходимо отметить следующие вещи:

  • Каждый потоковый отклик создает новый тред и копирует локальные переменные из текущего треда. Наличие большого количество тредовых локальных переменных может отрицательно сказаться на производительности. Большое количество тредов также препятствует производительности.
  • Незакрытие потокового отклика оставит соответствующий сокет открытым навсегда. Убедитесь, что вызываете close при использовании потокового отклика.
  • Сервера WEBrick буферизируют все отклики, поэтому включение ActionController::Live не будет работать. Необходимо использовать веб-сервер, не буферизирующий отклики автоматически.

13. Фильтрация лога

Rails ведет лог-файл для каждой среды в папке log. Это чрезвычайно полезно при отладке, что мы уже фактически наблюдали в вашем приложении, но в реальной жизни вам может быть не нужно хранение каждого бита информации в лог-файле.

13.1. Фильтрация параметров

Можно фильтровать конфиденциальные параметры запросов в файлах лога, присоединив их к config.filter_parameters в настройках приложения. Эти параметры будут помечены в логе как [FILTERED].

config.filter_parameters << :password

13.2. Фильтрация редиректов

Иногда нужно фильтровать из файлов лога некоторые конфиденциальные ссылки, на которые перенаправляет ваше приложение. Это можно осуществить с использованием конфигурационной опции config.filter_redirect:

config.filter_redirect << 's3.amazonaws.com'

Ей можно передать строку, регулярное выражение или массив из них.

config.filter_redirect.concat ['s3.amazonaws.com', /private_path/]

Соответствующие URL будут помечены как '[FILTERED]'.

14. Обработка ошибок

Скорее всего, ваше приложение будет содержать ошибки или, другими словами, вызывать исключения, которые нужно обработать. Например, если пользователь проходит по ссылке на ресурс, который больше не существует в базе данных, Active Record вызовет исключение ActiveRecord::RecordNotFound.

Дефолтный обработчик исключений Rails отображает сообщение "500 Server Error" для всех исключений. Если запрос сделан локально, отображается прекрасная трассировка и некоторая дополнительная информация, так что вы можете выяснить, что пошло не так, и разобраться с этим. Если запрос был удаленным, Rails отобразит пользователю лишь простое сообщение "500 Server Error", или "404 Not Found", если была проблема с роутингом или запись не была найдена. Иногда вы захотите настроить, как эти ошибки будут перехвачены, и как они будут отображены пользователю. В приложении на Rails доступны несколько уровней обработки исключений:

14.1. Дефолтные шаблоны 500 и 404

По умолчанию приложение в среде production будет рендерить или 404, или 500 сообщение об ошибке. Эти сообщения содержатся в статичных файлах HTML в папке public, в 404.html и 500.html соответственно. Можете настроить эти файлы, добавив дополнительную информацию и стили, но помните, что они статичны; т.е. нельзя использовать ERB, SCSS, CoffeeScript или макеты для них.

14.2. rescue_from

Если хотите сделать нечто более сложное при перехвате ошибок, можете использовать rescue_from, которая управляет исключениями определенного типа (или нескольких типов) во всем контроллере и его подклассах.

Когда происходит исключение, которое перехватывается директивой rescue_from, сбойный объект передается в обработчик. Обработчик может быть методом или объектом Proc, переданным опции :with. Также можно использовать блок вместо объекта Proc.

Вот как можно использовать rescue_from для перехвата всех ошибок ActiveRecord::RecordNotFound и что-то с ними делать.

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

Конечно, этот пример далеко не доработан, и ничуть не улучшает обработку исключений по умолчанию, но раз вы уже перехватили все эти исключения, то вольны делать с ними все, что хотите. Например, можете создать свои классы исключений, которые будут вызваны, когда у пользователя нет доступа в определенные разделы вашего приложения:

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_to :back
    end
end

class ClientsController < ApplicationController
  # Проверим, что пользователь имеет права доступа к клиентам.
  before_action :check_authorization

  # Отметим как экшны не беспокоятся об авторизационных делах.
  def edit
    @client = Client.find(params[:id])
  end

  private

    # Если пользователь не авторизован, просто вызываем исключение.
    def check_authorization
      raise User::NotAuthorized unless current_user.admin?
    end
end

Не следует делать rescue_from Exception или rescue_from StandardError, если у вас нет веской причины, так как это вызовет серьезные сторонние эффекты (например, вы не сможете увидеть подробности и трейс исключения при разработке).

Некоторые исключения перехватываемы только из класса ApplicationController, так как они вызываются до того, как контроллер будет инициализирован, и экшны будут выполнены.

Иногда хочется навязать определенному контроллеру быть доступным только через протокол HTTPS по причинам безопасности. Можно использовать в контроллере метод force_ssl, для принуждения к этому:

class DinnerController
  force_ssl
end

подобно фильтру, можно также передать :only и :except для принуждения безопасного соединения только определенным экшнам.

class DinnerController
  force_ssl only: :cheeseburger
  # или
  force_ssl except: :cheeseburger
end

Пожалуйста, отметьте, что если вы добавили force_ssl во многие контроллеры, то, возможно, вместо этого хотите навязать всему приложению использование HTTPS. В этом случае можно установить config.force_ssl в файле окружения.