В моей предыдущей серии я показал, как настроить собственного провайдера OAuth 2, используя oPRO , движок Rails. Сегодня мы собираемся решить ту же проблему, но на этот раз с помощью другого, более популярного инструмента — Doorkeeper gem , созданного Applicake в 2011 году. С тех пор проект значительно развился и теперь представляет собой полноценное и удобное решение. Doorkeeper может использоваться как с базовыми приложениями Rails, так и с Grape .
В этой статье я покажу вам, как создать свой собственный поставщик OAuth 2 и безопасный API с помощью Doorkeeper. Мы сделаем основные приготовления, интегрируем Doorkeeper, немного настроим его и представим области применения . Во второй части этой серии мы обсудим более сложные вещи, такие как настройка представлений, использование маркеров обновления, создание собственного поставщика OmniAuth и защита маршрутов Doorkeeper по умолчанию.
Исходный код клиентских и серверных приложений можно найти на GitHub .
Создание приложений
Я собираюсь использовать Rails 4.2 для этой демонстрации. Мы создадим два приложения: фактический поставщик (назовем его «сервер») и приложение для тестирования («клиент»). Начните с сервера:
$ rails new Keepa -T
Нам понадобится какая-то аутентификация для этого приложения, но Doorkeeper не определяет, какой из них использовать. Недавно я рассмотрел множество вариантов , но сегодня давайте закодируем наше простое решение с использованием bcrypt .
Gemfile
[...]
gem 'bcrypt-ruby'
[...]
Установите гем, сгенерируйте и примените новую миграцию:
$ bundle install
$ rails g model User email:string:index password_digest:string
$ rake db:migrate
Теперь оснастим модель User
модели / user.rb
[...]
has_secure_password
validates :email, presence: true
[...]
Создайте новый контроллер для регистрации:
users_controller.rb
class UsersController < ApplicationController
def new
@user = User.new
end
def create
@user = User.new(user_params)
if @user.save
session[:user_id] = @user.id
flash[:success] = "Welcome!"
redirect_to root_path
else
render :new
end
end
private
def user_params
params.require(:user).permit(:email, :password, :password_confirmation)
end
end
Вот соответствующий вид:
просмотров / пользователей / new.html.erb
<h1>Register</h1>
<%= form_for @user do |f| %>
<%= render 'shared/errors', object: @user %>
<div>
<%= f.label :email %>
<%= f.email_field :email %>
</div>
<div>
<%= f.label :password %>
<%= f.password_field :password %>
</div>
<div>
<%= f.label :password_confirmation %>
<%= f.password_field :password_confirmation %>
</div>
<%= f.submit %>
<% end %>
<%= link_to 'Log In', new_session_path %>
просмотров / общий / _errors.html.erb
<% if object.errors.any? %>
<div>
<h5>Some errors were found:</h5>
<ul>
<% object.errors.full_messages.each do |msg| %>
<li><%= msg %></li>
<% end %>
</ul>
</div>
<% end %>
Добавьте несколько маршрутов:
конфиг / routes.rb
[...]
resources :users, only: [:new, :create]
[...]
Чтобы проверить, вошел ли пользователь в систему или нет, мы будем использовать старый добрый метод current_user
application_controller.rb
[...]
private
def current_user
@current_user ||= User.find(session[:user_id]) if session[:user_id]
end
helper_method :current_user
[...]
Также требуется отдельный контроллер для управления пользовательскими сессиями:
sessions_controller.rb
class SessionsController < ApplicationController
def new
end
def create
@user = User.find_by(email: params[:email])
if @user && @user.authenticate(params[:password])
session[:user_id] = @user.id
flash[:success] = "Welcome back!"
redirect_to root_path
else
flash[:warning] = "You have entered incorrect email and/or password."
render :new
end
end
def destroy
session.delete(:user_id)
redirect_to root_path
end
end
authenticate
Вот мнения и маршруты:
просмотры / сессия / new.html.erb
<h1>Log In</h1>
<%= form_tag sessions_path, method: :post do %>
<div>
<%= label_tag :email %>
<%= email_field_tag :email %>
</div>
<div>
<%= label_tag :password %>
<%= password_field_tag :password %>
</div>
<%= submit_tag 'Log In!' %>
<% end %>
<%= link_to 'Register', new_user_path %>
конфиг / routes.rb
[...]
resources :sessions, only: [:new, :create]
delete '/logout', to: 'sessions#destroy', as: :logout
[...]
Наконец, добавьте контроллер статических страниц, корневую страницу и маршрут:
pages_controller.rb
class PagesController < ApplicationController
def index
end
end
просмотров / страниц / index.html.erb
<% if current_user %>
You are logged in as <%= current_user.email %><br>
<%= link_to 'Log Out', logout_path, method: :delete %>
<% else %>
<%= link_to 'Log In', new_session_path %><br>
<%= link_to 'Register', new_user_path %>
<% end %>
конфиг / routes.rb
[...]
root to: 'pages#index'
[...]
Все должно быть знакомо до сих пор. Серверное приложение готово, и мы можем начать интеграцию Doorkeeper.
Интегрирующий привратник
Добавьте новый драгоценный камень в Gemfile :
Gemfile
[...]
gem 'doorkeeper'
[...]
Установите его и запустите генератор Привратника:
$ bundle install
$ rails generate doorkeeper:install
Этот генератор создаст новый файл инициализатора и добавит строку use_doorkeeper
route.rb . Эта строка содержит несколько маршрутов Doorkeeper (для регистрации нового OAuth 2, запроса токена доступа и т. Д.), Которые мы обсудим позже.
Следующим шагом является создание миграций. По умолчанию Doorkeeper использует ActiveRecord, но вы можете использовать doorkeeper-mongodb для поддержки Mongo.
$ rails generate doorkeeper:migration
Вы можете добавить внешний ключ, как описано здесь , но я просто добавлю миграцию:
$ rake db:migrate
Откройте файл инициализатора Doorkeeper и найдите строку resource_owner_authenticator do
По умолчанию это вызывает исключение, поэтому замените содержимое блока на:
конфиг / Инициализаторы / doorkeeper.rb
[...]
User.find_by_id(session[:user_id]) || redirect_to(new_session_url)
[...]
Модель User
Вы можете загрузить сервер, зарегистрироваться и перейти на localhost: 3000 / oauth / apps . Это страница для создания вашего нового приложения OAuth 2. Создайте его, указав «http: // localhost: 3001 / oauth / callback» в качестве URL-адреса перенаправления. Обратите внимание, что мы используем порт 3001 для клиентского приложения (еще не существует), поскольку порт 3000 уже используется серверным приложением. После этого вы увидите идентификатор приложения и секретный ключ. Оставьте эту страницу открытой или запишите значения.
Следующим шагом является создание клиентского приложения.
Создание клиентского приложения
Запустите генератор Rails, чтобы создать приложение:
$ rails new KeepaClient -T
Добавьте контроллер статических страниц, корневую страницу и маршрут:
pages_controller.rb
class PagesController < ApplicationController
def index
end
end
просмотров / страниц / index.html.erb
<h1>Welcome!</h1>
конфиг / routes.rb
[...]
root to: 'pages#index'
[...]
Теперь давайте также создадим файл local_env.yml для хранения некоторой конфигурации, в частности Client Id и Secret, полученных от серверного приложения на предыдущем шаге:
конфиг / local_env.yml
server_base_url: 'http://localhost:3000'
oauth_token: <CLIENT ID>'
oauth_secret: '<SECRET>'
oauth_redirect_uri: 'http%3A%2F%2Flocalhost%3A3001%2Foauth%2Fcallback'
Загрузите это в ENV
конфиг / application.rb
[...]
if Rails.env.development?
config.before_configuration do
env_file = File.join(Rails.root, 'config', 'local_env.yml')
YAML.load(File.open(env_file)).each do |key, value|
ENV[key.to_s] = value
end if File.exists?(env_file)
end
end
[...]
Исключите файл .yml из контроля версий, если хотите:
.gitignore
[...]
config/local_env.yml
[...]
Получение токена доступа
Хорошо, мы готовы получить токен доступа, который будет использоваться для выполнения запросов API. Вы можете использовать для этого гем oauth2 , как описано здесь . Однако я еще раз остановлюсь на более низкоуровневом решении, чтобы вы поняли, как происходит весь процесс.
Мы будем использовать rest-client для отправки запросов.
Добавьте новый драгоценный камень и bundle install
Gemfile
[...]
gem 'rest-client'
[...]
Чтобы получить токен доступа, пользователю сначала необходимо посетить URL «localhost: 3000 / oauth / authorize», указав идентификатор клиента, URI перенаправления и тип ответа. Давайте введем вспомогательный метод для генерации соответствующего URL:
application_helper.rb
[...]
def new_oauth_token_path
"#{ENV['server_base_url']}/oauth/authorize?client_id=#{ENV['oauth_token']}&redirect_uri=#{ENV['oauth_redirect_uri']}&response_type=code"
end
[...]
Отобразите его на главной странице клиентского приложения:
просмотров / страниц / index.html.erb
[...]
<%= link_to 'Authorize via Keepa', new_oauth_token_path %>
[...]
Теперь представим маршрут обратного вызова:
конфиг / routes.rb
[...]
get '/oauth/callback', to: 'sessions#create'
[...]
sessions_controller.rb
class SessionsController < ApplicationController
def create
req_params = "client_id=#{ENV['oauth_token']}&client_secret=#{ENV['oauth_secret']}&code=#{params[:code]}&grant_type=authorization_code&redirect_uri=#{ENV['oauth_redirect_uri']}"
response = JSON.parse RestClient.post("#{ENV['server_base_url']}/oauth/token", req_params)
session[:access_token] = response['access_token']
redirect_to root_path
end
end
После посещения «localhost: 3000 / oauth / authorize» пользователь будет перенаправлен на URL обратного вызова с установленным параметром code
Внутри действия create
client_id
client_secret
code
grant_type
redirect_uri
Если все было сделано правильно, ответ будет содержать JSON с токеном доступа и временем его жизни (по умолчанию установлено 2 часа). В противном случае будет возвращена ошибка 401.
Затем мы анализируем ответ и сохраняем токен доступа в сеансе пользователя. Конечно, вам нужно будет ввести какую-то аутентификацию для реального приложения, но здесь все просто.
Если вы читаете мои статьи о OPRO, этот процесс должен быть очень знакомым. Если нет — мои поздравления, вы только что закодировали клиентское приложение, и теперь пользователи могут получить токены доступа.
Это все замечательно, но для чего мы можем использовать эти токены? Очевидно, что для защиты нашего API, давайте представим это сейчас!
Представляем простой API
Вернитесь в приложение сервера и создайте новый контроллер:
Контроллеры / API / users_controller.rb
class Api::UsersController < ApplicationController
before_action :doorkeeper_authorize!
def show
render json: current_resource_owner.as_json
end
private
def current_resource_owner
User.find(doorkeeper_token.resource_owner_id) if doorkeeper_token
end
end
Давайте продолжим шаг за шагом здесь.
before_action :doorkeeper_authorize!
позволяет нам защитить действия контроллера, предотвращая неавторизованные запросы. Это означает, что пользователи должны предоставить токены доступа для выполнения действия. Если токен не предоставлен, Дверной дворник преградит им путь, сказав: «Ты не пройдешь!», Как Гэндальф. Ну, на самом деле в нашем случае будет возвращена ошибка 401, но вы поняли.
current_resource_owner
doorkeeper_token.resource_owner_id
resource_owner_authenticator
as_json
User
Вы можете предоставить опцию except
current_resource_owner.as_json(except: :password_digest)
Добавить новый маршрут:
конфиг / routes.rb
namespace :api do
get 'user', to: 'users#show'
end
Теперь, если вы попытаетесь получить доступ к «localhost: 3000 / api / user», вы увидите пустую страницу. Откройте консоль, и вы увидите ошибку 401, означающую, что это действие теперь защищено.
Если вы хотите настроить эту неавторизованную страницу, вы можете переопределить метод doorkeeper_unauthorized_render_options
ApplicationController
Например, добавьте сообщение об ошибке, подобное этому:
application_controller.rb
def doorkeeper_unauthorized_render_options(error: nil)
{ json: { error: "Not authorized" } }
end
Настроить главную страницу клиентского приложения:
просмотров / страниц / index.html.erb
[...]
<% if session[:access_token] %>
<%= link_to 'Get User', "http://localhost:3000/api/user?access_token=#{session[:access_token]}" %>
<% else %>
<%= link_to 'Authorize via Keepa', new_oauth_token_path %>
<% end %>
Авторизуйтесь, нажмите на ссылку «Получить пользователя» и посмотрите результат — вы должны увидеть данные своего пользователя!
Работа с областями действия
Области — это способ определить, какие действия сможет выполнить клиент. Давайте представим две новые области:
-
public
-
write
Прежде всего, измените файл инициализатора Doorkeeper, чтобы включить следующие новые области:
конфиг / Инициализаторы / doorkeeper.rb
[...]
default_scopes :public
optional_scopes :write
[...]
default_scopes
optional_scopes
Если пропущен какой-то неизвестный объем, возникнет ошибка.
Обновите контроллер API:
апи / users_controller.rb
[...]
before_action -> { doorkeeper_authorize! :public }, only: :show
before_action -> { doorkeeper_authorize! :write }, only: :update
def show
render json: current_resource_owner.as_json
end
def update
render json: { result: current_resource_owner.touch(:updated_at) }
end
[...]
Мы передаем имя области действия doorkeeper_authorize!
говоря, какой объем требуется для выполнения действия. Обратите внимание, что можно также передать несколько областей:
doorkeeper_authorize! :admin, :write
Это означает, что действие может быть выполнено, если у клиента есть права admin
или права на write
Если вы хотите, чтобы присутствовали оба разрешения, используйте следующую конструкцию:
doorkeeper_authorize! :admin
doorkeeper_authorize! :write
Введите новый маршрут:
конфиг / routes.rb
[...]
namespace :api do
get 'user', to: 'users#show'
get 'user/update', to: 'users#update'
end
[...]
Чтобы все было как можно проще, я использую глагол GET, но для реального приложения было бы лучше использовать PATCH. Теперь в клиентском приложении давайте изменим вспомогательный метод, чтобы запрашивать как public
write
application_helper.rb
[...]
def new_oauth_token_path
"#{ENV['server_base_url']}/oauth/authorize?client_id=#{ENV['oauth_token']}&redirect_uri=#{ENV['oauth_redirect_uri']}&response_type=code&scope=public+write"
end
[...]
Мы добавили параметр области здесь. Несколько областей должны быть разделены знаком scope
Наконец, посетите «http: // localhost: 3000 / oauth / Applications», откройте свое приложение для редактирования и заполните поле «Области» +
public write
Перезагрузите сервер и посетите страницу авторизации. Вы заметите, что теперь у пользователя запрашивается разрешение на выполнение двух действий, но его имена не очень полезны. Например, что означает «писать»? Это может быть очевидно для вас, но не для наших пользователей. Поэтому было бы лучше предоставить немного больше информации по каждой области. Это можно сделать, отредактировав файл I18n привратника, который был сгенерирован для вас:
конфиг / локали / doorkeeper.en.yml
en:
doorkeeper:
scopes:
public: 'Access your public data.'
write: 'Make changes to your profile.'
[...]
Теперь пользователи увидят подробное описание для каждой области.
Наконец, добавьте новую ссылку на главную страницу:
просмотров / страниц / index.html.erb
[...]
<% if session[:access_token] %>
<%= link_to 'Get User', "http://localhost:3000/api/user?access_token=#{session[:access_token]}" %>
<%= link_to 'Update User', "http://localhost:3000/api/user/update?access_token=#{session[:access_token]}" %>
<% else %>
<%= link_to 'Authorize via Keepa', new_oauth_token_path %>
<% end %>
Иди и попробуй это!
Вывод
В этой статье мы заложили основу для наших приложений и интегрировали Doorkeeper. В настоящее время пользователи могут регистрировать свои приложения, запрашивать токены доступа и работать с областями действия. API теперь защищен, а несанкционированный доступ предотвращен.
В следующем посте мы представим токены обновления, защитим общие маршруты Doorkeeper, настроим представления и создадим собственного провайдера для OmniAuth, который можно упаковать как драгоценный камень.
Как всегда, не стесняйтесь оставлять свои вопросы и до скорой встречи!