【问题标题】:User Authentication with Grape and Devise使用 Grape 和 Devise 进行用户身份验证
【发布时间】:2014-12-24 18:45:06
【问题描述】:

我很难理解,也很难在 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,这些 API 受模块 @987654329 保护@。

我觉得这个解决方案不好,但我真的在寻找更简单的方法如何通过 API 实现用户身份验证。我有几个问题,我在下面列出。

问题

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

我为这么长的帖子道歉,但我希望更多的人有同样的问题,它可能会帮助我找到更多关于我的问题的答案。

【问题讨论】:

  • 真的,没有人在做同样的事情吗?还是阅读时间太长?天哪……

标签: ruby-on-rails api ruby-on-rails-4 devise grape


【解决方案1】:

添加 token_authenticable 以设计模块(适用于设计版本

在 user.rb 中将 :token_authenticable 添加到设计模块列表中,它应该如下所示:

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(&amp;:save)

使用身份验证令牌保护 Grape API

您需要将以下代码添加到 API::Root 以添加基于令牌的身份验证。如果你不知道 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

【讨论】:

    【解决方案2】:

    虽然我喜欢@MZaragoza 给出的问题和答案,但我认为值得注意的是,token_authentical 已从 Devise 中删除是有原因的!令牌的使用容易受到时间攻击。另请参阅 this postDevise's blog 因此我没有投票赞成 @MZaragoza 的回答。

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

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

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

    注意,为了能够做到这一点,您必须有一个 User 模型和 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:https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-bearer-16#section-2.3

    【讨论】:

    • 您还应该按未撤销的 oauth 访问令牌进行过滤。这种方法也适用于过期的令牌
    【解决方案3】:

    正如@PSR 所指出的,简单的基于令牌的身份验证并不安全。正确的解决方案将使用刷新令牌和访问令牌,如所述,例如在The Ultimate Guide to handling JWTs on frontend clients

    然而,现在我们可以使用 SameSiteHttpOnly cookie,这使得 web 应用的基于会话的身份验证安全且简单。

    config/initializers/session_store.rb

    # Use safe SameSite cookies. HttpOnly is already the default.
    Rails.application.config.session_store :cookie_store, same_site: :strict
    

    在 Grape API 库中

    # Enable session middleware for auth: https://stackoverflow.com/a/35428068/2771889
    use ActionDispatch::Session::CookieStore
    helpers do
      def session
        env['rack.session']
      end
    end
    

    API

    helpers Devise::Controllers::SignInOut
    
    resource :users do
      params do
        requires :user, type: Hash do
          requires :email
          requires :password
        end
      end
      post :login do
        user = User.find_by(email: params[:user][:email])
        if user&.valid_password?(params[:user][:password])
          sign_in(user)
          { user_id: user.id }
        else
          error!('Invalid email/password combination', 401)
        end
      end
    end
    

    两个警告:

    • 这会将会话层添加到 API,这将导致 Grape 默认情况下可以避免的一些开销。但是,安全刷新令牌方法也将使用 cookie,因此我看不到解决此问题的方法。此外,这不会影响您的应用的成败。
    • 检查浏览器对SameSite cookie 的支持:https://caniuse.com/same-site-cookie-attribute

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-01-04
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多