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

knu*_*ton 69 ruby session ruby-on-rails rails-activerecord

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

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

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

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

你能告诉我我错了吗?

gtd*_*gtd 44

我会说你的直觉current_user超出模型是正确的.

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

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

  • 授权模式通过设计打破MVC,任何解决这个问题的方法通常最终会变得更糟,然后简单地破坏该领域的MVC.如果您需要通过当前线程的注册表使用`User.current_user`然后执行它,请尽量不要过度使用它 (4认同)

小智 36

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

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

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

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

  • +10,如果我可以这样说.将请求状态保存到任何单例类方法/变量是一个非常糟糕的想法. (14认同)

Nat*_*ong 29

Controller应该告诉模型实例

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

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

def create
  @item = Item.new
  @item.current_user = current_user # or whatever your controller method is
  ...
end
Run Code Online (Sandbox Code Playgroud)

这假设Item有一个attr_accessorfor current_user.

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

  • 你做了一个有原则的论证,但实际上,这可能会导致很多额外的代码在很多地方设置当前用户.在我看来,有时线程本地的`User.current_user`方法使得其余的代码更短更容易理解,尽管破坏了MVC. (3认同)

Hes*_*sse 13

我全力以赴为瘦小的控制器和胖子模特,我认为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
Run Code Online (Sandbox Code Playgroud)

现在,在公共网站上:

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

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

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
Run Code Online (Sandbox Code Playgroud)

因此添加了登录功能,并将当前用户保存到全局.现在用户模型如下所示:

# 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
Run Code Online (Sandbox Code Playgroud)

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
Run Code Online (Sandbox Code Playgroud)

邮政模型被扩展,以便感觉只有当前登录用户的帖子.多么酷啊!

管理员帖子控制器看起来像这样:

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
Run Code Online (Sandbox Code Playgroud)

对于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
Run Code Online (Sandbox Code Playgroud)

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

如果我错了,请纠正我,但我想:

这一切都与关注点分离有关.即使只清楚只有控制器应该处理auth检查,当然登录的用户也不应该留在控制器层.

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


arm*_*rdj 10

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


Pro*_*ton 10

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

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

这是我解决它的方法

从 Rails 5.2 开始,你现在可以添加一个神奇的Current单例,它就像一个可以从你的应用程序内的任何地方访问的全局存储。

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

# app/models/current.rb
class Current < ActiveSupport::CurrentAttributes
  attribute :user
end
Run Code Online (Sandbox Code Playgroud)

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

# 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
Run Code Online (Sandbox Code Playgroud)

现在你可以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
Run Code Online (Sandbox Code Playgroud)

或者你可以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
Run Code Online (Sandbox Code Playgroud)

或者你可以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 %>
Run Code Online (Sandbox Code Playgroud)

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

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

就这样。

我希望这有帮助


khe*_*lll 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
Run Code Online (Sandbox Code Playgroud)


Vik*_*nic 7

这是 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
Run Code Online (Sandbox Code Playgroud)


小智 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
Run Code Online (Sandbox Code Playgroud)

  • 实际上,这在生产模式中有一个严重的缺陷,在开发模式下你永远找不到.在开发中,每次请求都会重新加载类,因此变量始终为空.然而在生产中,课程仍然存在.jimfish的逻辑几乎是正确的,但是因为'除了c.session [:user] .nil?' 子句,未登录的用户可能会捎带以前的用户权限.如果你真的想要使用它,你应该删除该子句. (5认同)
  • @ dasil003认为这个简单的修改会解决这个问题:User.current_user = c.session [:user] .present??User.find(c.session [:user]):无 (4认同)

ker*_*son 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
Run Code Online (Sandbox Code Playgroud)

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

  • @Mohamad,如果你能详细说明使用`attr_acessor`会不会很好 (2认同)