Rails:在Ruby on Rails中的模型中访问current_user


75

我需要在Ruby on Rails应用程序中实现细粒度的访问控制。单个用户的权限保存在数据库表中,我认为最好让各自的资源(即模型的实例)来决定是否允许某个用户对其进行读写。每次在控制器中做出这个决定肯定不会很干。
问题在于,为此,模型需要访问当前用户,才能调用。但是,模型通常无法访问会话数据。 may_read?(current_user, attribute_name)

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

不过,与Google相邻的结果建议我将对当前用户的引用保存在User类中,我想这是某人想到的,其应用程序不必一次容纳很多用户。;)

长话短说,我感觉到我希望从模型内部访问当前用户(即会话数据)是因为我做错了

你能告诉我我错了吗?


具有讽刺意味的是,在被问到这七年之后,“做错了”是404。:)
Andrew Grimm

1
“我认为最好让各自的资源(即模型的实例)来决定是否允许某个用户对其进行读取或写入。” 该模型可能不是该逻辑的最佳选择。像Pundit和Authority这样的宝石建立了一种模式,该模式为每个模型指定了单独的“策略”或“授权者”类(如果愿意,模型可以共享它们)。这些类提供了轻松与当前用户一起工作的方法。
内森·朗

Answers:


45

我想说您的直觉current_user是正确的。

像Daniel一样,我全都适合瘦控制器和胖模型,但职责也很明确。控制器的目的是管理传入的请求和会话。该模型应该能够回答“用户x可以对这个对象做y吗?”的问题,但是引用该对象是毫无意义的current_user。如果您在控制台中怎么办?如果正在执行cron作业怎么办?

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


谢谢,您的担忧令人信服。我将不得不更深入地研究Rails的某些访问控制插件的功能。
knuton

4
授权模式是通过设计破坏MVC的,因此解决此问题的任何方法通常只会变得更糟,然后简单地破坏该领域的MVC。如果您需要User.current_user通过当前线程的注册表进行使用,请
尽量

1
我只是认为模型不应该知道用户正在访问他们的想法只是在完美方面进行了错误的构想。在方法参数中,没有一个人会建议您在时区中通过模型。那真是愚蠢。但这是同一件事...您有一个时间对象,大多数应用程序都由应用程序控制器设置时区(通常基于登录者),然后模型知道时区。
冷冻了

@nroose检查您的假设,因为大多数时间模型最好由普通UTC时间戳服务并在UI级别进行本地化。有时您的模型确实需要了解与时区相关的知识,但前提是该模型实际上与某个物理位置相关联。同样的情况适用于用户,也许模型有责任确定访问权限,但要对其进行适当建模。要了解为什么对模型使用current_user是一个坏主意,请考虑:不注入用户就不可能在脚本或进程中操作对象吗?
gtd

1
在这里使用政策应该很有意义。检出一个名为pundit的宝石。它提供了一个单独的层(以使您的控制器很瘦)进行授权。
尼拉夫·甘地

35

尽管这个问题已经得到很多人的回答,但我只是想快速增加两分钱。

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

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

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


14
如果可以的话,+ 10。将请求状态保存到任何单例类方法/变量是一个非常糟糕的主意。
molf

33

控制器应告知模型实例

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

因此,如果模型实例需要了解当前用户,则控制器应告知它。

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

假设Item有一个attr_accessorfor current_user

(注意-我首先在另一个问题上发布了此答案但我刚刚注意到该问题是该问题的重复。)


3
您提出了一个有原则的论据,但实际上,这会导致很多额外的代码在很多地方设置当前用户。我认为User.current_user,尽管破坏了MVC,但有时线程局部方法会使其余代码更短,更易于理解。
antinome

惊人!我有几个嵌套的对象,所以我加入了IDS到的意见(隐藏)和attr_accessor中到模型
克里斯- [R

1
@antinome在OP的情况下,也许真正的问题是让模型处理权限检查。可以将单独的“策略”或“授权者”类交给模型和当前用户,然后从那里进行授权。这种方法意味着您在不需要检查的上下文中没有模型检查权限,例如Rake任务。
内森·朗

14

我全力支持瘦控制器和胖模型,而且我认为auth不应违反这一原则。

我已经使用Rails编码一年了,我来自PHP社区。对我来说,将当前用户设置为“请求长全局”是不重要的解决方案。在某些框架中,默认情况下会这样做,例如:

在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,或者您可能是从控制台等访问应用程序的。



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

我对那些根本不知道发问人的基本业务需求的人的回答“只是不这样做”感到惊讶。是的,通常应避免这种情况。但是在某些情况下它既适当又非常有用。我只有一个人。

这是我的解决方案:

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


1
真的,是什么时候?我想不出任何人愿意这样做的单一原因。根本不做。这是荒谬的,容易出错,并且会使您的代码无法使用(如果上下文不是HTTP请求/响应,则是其他内容)。有更好的解决方案,例如将attr_acessor主叫方设置为当前用户。我敢肯定,几乎每个经验丰富的程序员都会告诉您这是一个废话,您不应该这样做。我喜欢您回应的新颖性,但这真是可恶。
Mohamad'2

我的案例是捕获触发金融交易的用户(如果有的话),无论他们到达那里的方式/代码路径如何。
keredson '16

2
@Mohamad,如果您能详细说明使用方法,那就太好了attr_acessor
WM

任何生产代码都不应依赖于此类的运行时调试功能。
Shyam Habarakada

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
  attr_accessor :current_user
end

1
为什么让我感到困惑:您正在设置一个类变量User.current_user。但是我认为那是个坏主意,因为我假设User类的实例相同(如“ Ruby中的类是一流的对象-每个都是Class类的实例。” ruby-doc.org/core /classes/Class.html)将在整个Rails应用程序中使用。如果真是这样,如果有几个用户同时处于活动状态,则此方法显然会导致不良结果。当您在生产环境中使用此功能时,我假设它可以工作,但这意味着User类有多个实例。
knuton

5
实际上,这在生产模式中有一个严重的缺陷,而在开发模式中是找不到的。在开发中,该类在每次请求时都会重新加载,因此该变量始终以空开头。但是,在生产中,班级仍然存在。jimfish的逻辑几乎是正确的,但是由于“除非c.session [:user] .nil?” 子句,未登录的用户可能会背负以前的用户权限。如果您确实要使用此子句,则应删除该子句。
gtd

1
好主意。现在可以跨会话共享用户。安全漏洞。
lzap 2011年

4
@ dasil003认为此简单修改将解决以下问题:User.current_user = c.session [:user] .present??User.find(c.session [:用户]):无
tybro0103

1
您应该使用anaround_filter并将其添加到块的底部ensure User.current_user = nil,而不是cattr,请尝试使用Threadsafe方法 def self.current_user=(user) Thread.current[:current_user] = user end def self.current_user Thread.current[:current_user] end
scanales 2013年

4

我使用的是Declarative Authorization插件,它的功能与您所提到的类似。current_user它使用abefore_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

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


授权(以及身份验证)既属于控制器层(您允许访问操作),也属于模型层(您允许访问那里的资源)
lzap 2011年

2
不要使用类变量!请使用Thread.current ['current_user']或类似的名称。那是线程安全的!不要忘记在请求结束时重置current_user!
reto

1
这可能看起来像一个类变量,但实际上它只是一个设置Thread.current的方法。github.com/stffn/declarative_authorization/blob/master/lib/…–
evanbikes

4

进一步说明扶手椅的答案

在使用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 %>

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

您可以在此处阅读有关此答案的更多信息:当前所有内容

就这样。

我希望这有帮助


0

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


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

这是一个伟大的宝石!希望能帮助到你!

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.