Аутентификация пользователя с помощью Grape and Devise
Мне сложно понять и правильно внедрить Аутентификацию пользователя в API. Другими словами, у меня есть серьезная проблема, чтобы понять интеграцию API Grape с интерфейсами, такими как Backbone.js, AngularJS или Ember.js.
Я пытаюсь развернуть все разные подходы и много читать об этом, но Google возвращает мне действительно плохие ресурсы, и мне кажется, что нет действительно хорошей статьи по этой теме - Rails и аутентификация пользователя с помощью Devise и front-end frameworks.
Я опишу свой текущий стержень, и я надеюсь, что вы можете предоставить мне некоторые отзывы о моей реализации и, возможно, указать мне в правильном направлении.
Текущая реализация
У меня есть backend Rails API REST со следующим Gemfile (я намеренно сокращу весь код файла)
gem 'rails', '4.1.6'
gem 'mongoid', '~> 4.0.0'
gem 'devise'
gem 'grape'
gem 'rack-cors', :require => 'rack/cors'
В моей текущей реализации есть только API-интерфейсы со следующими маршрутами (routes.rb):
api_base /api API::Base
GET /:version/posts(.:format)
GET /:version/posts/:id(.:format)
POST /:version/posts(.:format)
DELETE /:version/posts/:id(.:format)
POST /:version/users/authenticate(.:format)
POST /:version/users/register(.:format)
DELETE /:version/users/logout(.:format)
Я создал следующую модель user.rb
class User
include Mongoid::Document
devise :database_authenticatable, :registerable,
:recoverable, :rememberable, :trackable, :validatable
field :email, type: String, default: ""
field :encrypted_password, type: String, default: ""
field :authentication_token, type: String
before_save :ensure_authentication_token!
def ensure_authentication_token!
self.authentication_token ||= generate_authentication_token
end
private
def generate_authentication_token
loop do
token = Devise.friendly_token
break token unless User.where(authentication_token: token).first
end
end
end
В моих контроллерах я создал следующую структуру папок: controllers- > api- > v1, и я создал следующую аутентификацию общего модуля (authentication.rb)
module API
module V1
module Authentication
extend ActiveSupport::Concern
included do
before do
error!("401 Unauthorized", 401) unless authenticated?
end
helpers do
def warden
env['warden']
end
def authenticated?
return true if warden.authenticated?
params[:access_token] && @user = User.find_by(authentication_token: params[:access_token])
end
def current_user
warden.user || @user
end
end
end
end
end
end
Поэтому каждый раз, когда я хочу убедиться, что мой ресурс будет вызван с помощью токена аутентификации, я просто могу добавить это, вызвав: include API::V1::Authentication
к ресурсу Grape:
module API
module V1
class Posts < Grape::API
include API::V1::Defaults
include API::V1::Authentication
Теперь у меня есть еще один ресурс Grape под названием Users (users.rb), и здесь я реализую методы аутентификации, регистрации и выхода из системы (я думаю, что я смешиваю здесь яблоки с грушами, и я должен извлечь процесс входа/выхода из системы в другой Ресурс винограда - сеанс).
module API
module V1
class Users < Grape::API
include API::V1::Defaults
resources :users do
desc "Authenticate user and return user object, access token"
params do
requires :email, :type => String, :desc => "User email"
requires :password, :type => String, :desc => "User password"
end
post 'authenticate' do
email = params[:email]
password = params[:password]
if email.nil? or password.nil?
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
return
end
user = User.find_by(email: email.downcase)
if user.nil?
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
return
end
if !user.valid_password?(password)
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
return
else
user.ensure_authentication_token!
user.save
status(201){status: 'ok', token: user.authentication_token }
end
end
desc "Register user and return user object, access token"
params do
requires :first_name, :type => String, :desc => "First Name"
requires :last_name, :type => String, :desc => "Last Name"
requires :email, :type => String, :desc => "Email"
requires :password, :type => String, :desc => "Password"
end
post 'register' do
user = User.new(
first_name: params[:first_name],
last_name: params[:last_name],
password: params[:password],
email: params[:email]
)
if user.valid?
user.save
return user
else
error!({:error_code => 404, :error_message => "Invalid email or password."}, 401)
end
end
desc "Logout user and return user object, access token"
params do
requires :token, :type => String, :desc => "Authenticaiton Token"
end
delete 'logout' do
user = User.find_by(authentication_token: params[:token])
if !user.nil?
user.remove_authentication_token!
status(200)
{
status: 'ok',
token: user.authentication_token
}
else
error!({:error_code => 404, :error_message => "Invalid token."}, 401)
end
end
end
end
end
end
Я понимаю, что я представляю здесь тонну кода, и это может не иметь смысла, но это то, что у меня есть сейчас, и я могу использовать authentication_token
для вызовов против моего API, которые защищены модулем Authentication
.
Я чувствую, что это решение не очень хорошо, но я действительно ищу проще, как добиться аутентификации пользователей через API. У меня есть несколько вопросов, которые перечислены ниже.
Вопросы
- Считаете ли вы, что такая реализация опасна, если да, то почему? - Я думаю, что это из-за использования одного токена. Есть ли способ улучшить этот шаблон? Я также видел реализацию с отдельной моделью
Token
, которая имеет время истечения срока действия и т.д. Но я думаю, что это почти похоже на повторное использование колеса, потому что для этой цели я могу реализовать OAuth2. Я хотел бы иметь более легкое решение.
- Хорошей практикой является создание нового модуля для аутентификации и включение его только в ресурсы, где это необходимо?
- Знаете ли вы о каком-либо хорошем учебнике по этой теме?
Rails + Devise + Grape? Кроме того, вы знаете о каких-либо хороших
проект Rails с открытым исходным кодом, который реализован таким образом?
- Как я могу реализовать его с другим подходом, который более безопасен?
Я прошу прощения за такой длинный пост, но я надеюсь, что у большего числа людей будет такая же проблема, и это может помочь мне найти больше ответов на мои вопросы.
Ответы
Ответ 1
Добавить token_authenticable для разработки модулей (работает с готовыми версиями <= 3.2)
В user.rb добавить: token_authenticatable в список модулей разработки, он должен выглядеть примерно так:
class User < ActiveRecord::Base
# ..code..
devise :database_authenticatable,
:token_authenticatable,
:invitable,
:registerable,
:recoverable,
:rememberable,
:trackable,
:validatable
attr_accessible :name, :email, :authentication_token
before_save :ensure_authentication_token
# ..code..
end
Создайте токен аутентификации самостоятельно (если версия версии > 3.2)
class User < ActiveRecord::Base
# ..code..
devise :database_authenticatable,
:invitable,
:registerable,
:recoverable,
:rememberable,
:trackable,
:validatable
attr_accessible :name, :email, :authentication_token
before_save :ensure_authentication_token
def ensure_authentication_token
self.authentication_token ||= generate_authentication_token
end
private
def generate_authentication_token
loop do
token = Devise.friendly_token
break token unless User.where(authentication_token: token).first
end
end
Добавить перенос для токена аутентификации
rails g migration add_auth_token_to_users
invoke active_record
create db/migrate/20141101204628_add_auth_token_to_users.rb
Изменить файл миграции, чтобы добавить: authentication_token столбец пользователям
class AddAuthTokenToUsers < ActiveRecord::Migration
def self.up
change_table :users do |t|
t.string :authentication_token
end
add_index :users, :authentication_token, :unique => true
end
def self.down
remove_column :users, :authentication_token
end
end
Запуск миграции
rake db:migrate
Создать токен для существующих пользователей
Нам нужно вызвать save для каждого экземпляра пользователя, который обеспечит наличие токена аутентификации для каждого пользователя.
User.all.each(&:save)
Защищенный API-интерфейс Grape с использованием токена аутентификации
Вам нужно добавить код ниже в API:: Root для добавления проверки подлинности на токенах. Если вы не знаете API:: Root, тогда прочитайте Создание RESTful API с использованием Grape
В приведенном ниже примере мы аутентифицируем пользователя на основе двух сценариев. Если пользователь зарегистрирован в веб-приложении, то используйте тот же сеанс. Если сеанс недоступен и передается токен auth, найдите пользователя на основе токена.
# lib/api/root.rb
module API
class Root < Grape::API
prefix 'api'
format :json
rescue_from :all, :backtrace => true
error_formatter :json, API::ErrorFormatter
before do
error!("401 Unauthorized", 401) unless authenticated
end
helpers do
def warden
env['warden']
end
def authenticated
return true if warden.authenticated?
params[:access_token] && @user = User.find_by_authentication_token(params[:access_token])
end
def current_user
warden.user || @user
end
end
mount API::V1::Root
mount API::V2::Root
end
end
Ответ 2
Хотя мне нравится вопрос и ответ, данный @MZaragoza, я думаю, стоит отметить, что token_authentical был удален из Devise по какой-то причине! Использование токенов уязвимо для временных атак. См. Также этот пост и Devise blog Поэтому я не поддержал @MZaragoza ответ.
Если вы используете свой API в сочетании с Doorkeeper, вы можете сделать что-то подобное, но вместо проверки подлинности_тока в таблице/модели пользователя вы ищете маркер в таблице OauthAccessTokens, т.е.
def authenticated
return true if warden.authenticated?
params[:access_token] && @user = OauthAccessToken.find_by_token(params[:access_token]).user
end
Это более безопасно, потому что этот токен (т.е. фактический access_token) существует только на определенный промежуток времени.
Примечание. Чтобы иметь возможность сделать это, у вас должна быть модель пользователя и модель OauthAccessToken:
class User < ActiveRecord::Base
has_many :oauth_access_tokens
end
class OauthAccessToken < ActiveRecord::Base
belongs_to :user, foreign_key: 'resource_owner_id'
end
РЕДАКТИРОВАТЬ: Также обратите внимание, что обычно вы не должны включать access_token в URL-адрес: http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16#section-2.3