【问题标题】:Rails simple access controlRails 简单的访问控制
【发布时间】:2025-11-24 22:25:01
【问题描述】:

我知道在 Rails 中制作了几个 gem 来处理授权。但是将这些 gem 用于简单的访问控制真的值得吗?

我的应用程序中只有几个“角色”,我觉得强大的gem毫无用处,甚至会拖慢响应时间。

我已经实现了一个解决方案,但后来我学习了一些安全类 (:p),我意识到我的模型是错误的(“默认允许,然后限制”而不是“默认拒绝,然后允许”)。

现在我怎样才能简单地实现“默认拒绝,在特定情况下允许”?

基本上我想放在我的 ApplicationController 的最顶部

class ApplicationController < ApplicationController::Base
  before_filter :deny_access

在我的其他控制器的最顶端:

class some_controller < ApplicationController
  before_filter :allow_access_to_[entity/user]

这些allow_access_to_ before_filters 应该执行类似skip_before_filter 的操作

def allow_access_to_[...]
  skip_before_filter(:deny_access) if condition
end

但这不起作用,因为这些allow_access before 过滤器不会在deny_access before_filter 之前进行评估

对于这种访问控制的自定义实现有什么变通方法和更好的解决方案?

编辑

  • 许多非 RESTful 操作
  • 我需要按操作访问控制
  • undefined method 'skip_before_filter' for #&lt;MyController... 为什么?
  • 我的 before_filters 可能会变得很棘手
before_action :find_project, except: [:index, :new, :create]
before_action(except: [:show, :index, :new, :create]) do |c|
   c.restrict_access_to_manager(@project.manager)
end

【问题讨论】:

  • 过滤器按列出的顺序调用之前。但是您可以使用prepend_before_filter 代替allow_access_to_ 将其添加到前置过滤器堆栈的前面。
  • 制作一个轻量级的 mixin 模块,它有一个名为 authorize(或其他名称)的方法。在 ApplicationController 级别添加此模块并调用 authorize 作为 ApplicationController's before_filter。让authorize 的默认实现拒绝一切。在您的子控制器中,覆盖 authorize 以执行您想要的授权。基本上这只是一个简单的授权 gem 的 DIY 形式。
  • 我需要按操作访问控制。所以我不能只在控制器中重新定义authorize 方法。或者,我必须在此方法中检查我正在调用哪个操作。
  • 不过,您在使用通用过滤器方法时会遇到同样的问题。像cancancan 这样的东西解决了这个问题,但那是一个外部的宝石(相当轻巧,但仍然超出了您的问题范围)。但是,如果您遵循基本的 CRUD 模式,只需为每个 CRUD 字母分配方法,并且仅在您有奇怪的地方时才覆盖。然后在authorize 中执行类似if :read, then 类型的逻辑。同样,如果是混入,大多数子控制器的代码几乎看不到它。这使您无需测试每个操作,但可以让您轻松更改(与 cancancan 相同)。
  • 使用过滤器方法,我可以做到prepend_before_action :allow_access_to_, only: [:action1, :action2]

标签: ruby-on-rails ruby access-control


【解决方案1】:

我真的建议使用经过实战考验的 gem 来进行身份验证和授权,而不是自己动手。这些 gem 拥有庞大的测试套件,而且设置起来并不难。

我最近使用PunditDevise 的角色实现了基于操作的授权

如果您不想进一步配置 pundit,只要您使用的 gem 提供 current_user 方法,Devise 就可以更改。

# app/controllers/application_controller.rb
class ApplicationController < ActionController::Base
  include Pundit

  rescue_from Pundit::NotAuthorizedError, with: :rescue_unauthorized

  # Lock actions untill authorization is performed
  before_action :authorize_user

  # Fallback when not authorized
  def rescue_unauthorized(exception)
    policy_name = exception.policy.class.to_s.underscore
    flash[:notice] = t(
      "#{policy_name}.#{exception.query}",
      scope: "pundit",
      default: :default
    )
    redirect_to(request.referrer || root_path)
  end
end

# app/models/user.rb
class User < ActiveRecord::Base
  has_many :roles, through: :memberships

  def authorized?(action)
    claim = String(action)
    roles.pluck(:claim).any? { |role_claim| role_claim == claim }
  end
end

# app/policies/user_policy.rb => maps to user_controller#actions
class UserPolicy < ApplicationPolicy
  class Scope < Scope
    attr_reader :user, :scope

    # user is automagically set to current_user
    def initialize(user, scope)
      @user = user
      @scope = scope
    end

    def resolve
      scope.all
    end
  end

  def index?
    # If user has a role which has the claim :view_users
    # Allow this user to use the user#index action
    @user.authorized? :view_users 
  end

  def new?
    @user.authorized? :new_users
  end

  def edit?
    @user.authorized? :edit_users
  end

  def create?
    new?
  end

  def update?
    edit?
  end

  def destroy?
    @user.authorized? :destroy_users
  end
end

长话短说:

如果您将 pundit 配置为对 github 页面上详细描述的每个请求强制授权,则控制器会根据使用的控制器评估策略。

UserController -> UserPolicy

动作用问号定义,即使是非安静的路线。

def index?
  # authorization is done inside the method.
  # true = authorization succes
  # false = authorization failure
end

这是我基于操作的授权的解决方案,希望对您有所帮助。

欢迎优化和反馈!

【讨论】:

  • 我的用户“角色”来自 LDAP,我手动重建了用户组的缓存。不确定我是否可以轻松地将其与 gem 交互。但我会尝试一些宝石。是的,我也使用 Devise。
  • 我认为问题不在于您可以使用的宝石。如果您可以让current_user 知道roles 他具有相应的允许操作,您可以轻松地使用权威构建您自己的授权系统。 Pundit 只期望判断此人是否被授权的真假。
  • 设计用于身份验证,而不是授权。 Pundit 非常简单,您可以毫不费力地推出自己的产品,它确实不会给您带来太多好处(但总体而言,它的设计很好——比 CanCan(Can) 好得多)。
【解决方案2】:

只要您致力于实现,滚动您自己的实现并不一定是坏事。

它不会经过社区测试和维护,因此从长远来看,您必须愿意自己维护它,如果它损害安全性,您需要真正确定自己在做什么并格外小心。如果您已经涵盖了这些并且现有的替代方案并不能真正满足您的需求,那么制作自己的替代方案并不是一个坏主意。总的来说,这是一次非常好的学习体验。

我用ActionAccess 推出了自己的产品,我对结果非常满意。

  • 默认锁定方法:

    class ApplicationController < ActionController::Base
      lock_access
    
      # ...
    end
    
  • 每次操作访问控制:

    class ArticlesController < ApplicationController
      let :admins, :all
      let :editors, [:index, :show, :edit, :update]
      let :all, [:index, :show]
    
      def index
        # ...
      end
    
      # ...
    end
    
  • 真正的轻量级实现。

我鼓励您不要使用它,而是查看源代码,它在 cmets 中占有相当大的份额,应该是一个很好的灵感来源。 ControllerAdditions 可能是一个不错的起点。

ActionAccess 在内部采用不同的方法,但您可以重构您的答案以模仿它的 API,如下所示:

module AccessControl
  extend ActiveSupport::Concern

  included do
    before_filter :lock_access
  end

  module ClassMethods
    def lock_access
      unless @authorized
        # Redirect user...
      end
    end

    def allow_manager_to(actions = [])
      prepend_before_action only: actions do
        @authorized = true if current_user_is_a_manager?
      end
    end
  end
end

class ApplicationController < ActionController::Base
  include AccessControl  # Locked by default

  # ...
end

class ProjectController < ApplicationController
  allow_managers_to [:edit, :update]  # Per-action access control

  # ...
end

以这个例子为伪代码,我没有测试过。

希望这会有所帮助。

【讨论】:

  • 感谢您添加此答案。我喜欢你的语法/名字,它看起来很干净,我一定会看看你的 gem 以获得一些灵感!
【解决方案3】:

我不喜欢我之前使用 prepend_before_action 的解决方案,这是使用 ActionController 回调的一个很好的实现

module AccessControl
  extend ActiveSupport::Concern

  class UnauthorizedException < Exception
  end

  class_methods do
    define_method :access_control do |*names, &blk|
      _insert_callbacks(names, blk) do |name, options|
        set_callback(:access_control, :before, name, options)
      end
    end
  end

  included do

    define_callbacks :access_control

    before_action :deny_by_default
    around_action :perform_if_access_granted

    def perform_if_access_granted
      run_callbacks :access_control do
        if @access_denied and not @access_authorized
          @request_authentication = true unless user_signed_in?
          render(
            file: File.join(Rails.root, 'app/views/errors/403.html'),
            status: 403,
            layout: 'error')
        else
          yield
        end
      end
    end

    def deny_by_default
      @access_denied ||= true
    end

    def allow_access
      @access_authorized = true
    end
  end
end

然后您可以添加自己的 allow_access_to_x 方法(例如在同一个 AccessControl 关注点中):

def allow_access_to_participants_of(project)
  return unless user_signed_in?
  allow_access if current_user.in?(project.executants)
end

通过以下方式在您的控制器中使用它(不要忘记在您的 ApplicationController 中包含 AccessControl

class ProjectsController < ApplicationController
  access_control(only: [:show, :edit, :update]) do
    set_project
    allow_access_to_participants_of(@project)
    allow_access_to_project_managers
  end

  def index; ...; end;
  def show; ...; end;
  def edit; ...; end;
  def update; ...; end;

  def set_project
    @project = Project.find(params[:project_id])
  end
end

【讨论】:

    【解决方案4】:

    编辑:过时的答案,我有一个更友好的实现,涉及使用 access_control

    根据 evanbikes 的建议,现在我将使用 prepend_before 操作。我发现它非常简单和灵活,但是如果我意识到它还不够好,我会尝试其他的东西。

    此外,如果您发现以下解决方案存在安全问题/其他问题,请发表评论和/或投反对票。我不喜欢在 SO 中留下不好的例子。

    class ApplicationController < ApplicationController::Base
      include AccessControl
      before_filter :access_denied
      ...
    

    我的访问控制模块

    module AccessControl
      extend ActiveSupport::Concern
      included do
        def access_denied(message: nil)
            unless @authorized
                flash.alert = 'Unauthorized access'
                flash.info = "Authorized entities : #{@authorized_entities.join(', '}" if @authorized_entities
                render 'static_pages/home', :status => :unauthorized
                end
            end
    
            def allow_access_to_managers
                (@authorized_entities ||= []) << "Project managers"
                @authorized = true if manager_logged_in?
            end
            ...
    

    我如何在控制器中使用交流电:

    class ProjectController < ApplicationController
      # In reverse because `prepend_` is LIFO
      prepend_before_action(except: [:show, :index, :new, :create]) do |c|
        c.allow_access_to_manager(@manager.administrateur)
      end
      prepend_before_action :find_manager, except: [:index, :new, :create]
    

    【讨论】:

      最近更新 更多