78

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

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

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

长话短说,我觉得我希望从模型中访问当前用户(即会话数据)来自我做错了

你能告诉我我怎么错了吗?

4

13 回答 13

48

我会说你远离current_user模型的直觉是正确的。

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

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

于 2009-10-15T19:35:54.877 回答
37

控制器应该告诉模型实例

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

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

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

这假设Item有一个attr_accessorfor current_user

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

于 2012-11-30T13:57:00.013 回答
36

尽管许多人已经回答了这个问题,但我只想快速添加我的两分钱。

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

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

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

于 2009-11-03T21:52:33.457 回答
15

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

于 2018-04-17T20:12:22.240 回答
14

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

我已经使用 Rails 编码一年了,我来自 PHP 社区。对我来说,将当前用户设置为“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 或者您可能从控制台等访问应用程序......

于 2015-01-27T10:37:21.850 回答
10

为了更清楚地了解armchairdj 的答案

在开发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 %>

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

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

就这样。

我希望这有帮助

于 2020-08-13T07:29:17.663 回答
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
于 2009-10-14T19:30:58.080 回答
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。它可以通过确认预期的返回类型变得更加健壮,并且可能通过确认框架的所有者是一种控制器,但通常只是花花公子。

于 2016-01-05T02:39:57.067 回答
4

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

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

于 2009-10-14T19:34:06.227 回答
4

我在我的一个应用程序中有这个。它只是查找当前控制器 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
于 2009-10-14T21:32:57.507 回答
0

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

于 2015-01-27T09:04:49.333 回答
0

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

它允许您在 Controller 中的每个操作中定义任何您想要的权限,并且由于您在任何控制器上定义了当前功能,您可以发送您需要的任何参数,例如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

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

于 2020-08-06T06:43:51.653 回答
0

这是 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
于 2021-09-24T10:15:49.003 回答