Подтвердить что ты не робот

Аутентификация пользователя с помощью 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 с открытым исходным кодом, который реализован таким образом?
  • Как я могу реализовать его с другим подходом, который более безопасен?

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

4b9b3361

Ответ 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