Ich muss eine differenzierte Zugriffssteuerung in einer Ruby on Rails-App implementieren. Die Berechtigungen für einzelne Benutzer werden in einer Datenbanktabelle gespeichert, und ich dachte, es wäre am besten, die jeweilige Ressource (dh die Instanz eines Modells) entscheiden zu lassen, ob ein bestimmter Benutzer daraus lesen oder darauf schreiben darf. Diese Entscheidung jedes Mal im Controller zu treffen, wäre sicherlich nicht sehr trocken.
Das Problem ist, dass das Modell dazu Zugriff auf den aktuellen Benutzer benötigt, um so etwas aufzurufen . Modelle haben im Allgemeinen jedoch keinen Zugriff auf Sitzungsdaten. may_read
?(current_user
, attribute_name
)
Es gibt einige Vorschläge, um einen Verweis auf den aktuellen Benutzer im aktuellen Thread zu speichern, z. B. in diesem Blog-Beitrag . Dies würde das Problem sicherlich lösen.
In den benachbarten Google-Ergebnissen wurde mir jedoch empfohlen, einen Verweis auf den aktuellen Benutzer in der Benutzerklasse zu speichern, der vermutlich von jemandem entwickelt wurde, dessen Anwendung nicht viele Benutzer gleichzeitig aufnehmen muss. ;)
Kurz gesagt, ich habe das Gefühl, dass mein Wunsch, aus einem Modell heraus auf den aktuellen Benutzer (dh Sitzungsdaten) zuzugreifen, darauf zurückzuführen ist, dass ich es falsch gemacht habe .
Kannst du mir sagen, wie ich falsch liege?
Antworten:
Ich würde sagen, Ihre Instinkte, sich
current_user
aus dem Modell herauszuhalten, sind korrekt.Wie Daniel bin ich alles für Skinny Controller und Fat Models, aber es gibt auch eine klare Aufteilung der Verantwortlichkeiten. Der Zweck des Controllers besteht darin, die eingehende Anforderung und Sitzung zu verwalten. Das Modell sollte in der Lage sein, die Frage "Kann Benutzer x y mit diesem Objekt tun?" Zu beantworten, aber es ist unsinnig, auf das zu verweisen
current_user
. Was ist, wenn Sie in der Konsole sind? Was ist, wenn ein Cron-Job läuft?In vielen Fällen kann dies mit der richtigen Berechtigungs-API im Modell einzeilig behandelt werden
before_filters
, die für mehrere Aktionen gilt. Wenn die Dinge jedoch komplexer werden, möchten Sie möglicherweise eine separate Ebene (möglicherweise inlib/
) implementieren , die die komplexere Autorisierungslogik kapselt, um zu verhindern, dass Ihr Controller aufgebläht wird und Ihr Modell nicht zu eng an den Webanforderungs- / Antwortzyklus gekoppelt wird .quelle
User.current_user
über die Registrierung des aktuellen Threads verwenden müssen, tun Sie dies, tun Sie einfach Ihr Bestes, um es nicht zuObwohl diese Frage von vielen beantwortet wurde, wollte ich nur meine zwei Cent schnell hinzufügen.
Die Verwendung des Ansatzes #current_user für das Benutzermodell sollte aus Gründen der Thread-Sicherheit mit Vorsicht implementiert werden.
Es ist in Ordnung, eine Klassen- / Singleton-Methode für Benutzer zu verwenden, wenn Sie daran denken, Thread.current als Methode zum Speichern und Abrufen Ihrer Werte zu verwenden. Dies ist jedoch nicht so einfach, da Sie auch Thread.current zurücksetzen müssen, damit die nächste Anforderung keine Berechtigungen erbt, die sie nicht erben sollte.
Der Punkt, den ich ansprechen möchte, ist, wenn Sie den Status in Klassen- oder Singleton-Variablen speichern, daran zu denken, dass Sie die Thread-Sicherheit aus dem Fenster werfen.
quelle
Der Controller sollte der Modellinstanz mitteilen
Die Arbeit mit der Datenbank ist Aufgabe des Modells. Die Bearbeitung von Webanforderungen, einschließlich der Kenntnis des Benutzers für die aktuelle Anforderung, ist Aufgabe des Controllers.
Wenn eine Modellinstanz den aktuellen Benutzer kennen muss, sollte ein Controller dies mitteilen.
def create @item = Item.new @item.current_user = current_user # or whatever your controller method is ... end
Dies setzt voraus, dass
Item
einattr_accessor
für hatcurrent_user
.(Hinweis - Ich habe diese Antwort zum ersten Mal auf eine andere Frage gepostet , aber ich habe gerade bemerkt, dass diese Frage ein Duplikat dieser Frage ist.)
quelle
User.current_user
macht der threadlokale Ansatz manchmal den Rest des Codes kürzer und verständlicher, obwohl MVC nicht funktioniert.Ich bin ganz auf dünne Controller- und Fat-Modelle eingestellt, und ich denke, Auth sollte dieses Prinzip nicht brechen.
Ich programmiere jetzt seit einem Jahr mit Rails und komme aus der PHP-Community.Für mich ist es eine triviale Lösung, den aktuellen Benutzer als "anforderungslang global" festzulegen. Dies erfolgt standardmäßig in einigen Frameworks, zum Beispiel:
In Yii können Sie auf den aktuellen Benutzer zugreifen, indem Sie Yii :: $ app-> user-> identity aufrufen. Siehe http://www.yiiframework.com/doc-2.0/guide-rest-authentication.html
In Lavavel können Sie dasselbe auch tun, indem Sie Auth :: user () aufrufen. Siehe http://laravel.com/docs/4.2/security
Warum, wenn ich nur den aktuellen Benutzer vom Controller übergeben kann?
Nehmen wir an, wir erstellen eine einfache Blog-Anwendung mit Unterstützung für mehrere Benutzer. Wir erstellen sowohl eine öffentliche Site (Anon-Benutzer können Blog-Beiträge lesen und kommentieren) als auch eine Admin-Site (Benutzer sind angemeldet und haben CRUD-Zugriff auf ihre Inhalte in der Datenbank.)
Hier sind "die Standard-ARs":
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
Jetzt auf der öffentlichen Seite:
class PostsController < ActionController::Base def index # Nothing special here, show latest posts on index page. @posts = Post.includes(:comments).latest(10) end end
Das war sauber und einfach. Auf der Admin-Site wird jedoch etwas mehr benötigt. Dies ist die Basisimplementierung für alle Admin-Controller:
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
Daher wurde die Anmeldefunktion hinzugefügt und der aktuelle Benutzer wird in einem globalen Benutzer gespeichert. Das Benutzermodell sieht nun folgendermaßen aus:
# 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 ist jetzt threadsicher
Lassen Sie uns andere Modelle erweitern, um dies zu nutzen:
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
Das Post-Modell wurde erweitert, sodass das Gefühl besteht, dass derzeit nur die Beiträge des Benutzers angemeldet sind. Wie cool ist das!
Der Admin Post Controller könnte ungefähr so aussehen:
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
Für das Kommentarmodell könnte die Administratorversion folgendermaßen aussehen:
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
IMHO: Dies ist eine leistungsstarke und sichere Methode, um sicherzustellen, dass Benutzer nur auf einmal auf ihre Daten zugreifen / diese ändern können. Überlegen Sie, wie viel Entwickler Testcode schreiben muss, wenn jede Abfrage mit "current_user.posts.whatever_method (...)" beginnen muss. Viel.
Korrigieren Sie mich, wenn ich falsch liege, aber ich denke:
Es geht um die Trennung von Bedenken. Selbst wenn klar ist, dass nur der Controller die Authentifizierungsprüfungen durchführen soll, sollte der aktuell angemeldete Benutzer keinesfalls in der Controller-Schicht bleiben.
Einziges, woran man sich erinnern sollte: NICHT überbeanspruchen! Denken Sie daran, dass es möglicherweise E-Mail-Mitarbeiter gibt, die User.current nicht verwenden, oder dass Sie über eine Konsole usw. auf die Anwendung zugreifen.
quelle
Alter Thread, aber erwähnenswert, dass es ab Rails 5.2 eine eingebaute Lösung dafür gibt: den aktuellen Modell-Singleton, der hier behandelt wird: https://evilmartians.com/chronicles/rails-5-2-active-storage-and -beyond # aktuell-alles
quelle
Nun, meine Vermutung hier ist, dass
current_user
es sich endlich um eine Benutzerinstanz handelt. Warum fügen Sie diese Berechtigungen nicht demUser
Modell oder dem Datenmodell hinzu und möchten, dass die Berechtigungen angewendet oder abgefragt werden?Ich vermute, dass Sie Ihr Modell irgendwie umstrukturieren und den aktuellen Benutzer als Parameter übergeben müssen, wie zum Beispiel:
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
quelle
Ich bin immer wieder erstaunt über die Antworten von Leuten, die nichts über die zugrunde liegenden Geschäftsanforderungen des Fragestellers wissen. Ja, im Allgemeinen sollte dies vermieden werden. Es gibt jedoch Umstände, unter denen dies angemessen und äußerst nützlich ist. Ich hatte selbst nur einen.
Hier war meine Lösung:
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
Dies führt den Stapel rückwärts und sucht nach einem Rahmen, auf den reagiert wird
current_user
. Wenn keine gefunden wird, wird null zurückgegeben. Es könnte robuster gemacht werden, indem der erwartete Rückgabetyp bestätigt wird und möglicherweise bestätigt wird, dass der Besitzer des Rahmens ein Controller-Typ ist, der jedoch im Allgemeinen nur gut funktioniert.quelle
attr_acessor
vom Anrufer auf den aktuellen Benutzer. Ich bin mir ziemlich sicher, dass fast jeder erfahrene Programmierer Ihnen sagen wird, dass dies eine beschissene Idee ist, und Sie sollten es nicht tun. Ich mag die Neuheit Ihrer Antwort, aber es ist ein Greuel.attr_acessor
Ich habe dies in einer Anwendung von mir. Es sucht einfach nach der aktuellen Controller-Sitzung [: user] und setzt sie auf eine User.current_user-Klassenvariable. Dieser Code funktioniert in der Produktion und ist ziemlich einfach. Ich wünschte, ich könnte sagen, ich hätte es mir ausgedacht, aber ich glaube, ich habe es von einem anderen Internet-Genie ausgeliehen.
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
quelle
around_filter
und dies am Ende des Blocksensure User.current_user = nil
hinzufügen. Versuchen Sie anstelle von cattr auch, einen Threadsafe-Ansatz zu verwendendef self.current_user=(user) Thread.current[:current_user] = user end
def self.current_user Thread.current[:current_user] end
Ich verwende das Plugin für die deklarative Autorisierung und es funktioniert ähnlich wie das, was Sie mit erwähnen.
current_user
Es verwendet abefore_filter
, um escurrent_user
herauszuziehen und dort zu speichern, wo die Modellebene darauf zugreifen kann. Sieht aus wie das:# 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
Ich verwende jedoch nicht die Modellfunktionen der deklarativen Autorisierung. Ich bin alle für den "Skinny Controller - Fat Model" -Ansatz, aber ich bin der Meinung, dass die Autorisierung (sowie die Authentifizierung) in die Controller-Ebene gehört.
quelle
Um mehr Licht auf die Antwort von armchairdj zu werfen
Ich habe mich dieser Herausforderung gestellt, als ich an einer Rails 6- Anwendung gearbeitet habe.
So habe ich es gelöst :
Ab Rails 5.2 können Sie jetzt einen magischen
Current
Singleton hinzufügen, der sich wie ein globaler Store verhält, auf den Sie von überall in Ihrer App zugreifen können.Definieren Sie es zunächst in Ihren Modellen :
# app/models/current.rb class Current < ActiveSupport::CurrentAttributes attribute :user end
Stellen Sie den Benutzer als Nächstes irgendwo in Ihrem Controller ein , um ihn in Modellen, Jobs, Mailern oder wo immer Sie möchten zugänglich machen zu können:
# 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
Jetzt können Sie das
Current.user
in Ihren Modellen aufrufen :# 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
ODER Sie können das
Current.user
in Ihren Formularen aufrufen :# 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
ODER Sie können das
Current.user
in Ihren Ansichten aufrufen :# 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 %>
Hinweis : Sie können sagen: "Diese Funktion verstößt gegen das Prinzip der Trennung von Bedenken!" Ja tut es. Verwenden Sie es nicht, wenn es sich falsch anfühlt.
Mehr über diese Antwort können Sie hier lesen: Alles aktuell
Das ist alles.
ich hoffe das hilft
quelle
Mein Gefühl ist, dass der aktuelle Benutzer Teil des "Kontexts" Ihres MVC-Modells ist. Denken Sie an den aktuellen Benutzer wie die aktuelle Zeit, den aktuellen Protokollierungsdatenstrom, die aktuelle Debugging-Ebene, die aktuelle Transaktion usw. Sie könnten alle diese übergeben. " Modalitäten "als Argumente in Ihre Funktionen. Oder Sie stellen es durch Variablen in einem Kontext außerhalb des aktuellen Funktionskörpers zur Verfügung. Der lokale Thread-Kontext ist aufgrund der einfachsten Thread-Sicherheit die bessere Wahl als globale oder Variablen mit anderweitigem Gültigkeitsbereich. Wie Josh K sagte, besteht die Gefahr bei Thread-Einheimischen darin, dass sie nach der Aufgabe gelöscht werden müssen, was ein Abhängigkeitsinjektions-Framework für Sie tun kann. MVC ist ein etwas vereinfachtes Bild der Anwendungsrealität und nicht alles wird davon abgedeckt.
quelle
Ich bin so spät zu dieser Party, aber wenn Sie eine differenzierte Zugriffskontrolle benötigen oder komplexe Berechtigungen haben, würde ich das Cancancan Gem auf jeden Fall empfehlen: https://github.com/CanCanCommunity/cancancan
Sie können Berechtigungen für jede Aktion in Ihrem Controller nach Belieben definieren. Da Sie die aktuelle Fähigkeit auf jedem Controller definieren, können Sie alle erforderlichen Parameter senden, z
current_user
. Sie können eine generischecurrent_ability
Methode definierenApplicationController
und die Dinge automatisch einrichten: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
Auf diese Weise können Sie eine
UserAbilities
Klasse mit Ihrem UserController, PostAbilities mit Ihrem PostController usw. verknüpfen. Und dann definieren Sie die komplexen Regeln darin: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
Es ist ein tolles Juwel! ich hoffe es hilft!
quelle