2014-10-29 63 views
19

我很难理解并正确实施用户身份验证在API中。换句话说,我很难理解Grape API与前端框架(如Backbone.js,AngularJS或Ember.js)的集成。葡萄和设计的用户身份验证

我试图摆动所有不同的方法,并阅读了很多关于这个,但谷歌回报我真正不好的资源,在我看来,就像没有真正好的文章在这个主题上 - Rails和用户身份验证与设计和前端框架

我会描述我目前的关键点,希望你能为我的实施提供一些反馈意见,也许可以让我指出正确的方向。

当前实现

我有后台Rails的REST API具有以下的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的葡萄资源:

module API 
    module V1 
    class Posts < Grape::API 
     include API::V1::Defaults 
     include API::V1::Authentication 

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

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实现用户身份验证。我有几个问题,我列在下面。

问题

  1. 你觉得这种实现是很危险的,如果是的话,为什么呢? - 我认为这是因为使用了一个令牌。有没有办法改善这种模式?我也看到了具有到期时间等的单独模型Token的实现。但我认为这几乎就像重新发明轮子,因为为此我可以实现OAuth2。我想有更轻的解决方案。
  2. 为身份验证创建新模块并将其仅包含到需要它的资源中是一种很好的做法?
  3. 你知道关于这个主题的任何好教程 - 实现 Rails + Devise + Grape吗?另外,你是否知道开源的Rails项目有哪些好的 ?
  4. 我该如何使用更安全的不同方法来实现它?

我很抱歉这么长的帖子,但我希望更多的人有同样的问题,这可能会帮助我找到更多的答案在我的问题。

+0

真的,没有人在做同样的事情?或者其阅读时间太长?我的天啊.... – 2014-10-29 18:01:20

回答

22

添加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 

添加迁移authentiction令牌

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

生成现有用户

我们需要调用保存令牌每个确保身份验证的用户实例令牌存在于每个用户。使用身份验证令牌

您需要将下面的代码添加到API ::根

User.all.each(&:save)

安全葡萄API中,为了增加基于令牌的认证。如果您不知道API :: Root,请阅读Building RESTful API using Grape

在下面的示例中,我们基于两种方案验证用户 - 如果用户登录到Web应用程序,则使用同一会话 - 如果会话不可用和身份验证令牌传递,然后找到用户基于令牌

# 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 
1

虽然我很喜欢这个问题,并通过@MZaragoza给出我认为这是值得一提的是token_authentical已经从设计去除是有原因的答案!使用令牌容易受到时间攻击。另见this postDevise's blog因此,我还没有upvoted @ MZaragoza的答案。

如果您使用的API与看门的组合,你可以做同样的事情,但不是检查用户表/模型,你看在OauthAccessTokens表中的令牌authentication_token,即

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 

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