В настоящее время обычной практикой является сильная зависимость от 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 .
Создание приложения только для API
Чтобы начать, выполните следующую команду:
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
|
Здесь ничего особенного не происходит: пост с заголовком и телом принадлежит пользователю.
Убедитесь, что установлены правильные ассоциации, а также предоставьте несколько простых проверок:
модели / user.rb
1
2
3
|
has_many :posts
validates :name, presence: true
|
модели / post.rb
1
2
3
4
|
belongs_to :user
validates :title, presence: true
validates :body, presence: true
|
Brilliant! Следующим шагом является загрузка нескольких образцов записей во вновь созданные таблицы.
Загрузка демонстрационных данных
Самый простой способ загрузить некоторые данные — использовать файл seed.rb в каталоге db . Тем не менее, я ленив (как и многие программисты) и не хочу думать ни о каком образце контента. Поэтому, почему бы нам не воспользоваться преимуществом фейкерного самоцвета, который может генерировать случайные данные различного типа: имена, электронные письма, хипстерские слова, тексты «lorem ipsum» и многое другое.
Gemfile
1
2
3
|
group :development do
gem ‘faker’
end
|
Установите драгоценный камень:
1
|
bundle install
|
Теперь настройте семена.rb :
дБ / seeds.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
|
Отвечая с JSON
Теперь, конечно, нам нужны некоторые маршруты и контроллеры для создания нашего API. Обычной практикой является вложение маршрутов api/
в api/
path. Кроме того, разработчики обычно предоставляют версию API в пути, например, api/v1/
. Позже, если будут внесены некоторые критические изменения, вы можете просто создать новое пространство имен ( v2
) и отдельный контроллер.
Вот как могут выглядеть ваши маршруты:
конфиг / routes.rb
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 внутри контроллеров . Заполните его своими контроллерами:
Контроллеры / API / v1 / users_controller.rb
1
2
3
4
5
6
|
module Api
module V1
class UsersController < ApplicationController
end
end
end
|
Контроллеры / API / v1 / posts_controller.rb
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 :
Gemfile
1
2
|
gem ‘jbuilder’, ‘~> 2.5’
gem ‘active_model_serializers’, ‘~> 0.10.0’
|
Затем запустите:
1
|
bundle install
|
Использование jBuilder Gem
jBuilder — это популярный гем, поддерживаемый командой Rails, который предоставляет простой DSL (предметно-ориентированный язык), позволяющий вам определять структуры JSON в ваших представлениях.
Предположим, мы хотим отобразить все сообщения, когда пользователь нажимает действие index
:
Контроллеры / API / v1 / posts_controller.rb
1
2
3
|
def index
@posts = Post.order(‘created_at DESC’)
end
|
Все, что вам нужно сделать, это создать представление с именем соответствующего действия с расширением .json.jbuilder . Обратите внимание, что представление также должно быть расположено под путем api / v1 :
просмотры / апи / v1 / сообщения / index.json.jbuilder
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).
Вот наш первый сериализатор:
сериализаторов / post_serializer.rb
1
2
3
|
class PostSerializer < ActiveModel::Serializer
attributes :id, :title, :body
end
|
Здесь мы говорим, что все эти поля должны присутствовать в результирующем JSON. Теперь такие методы, как to_json
и as_json
вызываемые для поста, будут использовать эту конфигурацию и возвращать соответствующий контент.
Чтобы увидеть его в действии, измените действие index
следующим образом:
Контроллеры / API / v1 / posts_controller.rb
1
2
3
4
5
|
def index
@posts = Post.order(‘created_at DESC’)
render json: @posts
end
|
as_json
будет автоматически вызываться для объекта @posts
.
А как насчет пользователей? Сериализаторы позволяют указывать отношения, как это делают модели. Более того, сериализаторы могут быть вложенными:
сериализаторов / post_serializer.rb
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
:
сериализаторов / post_serializer.rb
1
2
3
|
class UserSerializer < ActiveModel::Serializer
attributes :name
end
|
тогда @user.as_json
не вернет идентификатор пользователя. Тем не менее, @post.as_json
вернет имя пользователя и идентификатор, так что имейте это в виду.
Защита API
Во многих случаях мы не хотим, чтобы кто-либо просто выполнял какие-либо действия с использованием API. Итак, давайте представим простую проверку безопасности и заставим наших пользователей отправлять свои токены при создании и удалении сообщений.
Токен будет иметь неограниченный срок жизни и будет создан при регистрации пользователя. Прежде всего, добавьте новый столбец token
в таблицу users
:
1
|
rails g migration add_token_to_users token:string:index
|
Этот индекс должен гарантировать уникальность, поскольку не может быть двух пользователей с одинаковым токеном:
дб / мигрирует / xyz_add_token_to_users.rb
1
|
add_index :users, :token, unique: true
|
Применить миграцию:
1
|
rails db:migrate
|
Теперь добавьте before_save
вызов before_save
:
модели / user.rb
1
|
before_create -> {self.token = generate_token}
|
generate_token
метод generate_token
создаст токен в бесконечном цикле и проверит, является ли он уникальным или нет. Как только найден уникальный токен, верните его:
модели / user.rb
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:
Контроллеры / API / v1 / users_controller.rb
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
), но вы можете выбрать любой из них. Однако обратите внимание, что токен не всегда должен быть сериализован, например, когда вы возвращаете список всех пользователей — он должен храниться в безопасности!
просмотры / API / v1 / пользователи / create.json.jbuilder
1
2
3
|
json.id @user.id
json.name @user.name
json.token @user.token
|
Следующий шаг — проверить, все ли работает правильно. Вы можете использовать команду curl
или написать код на Ruby. Так как эта статья о Ruby, я пойду с опцией кодирования.
Тестирование Регистрация пользователя
Для выполнения HTTP-запроса мы будем использовать гем Фарадея , который предоставляет общий интерфейс для многих адаптеров (по умолчанию Net::HTTP
). Создайте отдельный файл Ruby, включите Фарадей и настройте клиент:
api_client.rb
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 :
api_client.rb
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 , поэтому не забудьте включить его:
Контроллеры / API / v1 / posts_controller.rb
1
2
3
4
|
class PostsController < ApplicationController
include ActionController::HttpAuthentication::Token::ControllerMethods
# …
end
|
Добавьте новую before_action
и соответствующий метод:
Контроллеры / API / v1 / posts_controller.rb
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
:
Контроллеры / API / v1 / posts_controller.rb
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
.
Теперь проверьте все, используя этот простой код:
api_client.rb
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
:
Контроллеры / API / v1 / posts_controller.rb
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 (без содержимого). Кроме того, вы можете ответить с идентификатором сообщения, который был удален, так как он все еще будет доступен из памяти.
Вот фрагмент кода для тестирования этой новой функции:
api_client.rb
1
2
3
4
|
response = client.delete do |req|
req.url ‘/api/v1/posts/6’
req.headers[‘Content-Type’] = ‘application/json’
end
|
Замените идентификатор сообщения на номер, который работает для вас.
Настройка CORS
Если вы хотите разрешить другим веб-службам доступ к вашему API (со стороны клиента), тогда CORS (Cross-Origin Resource Sharing) должен быть правильно настроен. По сути, CORS позволяет веб-приложениям отправлять запросы AJAX сторонним службам. К счастью, есть драгоценный камень, называемый стеллажами, который позволяет нам легко все настроить. Добавьте его в Gemfile :
Gemfile
1
|
gem ‘rack-cors’
|
Установите это:
1
|
bundle install
|
Затем предоставьте конфигурацию в файле config / initializers / cors.rb. На самом деле, этот файл уже создан для вас и содержит пример использования. Вы также можете найти довольно подробную документацию на странице драгоценного камня .
Например, следующая конфигурация позволит любому получить доступ к вашему API любым способом:
конфиг / Инициализаторы / cors.rb
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 :
Gemfile
1
|
gem ‘rack-attack’
|
Установите это:
1
|
bundle install
|
А затем предоставьте конфигурацию в файле инициализатора rack_attack.rb . В документации к гему перечислены все доступные опции и предложены некоторые варианты использования. Вот пример конфигурации, которая ограничивает доступ к службе для всех, кроме вас, и ограничивает максимальное количество запросов до 5 в секунду:
конфиг / Инициализаторы / rack_attack.rb
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 в качестве промежуточного программного обеспечения:
конфиг / application.rb
1
|
config.middleware.use Rack::Attack
|
Вывод
Мы подошли к концу этой статьи. Надеемся, что теперь вы чувствуете себя более уверенно в разработке API с помощью Rails! Обратите внимание, что это не единственный доступный вариант — еще одно популярное решение, которое существовало довольно давно, — это среда Grape , так что вам может быть интересно ее проверить.
Не стесняйтесь оставлять свои вопросы, если что-то показалось вам неясным. Я благодарю вас за то, что вы остались со мной, и счастливого кодирования!