【问题标题】:Rails: Access to current_user from within a model in Ruby on RailsRails:从 Ruby on Rails 的模型中访问 c​​urrent_user
【发布时间】:2010-12-06 19:05:20
【问题描述】:

我需要在 Ruby on Rails 应用程序中实现细粒度的访问控制。单个用户的权限保存在数据库表中,我认为最好让相应的资源(即模型的实例)决定是否允许某个用户读取或写入它。每次在控制器中做出这个决定肯定不会很干。
问题是,为了做到这一点,模型需要访问当前用户,才能调用may_read?(current_user, attribute_name) 之类的东西。但是,模型通常无法访问会话数据。

有很多建议可以在当前线程中保存对当前用户的引用,例如在this blog post。这肯定会解决问题。

不过,相邻的 Google 结果建议我在 User 类中保存对当前用户的引用,我猜这是某个应用程序不必同时容纳大量用户的人想出来的。 ;)

长话短说,我感觉我希望从模型中访问当前用户(即会话数据)来自我doing it wrong

你能告诉我我哪里错了吗?

【问题讨论】:

  • 具有讽刺意味的是,在被问到这个问题七年后,“做错了”是 404ing。 :)
  • “我认为最好让相应的资源(即模型的实例)决定是否允许某个用户对其进行读取或写入。”该模型可能不是该逻辑的最佳位置。像 Pundit 和 Authority 这样的 Gem 建立了一种模式,即为每个模型指定一个单独的“策略”或“授权者”类(如果你愿意,模型可以共享它们)。这些类提供了与当前用户轻松合作的方法。

标签: ruby-on-rails ruby session rails-activerecord


【解决方案1】:

我会说你将current_user 排除在模型之外的直觉是正确的。

和 Daniel 一样,我完全支持瘦控制器和胖模型,但也有明确的职责分工。控制器的目的是管理传入的请求和会话。该模型应该能够回答“用户 x 可以对这个对象做 y 吗?”这个问题,但它引用 current_user 是荒谬的。如果您在控制台中怎么办?如果它是一个 cron 作业正在运行怎么办?

在模型中具有正确权限 API 的许多情况下,这可以通过适用于多个操作的单行 before_filters 来处理。但是,如果事情变得越来越复杂,您可能需要实现一个单独的层(可能在 lib/ 中),它封装了更复杂的授权逻辑,以防止您的控制器变得臃肿,并防止您的模型变得过于紧密地耦合到 Web 请求/响应周期。

【讨论】:

  • 谢谢,您的担忧令人信服。我将不得不更深入地研究 Rails 的一些访问控制插件是如何运行的。
  • 授权模式在设计上破坏了 MVC,任何解决这个问题的方法通常都会比在该区域破坏 MVC 更糟糕。如果你需要通过当前线程的注册表使用User.current_user,那就去做吧,尽量不要过度使用它
  • 我只是认为模型不应该知道什么用户正在访问它们的想法只是一个考虑不周的完美努力。这里没有一个人会建议您将时区传递给方法参数中的模型。那将是愚蠢的。但那是一回事......你有一个时间对象,大多数应用程序都有应用程序控制器设置时区,通常基于谁登录,然后模型知道时区。
  • @nroose 检查您的假设,因为大多数时间模型最好使用纯 UTC 时间戳,并在 UI 级别进行本地化。有时您的模型确实需要知道与时区相关的事情,但前提是模型实际上与物理位置相关联。同样的事情也适用于用户,也许模型有责任确定访问权限,但要适当地建模。要了解为什么模型上的 current_user 是一个坏主意,请考虑:如果不注入用户就不可能在脚本或进程中操作对象吗?
  • 在这里使用策略应该是有意义的。结帐一个名为 pundit 的宝石。它为授权提供了一个单独的层(以使您的控制器保持精简)。
【解决方案2】:

虽然很多人都回答了这个问题,但我只是想快速添加我的两分钱。

由于线程安全,在用户模型上使用#current_user 方法时应谨慎执行。

如果您记得使用 Thread.current 作为一种方式或存储和检索您的值,则可以在 User 上使用类/单例方法。但这并不那么容易,因为您还必须重置 Thread.current 以便下一个请求不会继承不应继承的权限。

我想说的是,如果您将状态存储在类或单例变量中,请记住您将线程安全抛到了窗外。

【讨论】:

  • +10 如果可以的话。将请求状态保存到任何单例类方法/变量是一个非常糟糕的主意。
【解决方案3】:

Controller 应该告诉模型实例

使用数据库是模型的工作。处理网络请求,包括了解当前请求的用户,是控制器的工作。

因此,如果模型实例需要知道当前用户,控制器应该告诉它。

def create
  @item = Item.new
  @item.current_user = current_user # or whatever your controller method is
  ...
end

这假设Item 有一个attr_accessor 对应current_user

(注意 - 我首先在 another question, 上发布了这个答案,但我刚刚注意到这个问题与这个问题重复。)

【讨论】:

  • 您提出了一个原则性的论点,但实际上,这可能会导致在很多地方设置当前用户的大量额外代码。在我看来,有时线程局部的User.current_user 方法会使其余代码更短且更易于理解,尽管它破坏了 MVC。
  • 太棒了!我有几个嵌套对象,所以我将 id 添加到视图(隐藏)中并将 attr_accessor 添加到模型中
  • @antinome 在 OP 的情况下,真正的问题可能是让模型处理权限检查。可以将单独的“策略”或“授权者”类交给模型和当前用户并从那里授权。这种方法意味着您在不希望检查它们的上下文中没有模型检查权限,例如 Rake 任务。
【解决方案4】:

我完全支持瘦控制器和胖模型,我认为 auth 不应该打破这个原则。

我已经使用 Rails 编码一年了,我来自 PHP 社区。​​strong> 对我来说,将当前用户设置为“request-long global”是一个简单的解决方案。这在某些框架中是默认完成的,例如:

在 Yii 中,你可以通过调用 Yii::$app->user->identity 来访问当前用户。见http://www.yiiframework.com/doc-2.0/guide-rest-authentication.html

在 Lavavel 中,你也可以通过调用 Auth::user() 来做同样的事情。见http://laravel.com/docs/4.2/security

如果我可以从控制器传递当前用户,为什么?

假设我们正在创建一个支持多用户的简单博客应用程序。我们正在创建公共站点(匿名用户可以阅读和评论博客文章)和管理站点(用户已登录并且他们可以对数据库中的内容进行 CRUD 访问。)

这里是“标准的 AR”:

class Post < ActiveRecord::Base
  has_many :comments
  belongs_to :author, class_name: 'User', primary_key: author_id
end

class User < ActiveRecord::Base
  has_many: :posts
end

class Comment < ActiveRecord::Base
  belongs_to :post
end

现在,在公共网站上:

class PostsController < ActionController::Base
  def index
    # Nothing special here, show latest posts on index page.
    @posts = Post.includes(:comments).latest(10)
  end
end

那是干净和简单的。然而,在管理站点上,还需要更多东西。这是所有管理控制器的基本实现:

class Admin::BaseController < ActionController::Base
  before_action: :auth, :set_current_user
  after_action: :unset_current_user

  private

    def auth
      # The actual auth is missing for brievery
      @user = login_or_redirect
    end

    def set_current_user
      # User.current needs to use Thread.current!
      User.current = @user
    end

    def unset_current_user
      # User.current needs to use Thread.current!
      User.current = nil
    end
end

因此添加了登录功能并将当前用户保存到全局。现在用户模型看起来像这样:

# Let's extend the common User model to include current user method.
class Admin::User < User
  def self.current=(user)
    Thread.current[:current_user] = user
  end

  def self.current
    Thread.current[:current_user]
  end
end

User.current 现在是线程安全的

让我们扩展其他模型以利用这一点:

class Admin::Post < Post
  before_save: :assign_author

  def default_scope
    where(author: User.current)
  end

  def assign_author
    self.author = User.current
  end
end

帖子模型已扩展,因此感觉好像只有当前登录的用户帖子。 这太酷了!

管理员后控制器可能如下所示:

class Admin::PostsController < Admin::BaseController
  def index
    # Shows all posts (for the current user, of course!)
    @posts = Post.all
  end

  def new
    # Finds the post by id (if it belongs to the current user, of course!)
    @post = Post.find_by_id(params[:id])

    # Updates & saves the new post (for the current user, of course!)
    @post.attributes = params.require(:post).permit()
    if @post.save
      # ...
    else
      # ...
    end
  end
end

对于 Comment 模型,管理员版本可能如下所示:

class Admin::Comment < Comment
  validate: :check_posts_author

  private

    def check_posts_author
      unless post.author == User.current
        errors.add(:blog, 'Blog must be yours!')
      end
    end
end

恕我直言:这是一种强大且安全的方式,可确保用户一次性访问/修改他们的数据。想想如果每个查询都需要以“current_user.posts.whatever_method(...)”开头,那么开发人员需要编写多少测试代码?很多。

如果我错了,请纠正我,但我认为:

这一切都与关注点分离有关。即使很明显只有控制器应该处理身份验证检查,但当前登录的用户绝不应该留在控制器层。

唯一要记住的是:不要过度使用它!请记住,可能存在未使用 User.current 的电子邮件工作者,或者您可能从控制台等访问应用程序...

【讨论】:

    【解决方案5】:

    古老的线程,但值得注意的是,从 Rails 5.2 开始,有一个内置的解决方案:当前模型单例,在此介绍:https://evilmartians.com/chronicles/rails-5-2-active-storage-and-beyond#current-everything

    【讨论】:

      【解决方案6】:

      更多地了解armchairdj's answer

      我在开发 Rails 6 应用程序时遇到了这个挑战。

      我是这样解决的

      从 Rails 5.2 开始,您现在可以添加一个神奇的 Current 单例,它就像一个全局商店,可从您的应用程序内的任何位置访问。

      首先,在您的模型中定义它:

      # app/models/current.rb
      class Current < ActiveSupport::CurrentAttributes
        attribute :user
      end
      

      接下来,将用户设置在您的控制器中的某个位置,使其可以在模型、作业、邮件或任何您想要的地方访问:

      # app/controllers/application_controller.rb
      class ApplicationController < ActionController::Base
        before_action :set_current_user
      
        private
      
        def set_current_user
          Current.user = current_user
        end
      end
      

      现在您可以在您的模型中调用Current.user

      # app/models/post.rb
      class Post < ApplicationRecord
        # You don't have to specify the user when creating a post,
        # the current one would be used by default
        belongs_to :user, default: -> { Current.user }
      end
      

      或者您可以在表单中致电Current.user

      # app/forms/application_registration.rb
      class ApplicationRegistration
        include ActiveModel::Model
      
        attr_accessor :email, :user_id, :first_name, :last_name, :phone,
                          
        def save
          ActiveRecord::Base.transaction do
            return false unless valid?
      
            # User.create!(email: email)
            PersonalInfo.create!(user_id: Current.user.id, first_name: first_name,
                                last_name: last_name, phone: phone)
      
            true
          end
        end
      end
      

      或者您可以在您的视图中拨打Current.user

      # app/views/application_registrations/_form.html.erb
      <%= form_for @application_registration do |form| %>
        <div class="field">
          <%= form.label :email %>
          <%= form.text_field :email, value: Current.user.email %>
        </div>
      
        <div class="field">
          <%= form.label :first_name %>
          <%= form.text_field :first_name, value: Current.user.personal_info.first_name %>
        </div>
      
        <div class="actions">
          <%= form.submit %>
        </div>
      <% end %>
      

      注意:您可能会说:“此功能违反了关注点分离原则!”是的,它确实。如果感觉不对,请勿使用它。

      您可以在此处阅读有关此答案的更多信息:Current everything

      就是这样。

      我希望这会有所帮助

      【讨论】:

        【解决方案7】:

        我的猜测是 current_user 最终是一个 User 实例,那么,为什么不将这些权限添加到 User 模型或数据模型中,您希望拥有应用或查询的权限?

        我的猜测是你需要以某种方式重组你的模型并将当前用户作为参数传递,就像这样做:

        class Node < ActiveRecord
          belongs_to :user
        
          def authorized?(user)
            user && ( user.admin? or self.user_id == user.id )
          end
        end
        
        # inside controllers or helpers
        node.authorized? current_user
        

        【讨论】:

          【解决方案8】:

          我总是对那些对提问者的潜在业务需求一无所知的人的回答感到惊讶。是的,通常应该避免这种情况。但在某些情况下,它既合适又非常有用。我自己也有一个。

          这是我的解决方案:

          def find_current_user
            (1..Kernel.caller.length).each do |n|
              RubyVM::DebugInspector.open do |i|
                current_user = eval "current_user rescue nil", i.frame_binding(n)
                return current_user unless current_user.nil?
              end
            end
            return nil
          end
          

          这会向后遍历堆栈,寻找响应current_user 的帧。如果没有找到,则返回 nil。它可以通过确认预期的返回类型变得更加健壮,并且可能通过确认框架的所有者是一种控制器,但通常只是花花公子。

          【讨论】:

          • 真的,像什么时候?我想不出任何人想要这样做的单一原因。干脆不要这样做。这是荒谬的,容易出错,并且会使您的代码无法使用(如果上下文不是 HTTP 请求/响应)。有更好的解决方案,例如从调用者为当前用户设置attr_acessor。我敢肯定,几乎每个经验丰富的程序员都会告诉你这是一个糟糕的想法,你不应该这样做。我喜欢你的回复的新颖性,但它是可憎的。
          • 我的案例是捕获触发金融交易的用户(如果有的话),无论他们如何/通过哪个代码路径到达那里。
          • @Mohamad,如果您能详细说明如何使用 attr_acessor ,那就太好了
          • 生产代码不应依赖于这样的运行时调试功能。
          【解决方案9】:

          我的应用程序中有这个。它只是查找当前控制器 session[:user] 并将其设置为 User.current_user 类变量。这段代码在生产中工作,非常简单。我希望我可以说我想出了它,但我相信我是从其他地方的互联网天才那里借来的。

          class ApplicationController < ActionController::Base
             before_filter do |c|
               User.current_user = User.find(c.session[:user]) unless c.session[:user].nil?  
             end
          end
          
          class User < ActiveRecord::Base
            attr_accessor :current_user
          end
          

          【讨论】:

          • 为什么我很困惑:您正在设置一个类变量,User.current_user。我认为这不是一个好主意,因为我假设将使用 User 类的相同实例(如“Ruby 中的类是一等对象——每个都是类 Class 的一个实例。”ruby-doc.org/core/classes/Class.html)在整个 Rails 应用程序中。如果是这种情况,如果多个用户同时处于活动状态,这种方法显然会导致不需要的结果。当您在生产中使用它时,我假设它可以工作,但这意味着 User 类有多个实例。
          • 实际上这在生产模式中有一个严重的缺陷,你在开发模式中永远找不到。在开发中,每个请求都会重新加载该类,因此该变量将始终为空。但是在生产中,课程仍然存在。 jimfish 的逻辑几乎是正确的,但因为'除非 c.session[:user].nil?'子句,未登录的用户可能会捎带以前的用户权限。如果你真的想使用这个,你应该删除那个子句。
          • 非常糟糕的主意。用户现在可以跨会话共享。安全漏洞。
          • @dasil003 认为这个简单的修改可以解决这个问题:User.current_user = c.session[:user].present? ? User.find(c.session[:user]) : nil
          • 你应该使用around_filter并将其添加到块的底部ensure User.current_user = nil,而不是cattr,尝试使用线程安全方法def self.current_user=(user) Thread.current[:current_user] = user enddef self.current_user Thread.current[:current_user] end
          【解决方案10】:

          我正在使用声明式授权插件,它的功能类似于您在current_user 中提到的内容。它使用before_filtercurrent_user 拉出并将其存储在模型层可以访问的位置。看起来像这样:

          # set_current_user sets the global current user for this request.  This
          # is used by model security that does not have access to the
          # controller#current_user method.  It is called as a before_filter.
          def set_current_user
            Authorization.current_user = current_user
          end
          

          不过,我没有使用声明式授权的模型功能。我完全赞成“瘦控制器 - 胖模型”方法,但我的感觉是授权(以及身份验证)属于控制器层。

          【讨论】:

          • 授权(以及身份验证)既属于控制器层(您允许访问操作的地方)也属于模型(您允许访问那里的资源)
          • 不要使用类变量!改用 Thread.current['current_user'] 或类似的东西。那是线程安全的!不要忘记在请求结束时重置 current_user!
          • 这可能看起来像一个类变量,但它实际上只是一个设置 Thread.current 的方法。 github.com/stffn/declarative_authorization/blob/master/lib/…
          【解决方案11】:

          我的感觉是当前用户是 MVC 模型“上下文”的一部分,想想当前用户,比如当前时间、当前日志流、当前调试级别、当前事务等。你可以通过所有这些“模态”作为你的函数的参数。或者,您可以通过当前函数体之外的上下文中的变量使其可用。由于最简单的线程安全性,线程局部上下文是比全局变量或其他范围变量更好的选择。正如 Josh K 所说,线程局部变量的危险在于它们必须在任务完成后被清除,这是依赖注入框架可以为您做的事情。 MVC 是应用程序现实的某种简化图,它并未涵盖所有内容。

          【讨论】:

            【解决方案12】:

            我参加这个聚会已经很晚了,但是如果您需要细粒度的访问控制或具有复杂的权限,我绝对会推荐 Cancancan Gem: https://github.com/CanCanCommunity/cancancan

            它允许您在控制器中的每个操作中定义任何您想要的权限,并且由于您定义了任何控制器上的当前功能,您可以发送您需要的任何参数,例如current_user。您可以在ApplicationController 中定义一个通用的current_ability 方法并自动设置:

            class ApplicationController < ActionController::Base
              protect_from_forgery with: :null_session
              def current_ability
                klass = Object.const_defined?('MODEL_CLASS_NAME') ? MODEL_CLASS_NAME : controller_name.classify
                @current_ability ||= "#{klass.to_s}Abilities".constantize.new(current_user, request)
              end
            end
            

            这样,您可以将UserAbilities 类链接到您的 UserController,将 PostAbilities 链接到您的 PostController 等等。然后在里面定义复杂的规则:

            class UserAbilities
              include CanCan::Ability
            
              def initialize(user, request)
                if user
                  if user.admin?
                    can :manage, User
                  else
                    # Allow operations for logged in users.
                    can :show, User, id: user.id
                    can :index, User if user
                  end
                end
              end
            end
            

            这是一颗伟大的宝石!希望有帮助!

            【讨论】:

              【解决方案13】:

              这是 2021 年的召唤。从 Rails 5.2 开始,可以使用新的全局 API,但请谨慎使用,如 API 文档中所述:

              https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html

              提供线程隔离属性单例的抽象超类,在每个请求之前和之后自动重置。这使您可以轻松地为整个系统提供所有每个请求的属性。

              请注意:很容易过度使用像 Current 这样的全局单例,从而导致模型混乱。 Current 只能用于少数顶级全局变量,例如帐户、用户和请求详细信息。卡在 Current 中的属性应该被或多或少地用于所有请求的所有操作。如果你开始在其中粘贴特定于控制器的属性,你会造成混乱。

              # app/models/current.rb
              class Current < ActiveSupport::CurrentAttributes
                attribute :user
              end
              
              # app/controllers/application_controller.rb
              class ApplicationController < ActionController::Base
                before_action :set_current_user
              
                private
              
                def set_current_user
                  Current.user = current_user
                end
              end
              
              # and now in your model
              # app/models/post.rb
              class Post < ApplicationRecord
                # You don't have to specify the user when creating a post,
                # the current one would be used by default
                belongs_to :user, default: -> { Current.user }
              end
              

              【讨论】:

              • 这是大量的样板文件。如果我必须自己编写这么多代码,为什么还要为这个功能使用框架?
              • 这是否是很多样板,这是个人意见。这是一个包含三行代码的文件(实际上是class 内的一行)和另外几行您实际上必须设置该变量的行。总而言之,我认为这只是几行代码。我想知道这个功能在 Javascript 空间中需要多少个 npm 包? :) 这是一个全局的,每个请求的变量。您只需编写这么少的代码即可获得该功能,真是太疯狂了。
              • 最后,我只是将current_user作为相关模型函数的参数,并将其传递到调用该函数的位置。
              • 这可能是最好的解决方案,至少从“最佳实践”的角度来看是这样。 ?
              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2010-12-02
              • 2011-01-26
              • 2019-01-31
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多