Доступ к current_user из модели в Ruby on Rails
Мне нужно реализовать детальный контроль доступа в приложении Ruby on Rails. Разрешения для отдельных пользователей сохраняются в таблице базы данных, и я подумал, что было бы лучше позволить соответствующему ресурсу (т.е. экземпляру модели) решить, разрешено ли определенному пользователю выполнять чтение или запись в него. Принятие этого решения в контроллере каждый раз определенно не будет СУХОЙ.
Проблема в том, что для этого модели необходим доступ к текущему пользователю, чтобы вызвать что-то вроде may_read
? (current_user
, attribute_name
) Тем не менее, в целом модели не имеют доступа к данным сеанса.
Есть несколько предложений сохранить ссылку на текущего пользователя в текущей теме, например, в этом сообщении в блоге. Это, безусловно, решит проблему.
Соседние результаты Google посоветовали мне сохранить ссылку на текущего пользователя в классе User, хотя, как мне кажется, он был придуман кем-то, чье приложение не должно одновременно обслуживать большое количество пользователей.;)
Короче говоря, у меня возникает ощущение, что мое желание получить доступ к текущему пользователю (то есть данным сеанса) из модели исходит от того, что я сделал это неправильно.
Можете ли вы сказать мне, как я ошибаюсь?
13 ответов
Я бы сказал ваши инстинкты, чтобы сохранить current_user
из модели верны.
Как и Даниэль, я все для худых контролеров и толстых моделей, но есть и четкое разделение обязанностей. Назначение контроллера - управлять входящим запросом и сеансом. Модель должна иметь возможность ответить на вопрос "Может ли пользователь x сделать y с этим объектом?", Но для нее бессмысленно ссылаться на current_user
, Что делать, если вы находитесь в консоли? Что делать, если запущена работа cron?
Во многих случаях с правильным API разрешений в модели, это может быть обработано одной строкой before_filters
которые относятся к нескольким действиям. Однако, если все становится более сложным, вы можете реализовать отдельный слой (возможно, в lib/
), которая заключает в себе более сложную логику авторизации, чтобы предотвратить вздутие вашего контроллера и предотвратить слишком тесную связь вашей модели с циклом веб-запроса / ответа.
Хотя на этот вопрос ответили многие, я просто хотел быстро добавить свои два цента.
Использование подхода #current_user в модели User должно осуществляться с осторожностью из-за безопасности потоков.
Можно использовать метод класса / синглтона в User, если вы не забываете использовать Thread.current в качестве способа хранения или извлечения ваших значений. Но это не так просто, потому что вам также необходимо сбросить Thread.current, чтобы следующий запрос не наследовал разрешения, которые он не должен иметь.
Я хочу подчеркнуть, что если вы сохраняете состояние в переменных класса или синглтона, помните, что вы выбрасываете безопасность потока в окно.
Контроллер должен сообщить экземпляру модели
Работа с базой данных - работа модели. Работа с веб-запросами, включая знание пользователя для текущего запроса, является задачей контроллера.
Следовательно, если экземпляру модели необходимо знать текущего пользователя, контроллер должен сообщить об этом.
def create
@item = Item.new
@item.current_user = current_user # or whatever your controller method is
...
end
Это предполагает, что Item
имеет attr_accessor
за current_user
,
(Примечание. Сначала я опубликовал этот ответ на другой вопрос, но только что заметил, что этот вопрос является его дубликатом.)
Чтобы пролить больше света на ответ armchairdj
Я столкнулся с этой проблемой при работе над приложением на Rails 6.
Вот как я это решил:
Из Rails 5.2 вы теперь можете добавить магию Current
singleton, который действует как глобальный магазин, доступный из любого места внутри вашего приложения.
Сначала определите его в своих моделях:
# 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 %>
Примечание. Вы можете сказать: "Эта функция нарушает принцип разделения проблем!" Да, это так. Не используйте его, если что-то не так.
Вы можете прочитать больше об этом ответе здесь: Текущее все
Вот и все.
надеюсь, это поможет
Древний поток, но стоит отметить, что начиная с Rails 5.2, есть запутанное решение для этого: синглтон текущей модели, описанный здесь: https://evilmartians.com/chronicles/rails-5-2-active-storage-and-beyond
Я в курсе тощих контроллеров и толстых моделей, и я думаю, что аутентификация не должна нарушать этот принцип.
Я уже год программирую на 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.whwhat_method(...)"? Много.
Поправь меня если я не прав но думаю:
Это все о разделении интересов. Даже когда ясно, что только контроллер должен обрабатывать проверки подлинности, ни в коем случае не зарегистрированный в данный момент пользователь не должен оставаться на уровне контроллера.
Единственное, что нужно помнить: НЕ злоупотребляйте этим! Помните, что могут быть работники электронной почты, которые не используют User.current, или вы можете получить доступ к приложению из консоли и т. Д.
Ну, мое предположение вот что current_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
Это вызов 2021 года. Начиная с rails 5.2 есть новый глобальный API, который можно использовать, но используйте его с осторожностью, как указано в документации API:
https://api.rubyonrails.org/classes/ActiveSupport/CurrentAttributes.html
Абстрактный суперкласс, который предоставляет синглтон с изолированными от потока атрибутами, который автоматически сбрасывается до и после каждого запроса. Это позволяет сохранить доступность всех атрибутов запроса для всей системы.
Предупреждение: легко переборщить с глобальным синглтоном, таким как 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
Я всегда поражаюсь ответам "просто не делай этого" от людей, которые ничего не знают об основных бизнес-потребностях опрашивающего. Да, как правило, этого следует избегать. Но есть обстоятельства, когда это уместно и очень полезно. У меня был только один сам.
Вот мое решение:
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
, Если ничего не найдено, возвращается ноль. Это можно сделать более надежным, подтвердив ожидаемый тип возврата, и, возможно, подтвердив, что владелец фрейма является типом контроллера, но обычно работает просто модно.
У меня есть это в моем приложении. Он просто ищет текущий сеанс контроллеров [: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
Я использую плагин декларативной авторизации, и он делает нечто похожее на то, что вы упоминаете с 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
Я не использую возможности модели декларативной авторизации. Я полностью за подход "Skinny Controller - Fat Model", но я чувствую, что авторизация (а также аутентификация) принадлежит к уровню контроллера.
Я чувствую, что текущий пользователь является частью "контекста" вашей модели MVC, думайте о текущем пользователе как о текущем времени, текущем потоке журналирования, текущем уровне отладки, текущей транзакции и т. Д. Вы можете передать все это " модальности "в качестве аргументов в ваши функции. Или вы делаете это доступным через переменные в контексте вне текущего тела функции. Локальный контекст потока является лучшим выбором, чем глобальные переменные или переменные с областями видимости из-за простоты безопасности потока. Как сказал Джош К., опасность для локальных потоков заключается в том, что они должны быть очищены после выполнения задачи, что может сделать для вас среда внедрения зависимостей. MVC - это несколько упрощенная картина реальности приложения, и этим покрывается не все.
Я очень опаздываю на эту вечеринку, но если вам нужен детальный контроль доступа или сложные разрешения, я определенно рекомендую 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
Это отличный самоцвет! Надеюсь, это поможет!