关于Rails上的ruby:Grape和Devise的用户身份验证

User Authentication with Grape and Devise

我很难理解并且也无法正确实现API中的用户身份验证。换句话说,我很难理解Grape API与前端框架(例如Backbone.js,AngularJS或Ember.js)的集成。

我正在尝试探讨所有不同的方法,并阅读了很多相关内容,但是Google向我退回的资源确实很差,在我看来,好像没有关于该主题的非常好的文章-使用Devise进行Rails和用户身份验证和前端框架。

我将描述我目前的工作重点,希望您能向我提供有关实施情况的反馈,也许会指出正确的方向。

当前实现

我具有带有以下Gemfile的后端Rails REST API(我将特意缩短所有文件代码)

1
2
3
4
5
gem 'rails', '4.1.6'
gem 'mongoid', '~> 4.0.0'
gem 'devise'
gem 'grape'
gem 'rack-cors', :require => 'rack/cors'

我当前的实现仅具有以下Routes(routes.rb)的API:

1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
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

1
2
3
4
5
module API
  module V1
    class Posts < Grape::API
      include API::V1::Defaults
      include API::V1::Authentication

现在我有另一个名为Users(users.rb)的Grape资源,在这里我实现了身份验证,注册和注销的方法。(我认为我在这里将苹果和梨混合在一起,应该将登录/注销过程提取到另一个葡萄资源-会话)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
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项目,以这种方式实现?
  • 如何使用更安全的其他方法来实现它?
  • 对于这么长的帖子,我深表歉意,但我希望更多的人遇到同样的问题,这可能有助于我找到更多关于我的问题的答案。


    添加token_authenticable来设计模块(适用于设计版本<= 3.2)

    在user.rb中将:token_authenticatable添加到devise模块列表中,其外观应类似于以下内容:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    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)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    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

    为身份验证令牌添加迁移

    1
    2
    3
    rails g migration add_auth_token_to_users
          invoke  active_record
          create    db/migrate/20141101204628_add_auth_token_to_users.rb

    编辑迁移文件以向用户添加:authentication_token列

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    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

    您需要按以下顺序向API :: Root添加以下代码,以添加基于令牌的身份验证。如果您不知道API :: Root,请阅读使用Grape

    构建RESTful API。

    在下面的示例中,我们基于两种情况对用户进行身份验证–如果用户登录到Web应用程序,则使用同一会话–如果会话不可用,并且传递了身份验证令牌,则根据令牌

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    # 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

    尽管我喜欢@MZaragoza给出的问题和答案,但我认为值得注意的是,token_authentical已从Devise中删除是有原因的!令牌的使用容易受到定时攻击。另请参阅此帖子和Devise的博客,因此我尚未对@MZaragoza的答案进行投票。

    如果您将API与Doorkeeper结合使用,则可以执行类似的操作,但是您无需在User表/模型中检查authentication_token,而是在OauthAccessTokens表中查找令牌,即

    1
    2
    3
    4
    def authenticated
       return true if warden.authenticated?
       params[:access_token] && @user = OauthAccessToken.find_by_token(params[:access_token]).user
    end

    这更安全,因为该令牌(即实际的access_token)仅存在一定的时间。

    请注意,为了能够执行此操作,您必须具有User模型和OauthAccessToken模型,并具有:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    class User < ActiveRecord::Base

       has_many :oauth_access_tokens

    end

    class OauthAccessToken < ActiveRecord::Base
        belongs_to :user, foreign_key: 'resource_owner_id'
    end

    编辑:也请注意,通常您不应在URL中包含access_token:http://tools.ietf.org/html/draft-ietf-oauth-v2-bearer-16#section-2.3