В моей предыдущей статье мы познакомились с oPRO — движком Rails для создания полноценных поставщиков OAuth 2. Мы уже создали сервер (фактический поставщик) и клиентское приложение. На данный момент на сервере установлена базовая система аутентификации на базе Devise . Пользователи могут создавать приложения для получения клиентских и секретных ключей, аутентификации через OAuth 2 и выполнения примеров запросов API. Однако в настоящее время нет механизма хранения данных пользователя на стороне клиента. Более того, мы не получаем никакой информации о пользователе, кроме его токенов. В этой статье будут рассмотрены эти проблемы, а также представлены некоторые дополнительные действия API и рефакторинг кода.
Исходный код для сервера и клиентских приложений можно найти на GitHub .
Представляем пользователей
Хранение токенов внутри сеанса пользователя не очень удобно, поэтому я хотел бы представить новую таблицу users
для клиентского приложения и сохранить там все соответствующие данные. Наличие модели на месте также позволит нам извлечь некоторые методы, чтобы привести в порядок контроллеры.
Создайте новую миграцию:
$ rails g model User email:string uid:string access_token:string refresh_token:string expires_at:string
Помимо токенов и информации об истечении срока действия, мы также будем хранить uid и электронную почту пользователя — как вы, вероятно, делаете при использовании провайдеров аутентификации, таких как Twitter или Facebook.
Измените миграцию, чтобы включить некоторые индексы:
Миграции / xxx_create_users.rb
class CreateUsers < ActiveRecord::Migration def change create_table :users do |t| t.string :email, index: true t.string :uid, index: true, unique: true t.string :access_token t.string :refresh_token t.string :expires_at t.timestamps null: false end end end
и применить его:
$ rake db:migrate
Также добавьте основные правила проверки:
модели / user.rb
[...] validates :access_token, presence: true validates :refresh_token, presence: true validates :expires_at, presence: true validates :uid, presence: true, uniqueness: true [...]
Имея это на месте, мы можем настроить SessionsController
(сейчас я просто кодирую его скелет):
sessions_controller.rb
[...] def create user = authenticate_and_save_user if user login_user flash[:success] = "Welcome, #{user.email}!" else flash[:warning] = "Can't authenticate you..." end redirect_to root_path end [...]
Мы должны позаботиться о двух вещах: выполнить аутентификацию пользователя и фактически войти в систему. Мы можем оставить код из предыдущей итерации
JSON.parse RestClient.post( [...] )
прямо в действии контроллера, но это не очень хорошая идея. В производственной среде вы, вероятно, создали бы отдельный адаптер аутентификации для своего приложения и стали бы его украшением. Однако для наших целей будет достаточно извлечь где-то код в отдельный файл. Я буду придерживаться модели, но вы также можете поместить ее в каталог lib (просто не забудьте запросить этот файл).
модели / opro_api.rb
class OproApi TOKEN_URL = "#{ENV['opro_base_url']}/oauth/token.json" API_URL = "#{ENV['opro_base_url']}/api" attr_reader :access_token, :refresh_token def initialize(access_token: nil, refresh_token: nil) @access_token = access_token @refresh_token = refresh_token end def authenticate!(code) return if access_token JSON.parse(RestClient.post(TOKEN_URL, { client_id: ENV['opro_client_id'], client_secret: ENV['opro_client_secret'], code: code }, accept: :json)) end end
TOKEN_URL
и API_URL
— это просто удобные константы.
access_token
и refresh_token
являются переменными экземпляра, которые будут использоваться в различных методах этого класса.
При определении метода initialize
я перечисляю аргументы в стиле хеш-функции — эта классная функция поддерживается в более новых версиях Ruby и позволяет легко передавать аргументы (вам не нужно запоминать их порядок).
authenticate!
содержит код из предыдущей итерации (я только добавил оператор return
). Отправляет
запрос и возвращает проанализированный JSON.
Теперь вернемся к контроллеру:
sessions_controller.rb
[...] def create user = User.from_opro(OproApi.new.authenticate!(params[:code])) if user login_user flash[:success] = "Welcome, #{user.email}!" else flash[:warning] = "Can't authenticate you..." end redirect_to root_path end [...]
Здесь мы пользуемся преимуществами недавно созданной authenticate!
метод. Следующим шагом является код from_opro
класса from_opro
который принимает хеш пользователя и сохраняет его в базе данных. Если вы прочитали мою статью об OAuth 2 , я представил очень похожий метод, который называется from_omniauth
.
модели / user.rb
[...] class << self def from_opro(auth = nil) return false unless auth user = User.find_or_initialize_by(uid: auth['uid']) user.access_token = auth['access_token'] user.refresh_token = auth['refresh_token'] user.email = auth['email'] user.save user end end [...]
Если по какой-то причине у хэша аутентификации нет значения, просто верните false
— это заставит наш контроллер выдать ошибку. В противном случае найдите или инициализируйте пользователя по имени пользователя, обновите все атрибуты и верните запись в результате.
Большой! Последний шаг — войти в систему. Для этого я введу несколько вспомогательных методов:
application_controller.rb
[...] private [...] def login(user) session[:user_id] = user.id current_user = user end def current_user @current_user ||= User.find_by(id: session[:user_id]) end def current_user=(user) @current_user = user end helper_method :new_opro_token_path, :current_user [...]
Это очень простые методы, которые вы, вероятно, видели бесчисленное количество раз. current_user
будет использоваться в представлениях, поэтому я помечаю его как вспомогательный.
Давайте также изменим представление:
просмотров / страниц / index.html.erb
<h1>Welcome!</h1> <% if current_user %> <ul> <li><%= link_to 'Show some money', api_tests_path %></li> </ul> <% else %> <%= link_to 'Authenticate via oPRO', new_opro_token_path %> <% end %>
Я предполагаю, что пользователи могут захотеть выйти из системы, поэтому представьте также соответствующее действие:
конфиг / routes.rb
[...] delete '/logout', to: 'sessions#destroy', as: :logout [...]
sessions_controller.rb
[...] def destroy logout flash[:success] = "See you!" redirect_to root_path end [...]
logout
— это еще один метод:
application_controller.rb
[...] def logout session.delete(:user_id) current_user = nil end [...]
Представьте новую ссылку:
просмотров / страниц / index.html.erb
<h1>Welcome!</h1> <% if current_user %> <ul> <li><%= link_to 'Show some money', api_tests_path %></li> </ul> <%= link_to 'Logout', logout_path, method: :delete %> <% else %> <%= link_to 'Authenticate via oPRO', new_opro_token_path %> <% end %>
Настройка хэша аутентификации
В этот момент вы можете подумать: «Как в мире мы можем получить электронную почту и идентификатор пользователя, если по умолчанию oPRO перечисляет только токены и информацию об истечении срока действия?». Ну, вы правы, с этим нужно что-то делать , Проблема, однако, в том, что текущая стабильная версия не имеет возможности переопределить TokenController
. Этот контроллер фактически заботится или генерирует все токены и создает хэш аутентификации. Тем не менее, я немного изменил исходный код OPRO, который был объединен с master
веткой. Сейчас вам нужно будет указать это напрямую:
Gemfile
[...] gem 'opro', github: 'opro/opro', branch: 'master' [...]
Если что-то изменится в будущем, я буду обновлять эту статью по мере необходимости.
Прежде всего, введите новый маршрут:
конфиг / routes.rb
[...] mount_opro_oauth controllers: { oauth_new: 'oauth/auth', oauth_token: 'oauth/token' }, except: :docs [...]
Создайте собственный контроллер, унаследованный от исходного:
Контроллеры / OAuth / token_controller.rb
class Oauth::TokenController < Opro::Oauth::TokenController end
Нам не нужно вносить исправления в какие-либо действия, поскольку единственное, что нужно сделать, это изменить представление:
просмотров / OAuth / маркер / create.json.jbuilder
json.access_token @auth_grant.access_token json.token_type Opro.token_type || 'bearer' json.refresh_token @auth_grant.refresh_token json.expires_in @auth_grant.expires_in json.uid @auth_grant.user.id json.email @auth_grant.user.email
jbuilder позаботится о создании правильного JSON для нас.
Все, кроме uid
и email
, взято из исходного представления . Вы можете добавить любые другие поля здесь, если необходимо.
Тебе хорошо идти. Загрузите сервер и попробуйте пройти аутентификацию. Вся информация пользователя должна храниться правильно.
Работа с API
Немного Рефакторинга
Мы хорошо поработали, но мне все еще не нравится, как наш код выполняет запросы API. Давайте распакуем его в файл opro_api.rb :
модели / opro_api.rb
[...] def test_api JSON.parse(RestClient.get("#{ENV['opro_base_url']}/oauth_tests/show_me_the_money.json", params: { access_token: access_token }, accept: :json)) end [...]
Теперь это можно назвать из действия контроллера:
api_tests_controller.rb
class ApiTestsController < ApplicationController before_action :prepare_client def index @response = @client.test_api end private def prepare_client @client = OproApi.new(access_token: current_user.access_token) end end
Нам потребуется @client
для выполнения любого запроса API, поэтому я поместил его внутри before_action
.
Это хорошо, но что если текущий пользователь по какой-то причине не имеет токена? Давайте проверим это, прежде чем делать что-то еще:
api_tests_controller.rb
class ApiTestsController < ApplicationController before_action :check_token before_action :prepare_client [...] private [...] def check_token redirect_to new_opro_token_path and return if !current_user || current_user.token_missing? end end
модели / user.rb
[...] def token_missing? !self.access_token.present? end [...]
Теперь, если токен не существует, пользователю будет предложено пройти аутентификацию еще раз.
Представляем больше действий
Давайте добавим еще два пользовательских действия API: получение информации о пользователе и обновление пользователя. Первым шагом является добавление нового Api::UsersController
в серверное приложение:
конфиг / routes.rb
[...] namespace :api do resources :users, only: [:show, :update] end [...]
Контроллеры / API / users_controller.rb
class Api::UsersController < ApplicationController def show @user = User.find(params[:id]) end def update @user = User.find(params[:id]) @user.last_sign_in_ip = params[:ip] render json: {result: @user.save} end end
На самом деле не имеет значения, что делает update
, поэтому для демонстрации давайте просто last_sign_in_ip
столбец last_sign_in_ip
представленный Devise.
Не забудьте посмотреть:
просмотров / API / пользователей / show.json.jbuilder
json.user do json.email @user.email end
Еще раз, я возвращаю некоторые образцы данных здесь.
Однако следует отметить две вещи. Перед выполнением этих действий мы не проверяли, есть ли в запросе токен доступа и действителен ли он. oPRO предоставляет allow_oauth!
метод, который принимает only
и без параметров, как и before_action
. По умолчанию никакие действия запрещены на основании токена доступа, поэтому добавьте эту строку в контроллер:
Контроллеры / API / users_controller.rb
class Api::UsersController < ApplicationController allow_oauth! [...] end
Еще одна вещь, которую следует учитывать, это то, что мы не будем отправлять токен CSRF при выполнении действия update
, поэтому Rails вызовет исключение. Чтобы избежать этого, измените эту строку:
application_controller.rb
[...] protect_from_forgery [...]
Теперь запрос защиты от подделки будет использовать null_session
, что означает, что сеанс будет аннулирован, если токен CSRF не предоставлен, но не сброшен полностью. Пока мы полагаемся на токен доступа для выполнения аутентификации, все должно быть хорошо.
Вернитесь к клиентскому приложению и настройте API-интерфейс нашего бедняка:
модели / opro_api.rb
[...] def get_user(id) JSON.parse(RestClient.get("#{API_URL}/users/#{id}.json", params: { access_token: access_token }, accept: :json)) end def update_user(id) JSON.parse(RestClient.patch("#{API_URL}/users/#{id}.json", { access_token: access_token, ip: "#{rand(100)}.1.1.1" }, accept: :json)) end [...]
Теперь контроллер:
api_tests_controller.rb
[...] def show @response = @client.get_user(params[:id]) end def update @response = @client.update_user(params[:id]) end [...]
Представления просто предоставят ответ:
просмотров / api_tests / show.html.erb
<pre><%= @response.to_yaml %></pre>
просмотров / api_tests / update.html.erb
<pre><%= @response.to_yaml %></pre>
Добавить новые маршруты:
конфиг / routes.rb
[...] resources :api_tests, only: [:index, :show, :update] [...]
Предоставьте ссылки на эти новые действия:
просмотров / страниц / index.html.erb
[...] <ul> <li><%= link_to 'Show some money', api_tests_path %></li> <li><%= link_to 'Get a user', api_test_path(1) %></li> <li><%= link_to 'Update a user', api_test_path(1), method: :patch %></li> </ul> [...]
Я просто жестко запрограммировал здесь идентификатор пользователя, но для этой демонстрации это не имеет значения. Продолжайте играть немного API! Однако обратите внимание, что для корректной работы действия update
вы должны разрешить «запись» доступа к приложению при аутентификации. Я расскажу больше о различных разрешениях в следующем разделе.
Вывод
Наше приложение начинает выглядеть довольно красиво, но, опять же, этого недостаточно. У нас еще есть несколько вещей, чтобы позаботиться о:
- Жетоны доступа должны иметь ограниченный срок службы, и следует ввести некоторые ограничения скорости.
- Мы не обсуждали другие сложные темы, такие как работа с областью действия, представление собственного решения для проверки подлинности и обмен учетными данными пользователя для токена.
Итак, последняя, но не менее важная часть этой статьи будет охватывать все эти темы. Держись крепче!