Program Tip

Ruby on Rails의 모델 내에서 current_user에 액세스

programtip 2020. 11. 7. 10:24
반응형

Ruby on Rails의 모델 내에서 current_user에 액세스


Ruby on Rails 앱에서 세분화 된 액세스 제어를 구현해야합니다. 개별 사용자에 대한 권한은 데이터베이스 테이블에 저장되며 각 리소스 (즉, 모델의 인스턴스)가 특정 사용자의 읽기 또는 쓰기 허용 여부를 결정하도록하는 것이 최선이라고 생각했습니다. 매번 컨트롤러에서 이러한 결정을 내리는 것은 확실히 매우 건조하지 않을 것입니다.
문제는이를 수행하기 위해 모델이 현재 사용자에게 액세스하여 . 하지만 일반적으로 모델은 세션 데이터에 액세스 할 수 없습니다.may_read?(current_user, attribute_name)

현재 스레드에서 현재 사용자에 대한 참조를 저장하기위한 몇 가지 제안 이 있습니다 ( 예 : 이 블로그 게시물) . 이것은 확실히 문제를 해결할 것입니다.

이웃 Google 결과는 User 클래스에 현재 사용자에 대한 참조를 저장하라고 조언했는데, 애플리케이션이 한 번에 많은 사용자를 수용 할 필요가없는 누군가가 생각한 것 같습니다. ;)

간단히 말해서, 모델 내에서 현재 사용자 (예 : 세션 데이터)에 액세스하려는 나의 소망은 내가 잘못한 것에서 비롯된 입니다.

내가 어떻게 틀렸는 지 말해 줄 수있어?


current_user모델에서 벗어나 려는 당신의 본능 이 정확 하다고 말하고 싶습니다 .

다니엘처럼 나는 모두 마른 컨트롤러와 뚱뚱한 모델을 좋아하지만 책임의 명확한 구분도 있습니다. 컨트롤러의 목적은 들어오는 요청과 세션을 관리하는 것입니다. 모델은 "사용자 x가이 객체에 y를 할 수 있습니까?"라는 질문에 답할 수 있어야하지만 current_user. 콘솔에 있다면 어떨까요? 크론 작업이 실행 중이면 어떻게됩니까?

모델에 올바른 권한 API가있는 많은 경우 before_filters여러 작업에 적용되는 한 줄로 처리 할 수 ​​있습니다 . 그러나 상황이 더 복잡해 lib/지면 컨트롤러가 부풀어 오르는 것을 방지하고 모델이 웹 요청 / 응답주기에 너무 밀접하게 연결되는 것을 방지하기 위해 더 복잡한 권한 부여 논리를 캡슐화 하는 별도의 레이어 (에서 가능 ) 를 구현할 수 있습니다. .


이 질문에 많은 사람들이 대답했지만 저는 2 센트를 빨리 추가하고 싶었습니다.

User 모델에서 #current_user 접근 방식을 사용하는 것은 스레드 안전성 때문에주의해서 구현해야합니다.

Thread.current를 방법으로 사용하거나 값을 저장하고 검색하는 것을 기억한다면 User에서 클래스 / 싱글 톤 메서드를 사용하는 것이 좋습니다. 그러나 Thread.current를 재설정해야하므로 다음 요청이 권한을 상속하지 않아야하기 때문에 그렇게 쉽지는 않습니다.

내가하려는 요점은 상태를 클래스 또는 싱글 톤 변수에 저장하는 경우 스레드 안전을 창 밖으로 던지고 있다는 것을 기억하십시오.


컨트롤러는 모델 인스턴스에 알려야합니다.

데이터베이스 작업은 모델의 작업입니다. 현재 요청에 대한 사용자를 아는 것을 포함하여 웹 요청을 처리하는 것이 컨트롤러의 작업입니다.

따라서 모델 인스턴스가 현재 사용자를 알아야하는 경우 컨트롤러가이를 알려야합니다.

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

이것은 for Item있다고 가정합니다 .attr_accessorcurrent_user

(참고-이 답변을 다른 질문 에 처음 게시 했지만이 질문이이 질문과 중복되는 것을 방금 확인했습니다.)


저는 스키니 컨트롤러와 팻 모델에 모두 참여하고 있으며 인증이이 원칙을 어기면 안된다고 생각합니다.

저는 1 년 동안 Rails로 코딩을 해왔고 PHP 커뮤니티에서 왔습니다. 저에게는 현재 사용자를 "request-long global"로 설정하는 것이 간단한 솔루션입니다. 이는 기본적으로 일부 프레임 워크에서 수행됩니다. 예를 들면 다음과 같습니다.

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를 참조 하십시오.

컨트롤러에서 현재 사용자를 전달할 수있는 이유는 무엇입니까?

다중 사용자를 지원하는 간단한 블로그 애플리케이션을 만들고 있다고 가정 해 보겠습니다. 우리는 공개 사이트 (anon 사용자가 블로그 게시물을 읽고 댓글을 달 수 있음)와 관리자 사이트 (사용자가 로그인되어 있고 데이터베이스의 콘텐츠에 대한 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

댓글 모델의 경우 관리자 버전은 다음과 같습니다.

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 : 이것은 사용자가 한 번에 모든 데이터에 액세스 / 수정할 수 있도록하는 강력하고 안전한 방법입니다. 모든 쿼리가 "current_user.posts.whatever_method (...)"로 시작해야하는 경우 개발자가 얼마나 많은 테스트 코드를 작성해야하는지 생각해보십시오. 많이.

내가 틀렸다면 정정하지만 다음과 같이 생각합니다.

문제의 분리에 관한 것입니다. 컨트롤러 만 인증 검사를 처리해야한다는 것이 분명한 경우에도 현재 로그인 한 사용자는 컨트롤러 계층에 머물러서는 안됩니다.

기억해야 할 사항 : 과용하지 마십시오! User.current를 사용하지 않는 이메일 작업자가 있거나 콘솔 등에서 애플리케이션에 액세스 할 수 있습니다.


Ancient thread, but worth noting that starting in Rails 5.2, there's a baked-in solution to this: the Current model singleton, covered here: https://evilmartians.com/chronicles/rails-5-2-active-storage-and-beyond#current-everything


Well my guess here is that current_user is finally a User instance, so, why don't u add these permissions to the User model or to the data model u want to have the permissions to be applied or queried?

My guess is that u need to restructure your model somehow and pass the current user as a param, like doing:

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

I have this in an application of mine. It simply looks for the current controllers session[:user] and sets it to a User.current_user class variable. This code works in production and is pretty simple. I wish I could say I came up with it, but I believe I borrowed it from an internet genius elsewhere.

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

I'm always amazed at "just don't do that" responses by people who know nothing of the questioner's underlying business need. Yes, generally this should be avoided. But there are circumstances where it's both appropriate and highly useful. I just had one myself.

Here was my solution:

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

This walks the stack backwards looking for a frame that responds to current_user. If none is found it returns nil. It could be made more robust by confirming the expected return type, and possibly by confirming owner of the frame is a type of controller, but generally works just dandy.


I'm using the Declarative Authorization plugin, and it does something similar to what you are mentioning with current_user It uses a before_filter to pull current_user out and store it where the model layer can get to it. Looks like this:

# 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

I'm not using the model features of Declarative Authorization though. I'm all for the "Skinny Controller - Fat Model" approach, but my feeling is that authorization (as well as authentication) is something that belongs in the controller layer.


My feeling is the current user is part of the "context" of your MVC model, think of the current user like of the current time, the current logging stream, the current debugging level, the current transaction etc. You could pass all these "modalities" as arguments into your functions. Or you make it available by variables in a context outside the current function body. Thread local context is the better choice than global or otherwise scoped variables because of easiest thread safety. As Josh K said, the danger with thread locals is that they must be cleared after the task, something a dependency injection framework can do for you. MVC is a somewhat simplified picture of the application reality and not everything is covered by it.

참고URL : https://stackoverflow.com/questions/1568218/access-to-current-user-from-within-a-model-in-ruby-on-rails

반응형