Статьи

Создание API с помощью Rails

В настоящее время обычной практикой является сильная зависимость от API (интерфейсов прикладного программирования). Их используют не только крупные сервисы, такие как Facebook и Twitter — API очень популярны из-за распространения клиентских сред, таких как React, Angular и многих других. Ruby on Rails следует этой тенденции, а в последней версии представлена ​​новая функция, позволяющая создавать приложения только для API.

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

В этой статье рассказывается, как создавать приложения Rails только для API, а также объясняется, как структурировать маршруты и контроллеры, использовать формат JSON, добавить сериализаторы и настроить CORS (Cross-Origin Resource Sharing). Вы также узнаете о некоторых вариантах защиты API и защиты от злоупотреблений.

Источник этой статьи доступен на GitHub .

Чтобы начать, выполните следующую команду:

1
rails new RailsApiDemo —api

Он собирается создать новое приложение Rails только для API под названием RailsApiDemo . Не забывайте, что поддержка опции --api была добавлена ​​только в Rails 5, поэтому убедитесь, что у вас установлена ​​эта или более новая версия.

Откройте Gemfile и обратите внимание, что он намного меньше обычного: драгоценные камни, такие как coffee-rails , turbolinks и coffee-rails turbolinks , исчезли.

Файл config / application.rb содержит новую строку:

1
config.api_only = true

Это означает, что Rails собирается загрузить меньший набор промежуточного программного обеспечения: например, нет поддержки файлов cookie и сеансов. Более того, если вы попытаетесь создать каркас, представления и ресурсы не будут созданы. На самом деле, если вы проверите каталог views / layouts , вы заметите, что файл application.html.erb также отсутствует.

Другое важное отличие состоит в том, что ApplicationController наследуется от ActionController::API , а не ActionController::Base .

Вот и все — в общем, это базовое приложение на Rails, которое вы видели много раз. Теперь давайте добавим пару моделей, чтобы у нас было с чем работать:

1
2
3
rails g model User name:string
rails g model Post title:string body:text user:belongs_to
rails db:migrate

Здесь ничего особенного не происходит: пост с заголовком и телом принадлежит пользователю.

Убедитесь, что установлены правильные ассоциации, а также предоставьте несколько простых проверок:

1
2
3
has_many :posts
 
 validates :name, presence: true

1
2
3
4
belongs_to :user
 
 validates :title, presence: true
 validates :body, presence: true

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

Самый простой способ загрузить некоторые данные — использовать файл seed.rb в каталоге db . Тем не менее, я ленив (как и многие программисты) и не хочу думать ни о каком образце контента. Поэтому, почему бы нам не воспользоваться преимуществом фейкерного самоцвета, который может генерировать случайные данные различного типа: имена, электронные письма, хипстерские слова, тексты «lorem ipsum» и многое другое.

1
2
3
group :development do
    gem ‘faker’
end

Установите драгоценный камень:

1
bundle install

Теперь настройте семена.rb :

1
2
3
4
5.times do
  user = User.create({name: Faker::Name.name})
  user.posts.create({title: Faker::Book.title, body: Faker::Lorem.sentence})
end

Наконец, загрузите ваши данные:

1
rails db:seed

Теперь, конечно, нам нужны некоторые маршруты и контроллеры для создания нашего API. Обычной практикой является вложение маршрутов api/ в api/ path. Кроме того, разработчики обычно предоставляют версию API в пути, например, api/v1/ . Позже, если будут внесены некоторые критические изменения, вы можете просто создать новое пространство имен ( v2 ) и отдельный контроллер.

Вот как могут выглядеть ваши маршруты:

1
2
3
4
5
6
namespace ‘api’ do
    namespace ‘v1’ do
      resources :posts
      resources :users
    end
end

Это генерирует маршруты как:

1
2
3
api_v1_posts GET /api/v1/posts(.:format) api/v1/posts#index
            POST /api/v1/posts(.:format) api/v1/posts#create
api_v1_post GET /api/v1/posts/:id(.:format) api/v1/posts#show

Вы можете использовать метод области вместо namespace , но тогда по умолчанию он будет искать UsersController и PostsController внутри каталога контроллеров , а не внутри controllers / api / v1 , поэтому будьте осторожны.

Создайте папку api с вложенным каталогом v1 внутри контроллеров . Заполните его своими контроллерами:

1
2
3
4
5
6
module Api
    module V1
        class UsersController < ApplicationController
        end
    end
end
1
2
3
4
5
6
module Api
    module V1
        class PostsController < ApplicationController
        end
    end
end

Обратите внимание, что вы не только должны вкладывать файл контроллера в путь api / v1 , но и сам класс также должен находиться в пространстве имен внутри модулей Api и V1 .

Следующий вопрос: как правильно ответить данными в формате JSON? В этой статье мы попробуем эти решения: гемы jBuilder и active_model_serializers. Поэтому, прежде чем перейти к следующему разделу, поместите их в Gemfile :

1
2
gem ‘jbuilder’, ‘~> 2.5’
gem ‘active_model_serializers’, ‘~> 0.10.0’

Затем запустите:

1
bundle install

jBuilder — это популярный гем, поддерживаемый командой Rails, который предоставляет простой DSL (предметно-ориентированный язык), позволяющий вам определять структуры JSON в ваших представлениях.

Предположим, мы хотим отобразить все сообщения, когда пользователь нажимает действие index :

1
2
3
def index
    @posts = Post.order(‘created_at DESC’)
end

Все, что вам нужно сделать, это создать представление с именем соответствующего действия с расширением .json.jbuilder . Обратите внимание, что представление также должно быть расположено под путем api / v1 :

1
2
3
4
5
json.array!
  json.id post.id
  json.title post.title
  json.body post.body
end

json.array! @posts массив @posts . json.id , json.title и json.body генерируют ключи с соответствующими именами, устанавливая аргументы в качестве значений. Если вы перейдете по адресу http: // localhost: 3000 / api / v1 / posts.json , вы увидите вывод, похожий на этот:

1
2
3
4
[
    {«id»: 1, «title»: «Title 1», «body»: «Body 1»},
    {«id»: 2, «title»: «Title 2», «body»: «Body 2»}
]

Что если мы хотим отобразить автора для каждого поста? Это просто:

1
2
3
4
5
6
7
8
9
json.array!
  json.id post.id
  json.title post.title
  json.body post.body
  json.user do
    json.id post.user.id
    json.name post.user.name
  end
end

Результат изменится на:

1
2
3
[
    {«id»: 1, «title»: «Title 1», «body»: «Body 1», «user»: {«id»: 1, «name»: «Username»}}
]

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

Обратите внимание, что jBuilder поддерживает партиалы, как и любое обычное представление Rails, поэтому вы также можете сказать:

1
json.partial!

и затем создайте файл views / api / v1 / posts / _post.json.jbuilder со следующим содержимым:

1
2
3
4
5
6
7
json.id post.id
json.title post.title
json.body post.body
json.user do
    json.id post.user.id
    json.name post.user.name
end

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

Камень rails_model_serializers был создан командой, которая изначально управляла rails-api. Как указано в документации, rails_model_serializers приносит соглашение о конфигурации для вашего поколения JSON. По сути, вы определяете, какие поля должны использоваться при сериализации (то есть при создании JSON).

Вот наш первый сериализатор:

1
2
3
class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :body
end

Здесь мы говорим, что все эти поля должны присутствовать в результирующем JSON. Теперь такие методы, как to_json и as_json вызываемые для поста, будут использовать эту конфигурацию и возвращать соответствующий контент.

Чтобы увидеть его в действии, измените действие index следующим образом:

1
2
3
4
5
def index
    @posts = Post.order(‘created_at DESC’)
     
    render json: @posts
end

as_json будет автоматически вызываться для объекта @posts .

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

1
2
3
4
5
6
7
8
class PostSerializer < ActiveModel::Serializer
  attributes :id, :title, :body
  belongs_to :user
 
  class UserSerializer < ActiveModel::Serializer
    attributes :id, :name
  end
end

Теперь, когда вы сериализируете сообщение, оно автоматически будет содержать вложенный user ключ с его идентификатором и именем. Если позже вы создадите отдельный сериализатор для пользователя с исключенным атрибутом :id :

1
2
3
class UserSerializer < ActiveModel::Serializer
    attributes :name
end

тогда @user.as_json не вернет идентификатор пользователя. Тем не менее, @post.as_json вернет имя пользователя и идентификатор, так что имейте это в виду.

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

Токен будет иметь неограниченный срок жизни и будет создан при регистрации пользователя. Прежде всего, добавьте новый столбец token в таблицу users :

1
rails g migration add_token_to_users token:string:index

Этот индекс должен гарантировать уникальность, поскольку не может быть двух пользователей с одинаковым токеном:

1
add_index :users, :token, unique: true

Применить миграцию:

1
rails db:migrate

Теперь добавьте before_save вызов before_save :

1
before_create -> {self.token = generate_token}

generate_token метод generate_token создаст токен в бесконечном цикле и проверит, является ли он уникальным или нет. Как только найден уникальный токен, верните его:

1
2
3
4
5
6
7
8
private
 
def generate_token
    loop do
      token = SecureRandom.hex
      return token unless User.exists?({token: token})
    end
end

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

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

01
02
03
04
05
06
07
08
09
10
11
12
13
14
def create
    @user = User.new(user_params)
    if @user.save
      render status: :created
    else
      render json: @user.errors, status: :unprocessable_entity
    end
end
 
private
 
def user_params
    params.require(:user).permit(:name)
end

Рекомендуется возвращать значимые коды состояния HTTP, чтобы разработчики точно понимали, что происходит. Теперь вы можете либо предоставить новый сериализатор для пользователей, либо использовать файл .json.jbuilder . Я предпочитаю последний вариант (поэтому я не :json опцию :json методу render ), но вы можете выбрать любой из них. Однако обратите внимание, что токен не всегда должен быть сериализован, например, когда вы возвращаете список всех пользователей — он должен храниться в безопасности!

1
2
3
json.id @user.id
json.name @user.name
json.token @user.token

Следующий шаг — проверить, все ли работает правильно. Вы можете использовать команду curl или написать код на Ruby. Так как эта статья о Ruby, я пойду с опцией кодирования.

Для выполнения HTTP-запроса мы будем использовать гем Фарадея , который предоставляет общий интерфейс для многих адаптеров (по умолчанию Net::HTTP ). Создайте отдельный файл Ruby, включите Фарадей и настройте клиент:

01
02
03
04
05
06
07
08
09
10
11
require ‘faraday’
 
client = Faraday.new(url: ‘http://localhost:3000’) do |config|
  config.adapter Faraday.default_adapter
end
 
response = client.post do |req|
  req.url ‘/api/v1/users’
  req.headers[‘Content-Type’] = ‘application/json’
  req.body = ‘{ «user»: {«name»: «test user»} }’
end

Все эти параметры не требуют пояснений: мы выбираем адаптер по умолчанию, устанавливаем URL запроса на http: // localhost: 300 / api / v1 / users , меняем тип контента на application/json и предоставляем тело нашего запроса. ,

Ответ сервера будет содержать JSON, поэтому для его анализа я буду использовать гем Oj :

1
2
3
4
5
6
require ‘oj’
 
# client here…
 
puts Oj.load(response.body)
puts response.status

Помимо проанализированного ответа, я также отображаю код состояния для целей отладки.

Теперь вы можете просто запустить этот скрипт:

1
ruby api_client.rb

и сохраните полученный токен где-нибудь — мы будем использовать его в следующем разделе.

Чтобы обеспечить аутентификацию токена, можно использовать метод authenticate_or_request_with_http_token . Он является частью модуля ActionController :: HttpAuthentication :: Token :: ControllerMethods , поэтому не забудьте включить его:

1
2
3
4
class PostsController < ApplicationController
    include ActionController::HttpAuthentication::Token::ControllerMethods
    # …
end

Добавьте новую before_action и соответствующий метод:

01
02
03
04
05
06
07
08
09
10
11
12
13
before_action :authenticate, only: [:create, :destroy]
 
# …
 
private
 
# …
 
def authenticate
    authenticate_or_request_with_http_token do |token, options|
      @user = User.find_by(token: token)
    end
end

Теперь, если токен не установлен или если пользователь с таким токеном не может быть найден, будет возвращена ошибка 401, что не позволит выполнить действие.

Обратите внимание, что связь между клиентом и сервером должна осуществляться через HTTPS, поскольку в противном случае токены могут быть легко подделаны. Разумеется, предоставленное решение не является идеальным, и во многих случаях для аутентификации предпочтительно использовать протокол OAuth 2 . Существует как минимум два драгоценных камня, которые значительно упрощают процесс поддержки этой функции: Doorkeeper и oPRO .

Чтобы увидеть нашу аутентификацию в действии, добавьте действие create в PostsController :

1
2
3
4
5
6
7
8
def create
    @post = @user.posts.new(post_params)
    if @post.save
        render json: @post, status: :created
    else
        render json: @post.errors, status: :unprocessable_entity
    end
end

Мы используем здесь сериализатор для отображения правильного JSON. @user уже был установлен внутри before_action .

Теперь проверьте все, используя этот простой код:

01
02
03
04
05
06
07
08
09
10
client = Faraday.new(url: ‘http://localhost:3000’) do |config|
  config.adapter Faraday.default_adapter
  config.token_auth(‘127a74dbec6f156401b236d6cb32db0d’)
end
 
response = client.post do |req|
  req.url ‘/api/v1/posts’
  req.headers[‘Content-Type’] = ‘application/json’
  req.body = ‘{ «post»: {«title»: «Title», «body»: «Text»} }’
end

Замените аргумент, переданный token_auth токеном, полученным при регистрации, и запустите скрипт.

1
ruby api_client.rb

Удаление поста производится аналогичным образом. Добавьте действие destroy :

1
2
3
4
5
6
7
8
def destroy
    @post = @user.posts.find_by(params[:id])
    if @post
      @post.destroy
    else
      render json: {post: «not found»}, status: :not_found
    end
end

Мы разрешаем пользователям уничтожать только те сообщения, которые им принадлежат. Если сообщение успешно удалено, будет возвращен код состояния 204 (без содержимого). Кроме того, вы можете ответить с идентификатором сообщения, который был удален, так как он все еще будет доступен из памяти.

Вот фрагмент кода для тестирования этой новой функции:

1
2
3
4
response = client.delete do |req|
  req.url ‘/api/v1/posts/6’
  req.headers[‘Content-Type’] = ‘application/json’
end

Замените идентификатор сообщения на номер, который работает для вас.

Если вы хотите разрешить другим веб-службам доступ к вашему API (со стороны клиента), тогда CORS (Cross-Origin Resource Sharing) должен быть правильно настроен. По сути, CORS позволяет веб-приложениям отправлять запросы AJAX сторонним службам. К счастью, есть драгоценный камень, называемый стеллажами, который позволяет нам легко все настроить. Добавьте его в Gemfile :

1
gem ‘rack-cors’

Установите это:

1
bundle install

Затем предоставьте конфигурацию в файле config / initializers / cors.rb. На самом деле, этот файл уже создан для вас и содержит пример использования. Вы также можете найти довольно подробную документацию на странице драгоценного камня .

Например, следующая конфигурация позволит любому получить доступ к вашему API любым способом:

1
2
3
4
5
6
7
8
9
Rails.application.config.middleware.insert_before 0, Rack::Cors do
  allow do
    origins ‘*’
 
    resource ‘/api/*’,
      headers: :any,
      methods: [:get, :post, :put, :patch, :delete, :options, :head]
  end
end

Последнее, что я собираюсь упомянуть в этом руководстве, — это как защитить свой API от злоупотреблений и атак типа отказ в обслуживании. Есть замечательный гем под названием rack-attack (созданный людьми из Kickstarter), который позволяет вам занести в черный или белый список клиентов, предотвратить переполнение сервера запросами и многое другое.

Бросьте драгоценный камень в Gemfile :

1
gem ‘rack-attack’

Установите это:

1
bundle install

А затем предоставьте конфигурацию в файле инициализатора rack_attack.rb . В документации к гему перечислены все доступные опции и предложены некоторые варианты использования. Вот пример конфигурации, которая ограничивает доступ к службе для всех, кроме вас, и ограничивает максимальное количество запросов до 5 в секунду:

01
02
03
04
05
06
07
08
09
10
class Rack::Attack
  safelist(‘allow from localhost’) do |req|
    # Requests are allowed if the return value is truthy
    ‘127.0.0.1’ == req.ip ||
  end
 
  throttle(‘req/ip’, :limit => 5, :period => 1.second) do |req|
    req.ip
  end
end

Еще одна вещь, которую нужно сделать, это включить RackAttack в качестве промежуточного программного обеспечения:

1
config.middleware.use Rack::Attack

Мы подошли к концу этой статьи. Надеемся, что теперь вы чувствуете себя более уверенно в разработке API с помощью Rails! Обратите внимание, что это не единственный доступный вариант — еще одно популярное решение, которое существовало довольно давно, — это среда Grape , так что вам может быть интересно ее проверить.

Не стесняйтесь оставлять свои вопросы, если что-то показалось вам неясным. Я благодарю вас за то, что вы остались со мной, и счастливого кодирования!