Rails: 在 RubyonRails 中从模型中访问 current_user

我需要在 RubyonRails 应用程序中实现细粒度的访问控制。个人用户的权限保存在数据库表中,我认为最好让各自的资源(即模型的实例)决定是否允许某个用户读取或写入它。每次在控制器中做这样的决定当然不会很枯燥。
问题是,为了做到这一点,模型需要访问当前用户,调用类似 may_read的东西?(current_userattribute_name) .但是,模型通常不能访问会话数据。译注:

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

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

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

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

82473 次浏览

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

我使用的是 Declarative Authorization 插件,它的功能和你在 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

不过,我没有使用声明式授权的模型特性。我完全支持“ Skinny Controller-Fat Model”方法,但我的感觉是授权(以及身份验证)属于控制器层。

我的申请里有这个。它只是查找当前控制器会话[ : 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

我认为你不让 current_user参与模型的直觉是正确的。

和丹尼尔一样,我完全支持瘦控制器和胖模特,但也有明确的责任分工。控制器的作用是管理传入的请求和会话。模型应该能够回答这样的问题: “用户 x 可以对这个对象进行 y 操作吗?”但是引用 current_user是没有意义的。如果您在控制台中呢?如果这是一个 cron 作业运行?

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

虽然这个问题已经得到了很多人的回答,但我还是想快速补充一下我的看法。

出于线程安全的考虑,在 User 模型上使用 # current _ User 方法应该谨慎实现。

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

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

Controller 应该告诉模型实例

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

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

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

这假设 Item对于 current_user有一个 attr_accessor

(注意-我首先在 还有一个问题,上发布了这个答案,但是我刚刚注意到这个问题是这个问题的复制品。)

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

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

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

在 Yii,您可以通过调用 Yii: : $app-> user-> Identity 来访问当前用户

在 Lavavel,你也可以通过调用 Auth: : user ()来做同样的事情

为什么我只能从控制器传递当前用户? ?

假设我们正在创建一个支持多用户的简单博客应用程序。我们正在创建两个公共网站(匿名用户可以阅读和评论博客帖子)和管理网站(用户登录,他们有 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

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

恕我直言: 这是一个强大而安全的方法,可以确保用户只能访问/修改他们的数据,所有在一起。考虑一下,如果每个查询都需要以“ current _ user”开头,那么开发人员需要编写多少测试代码。Whatever _ method (...)”?非常喜欢。

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

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

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

我总是惊讶于那些对提问者的潜在商业需求一无所知的人的“不要那样做”的回答。是的,一般来说这是应该避免的。但在某些情况下,这种方法既合适又非常有用。我自己也有一个。

我的解决办法是:

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的帧。如果找不到,则返回空。通过确认预期的返回类型,可能通过确认帧的所有者是一种控制器,它可以变得更加健壮,但通常工作得很好。

古老的线程,但是值得注意的是,从 Rails 5.2开始,有一个固定的解决方案: Current model singleton,这里介绍: https://evilmartians.com/chronicles/rails-5-2-active-storage-and-beyond#current-everything

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

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

这是一个伟大的宝石! 希望它的帮助!

为了更清楚地了解 扶手椅的回答

我在处理 铁路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 %>

注意 : 你可能会说: “这个特性打破了关注点分离原则!”是的,没错。如果感觉不对就别用。

你可以在这里阅读更多关于这个答案的内容: 流通一切

仅此而已。

希望这个能帮上忙

这里是2021号。因为 Rails 5.2有一个新的全局 API 可以使用,但是要谨慎使用,正如 API 文档中所说:

Https://api.rubyonrails.org/classes/activesupport/currentattributes.html

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

提醒一句: 像 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