从Ruby on Rails中的模型中访问current_user

时间:2009-10-14 18:45:09

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

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

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

相邻的Google搜索结果建议我在User类中保存对当前用户的引用,我想这应该是那些应用程序不必同时容纳大量用户的人所想到的。 ;)

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

你能告诉我我错了吗?

12 个答案:

答案 0 :(得分:40)

我说你保持current_user不在模型范围内的直觉是正确的。

像丹尼尔一样,我只是为了瘦弱的控制者和胖子模特,但也有明确的责任分工。控制器的目的是管理传入的请求和会话。该模型应该能够回答“用户x可以对这个对象做什么吗?”的问题,但它引用current_user是没有意义的。如果你在控制台怎么办?如果它是一个cron作业怎么办?

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

答案 1 :(得分:35)

尽管很多人都回答了这个问题,但我想快速加入我的两分钱。

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

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

我想说的是,如果你在类或单例变量中存储状态,请记住你正在抛出窗口中的线程安全。

答案 2 :(得分:29)

Controller应该告诉模型实例

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

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

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

这假设Item attr_accessorcurrent_user

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

答案 3 :(得分:13)

我全身心投入到瘦身的控制器中。胖模特,我认为auth不应该违反这个原则。

我已经使用Rails编写了一年的编码,而且我来自PHP社区。对我来说,将当前用户设置为“请求全局”是一个简单的解决方案。默认情况下,这在某些框架中完成,例如:

在Yii中,您可以通过调用Yii :: $ app-> user->身份来访问当前用户。见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

那很干净&amp;简单。但是,在管理员网站上,还需要更多内容。这是所有管理员控制器的基本实现:

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

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

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

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

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

关注的是分离问题。即使很清楚只有控制器应该处理auth检查,但当前登录的用户绝不应该留在控制器层中。

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

答案 4 :(得分:8)

我的猜测是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

答案 5 :(得分:8)

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

答案 6 :(得分:5)

我在我的应用程序中有这个。它只查找当前控制器会话[: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
  cattr_accessor :current_user
end

答案 7 :(得分:5)

我总是惊讶于&#34;只是不做那个&#34;那些对提问者的基本业务需求一无所知的人的回答。是的,通常应该避免这种情况。但在某些情况下,它既适合又非常有用。我自己就有一个。

这是我的解决方案:

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。通过确认预期的返回类型可以使其更加健壮,并且可能通过确认框架的所有者是一种控制器,但通常只是花花公子。

答案 8 :(得分:4)

我正在使用声明授权插件,它的功能与您在current_user中提到的类似。它使用before_filter拉出current_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

虽然我没有使用声明授权的模型功能。我全都是为了“瘦控制器 - 胖模型”的方法,但我的感觉是授权(以及身份验证)属于控制器层。

答案 9 :(得分:3)

进一步了解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

仅此而已。

我希望这会有所帮助

答案 10 :(得分:0)

我的感觉是当前用户是&#34; context&#34;的一部分。对于您的MVC模型,请考虑当前用户的当前时间,当前日志记录流,当前调试级别,当前事务等。您可以通过所有这些&#34;模式&#34;作为你的函数的参数。或者,您可以通过当前函数体外部的上下文中的变量使其可用。由于最简单的线程安全性,线程本地上下文比全局或其他范围的变量更好。正如Josh K所说,线程本地的危险在于它们必须在任务之后被清除,这是依赖注入框架可以为你做的事情。 MVC是一个有点简化的应用程序现实图片,并不是所有内容都被它覆盖。

答案 11 :(得分:0)

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

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

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

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