我需要在Ruby on Rails应用程序中实现细粒度的访问控制。各个用户的权限保存在数据库表中,我认为最好让相应的资源(即模型的实例)决定是否允许某个用户读取或写入某个用户。每次在控制器中做出这个决定肯定不会很干
问题是,为了做到这一点,模型需要访问当前用户,调用
之类的东西。但是,模型通常无法访问会话数据。 may_read
?(current_user
, attribute_name
)
有一些建议要保存对当前线程中当前用户的引用,例如在this blog post。这肯定会解决问题。
相邻的Google搜索结果建议我在User类中保存对当前用户的引用,我想这应该是那些应用程序不必同时容纳大量用户的人所想到的。 ;)
长话短说,我感觉我希望从模型中访问当前用户(即会话数据)来自我doing it wrong。
你能告诉我我错了吗?
答案 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)
使用数据库是模型的工作。处理Web请求(包括了解当前请求的用户)是控制器的工作。
因此,如果模型实例需要知道当前用户,控制器应该告诉它。
def create
@item = Item.new
@item.current_user = current_user # or whatever your controller method is
...
end
这假设Item
attr_accessor
有current_user
。
(注意 - 我首先在another question,上发布了这个答案,但我刚刚注意到这个问题与此问题重复。)
答案 3 :(得分:13)
我全身心投入到瘦身的控制器中。胖模特,我认为auth不应该违反这个原则。
我已经使用Rails编写了一年的编码,而且我来自PHP社区。 strong>对我来说,将当前用户设置为“请求全局”是一个简单的解决方案。默认情况下,这在某些框架中完成,例如:
在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
这是一颗伟大的宝石!希望对您有帮助!