在Rails应用程序中管理用户权限的完整指南

492 阅读5分钟

网络应用程序的一个共同要求是能够分配特定的角色和权限。

许多类型的网络应用程序在提供限制性访问时对管理员和普通用户进行区分。这通常是使用一个简单的布尔值来确定用户是否是管理员。然而,角色和权限可以变得更加复杂。

你的应用程序的价值在于限制对某些数据和行动的访问。这绝对是你不希望搞砸的事情。在这篇文章中,我们将解释如何在一个基本的Ruby on Rails应用程序中实现角色和权限。

我需要一个 gem 来管理权限吗?

不,你不需要gem,特别是如果你的应用程序很小,你想避免在你的代码库中添加更多的依赖性。 然而,如果你正在寻找替代方案,这里有最流行的处理角色和权限的gem:

  • Devise:Devise是一个用于认证和角色管理的宝石,它是一个非常复杂和强大的解决方案。 它在GitHub上有21.7k颗星,是本篇文章中最受欢迎的 repo,但它所做的不仅仅是角色管理。它被称为认证解决方案,所以只有当你需要一个非常健壮的库时才会将其应用到你的代码库中。

  • Pundit:Pundit是一个使用简单的Ruby对象的宝石,它可能是我们将涉及的最简单的策略宝石。它使用简单,具有最小的授权,并且与使用纯Ruby相似。它在GitHub上有7.3k颗星,是目前最受欢迎的策略宝石。

  • CanCan:CanCan是一个授权库,可以限制一个特定用户被允许访问的资源。然而,CanCan已经被废弃多年,只适用于Rails 3和更早的版本。

  • CanCanCan:CanCanCan是另一个用于Ruby和Ruby on Rails的授权库。它是CanCan的替代品,目前正在维护中。它在GitHub上有4.9k颗星,是最不受欢迎的,但它工作得相当好,而且维护得很好。

所有这些宝石都很好,但在普通的Ruby中自己构建权限也不是太难。我将向你展示如何在没有宝石的情况下管理权限,使用一种叫做策略对象模式的策略。

策略对象模式

策略对象是一种用于处理权限和角色的设计模式。你可以在每次需要检查某物或某人是否被允许执行某个动作时使用它。它封装了复杂的业务规则,可以很容易地被其他具有不同规则的策略对象所取代。所有的外部依赖都被注入到策略对象中,封装了权限检查逻辑,从而产生了一个干净的控制器和模型。像Pundit、Cancan和Cancanc这样的宝石实现了这种模式。

纯粹的策略对象规则

  • 返回值必须是一个布尔值
  • 逻辑必须是简单的
  • 在方法内部,我们应该只调用所传递对象的方法

实施

让我们从命名规则开始;文件名有_policy 的后缀,类和策略在最后。在这个方法中,名字总是以? 字符结束(例如,UsersPolicy#allowed? )。

下面是一些示例代码:

class UsersPolicy
  def initialize(user)
    @user = user
  end

  def allowed?
    admin? || editor?
  end

  def editor?
    @user.where(editor: true)
  end

  def admin?
    @user.where(admin: true)
  end
end

我应该在哪些情况下使用它们?

当你的应用程序有一个以上的限制性访问和限制性动作类型时。例如,可以用以下方式创建帖子:

  • 至少有一个标签。
  • 限制只有管理员和编辑可以创建,以及
  • 要求编辑者需要经过验证。

下面是一个没有策略对象的控制器例子:

class PostsController < ApplicationController
  def create
    if @post.tag_ids.size > 0
    && (current_user.role == ‘admin’
    || (current_user.role == ‘editor’ && current_user.verified_email))
      # create
    end
  end
end

因为上面的条件检查又长又丑,而且无法阅读,所以应该应用策略对象模式。

让我们开始创建PostsCreationPolicy :

class PostsCreationPolicy
  attr_reader :user, :post

  def initialize(user, post)
    @user = user
    @post = post
  end

  def self.create?(user, post)
    new(user, post).create?
  end

  def create?
    with_tags? && author_is_allowed?
  end

  private

  def with_tags?
    post.tag_ids.size > 0
  end

  def author_is_allowed?
    is_admin? || editor_is_verified?
  end

  def is_admin?
    user.role == ‘admin’
  end

  def editor_is_verified?
    user.role == ‘editor` && user.verified_email
  end
end

我们带有策略对象的控制器看起来像这样:

class PostsController < ApplicationController
  def create
    if PostsCreationPolicy.create?(current_user, @post)
      # create
    end
  end
end

如何在Rails中使用策略对象

在app/policies 内创建一个策略目录,并将所有的策略类放在那里。当你需要调用控制器时,你可以直接在一个动作中调用,或者使用before_action

class PostsController < ApplicationController
  before_action :authorized?, only: [:edit, :create, :update, :destroy]

  def authorized?
    unless ::PostsCreationPolicy.create?(current_user, @post)
      render :file => "public/404.html", :status => :unauthorized
    end
  end
end

如何测试策略对象

测试控制器中的行为很简单:

require 'rails_helper'

RSpec.describe "/posts", type: :request do
  describe "when user is not allowed" do
    let(:user_not_allowed) { create(:user, admin: false, editor: false) }
    let(:tag) { create(:tag) }
    let(:valid_attributes) { attributes_for(:post, tag_id: tag.id) }

    before do
      sign_in user_not_allowed
    end

    describe "GET /index" do
      it "return code 401" do
        diet = Post.create! valid_attributes
        get edit_post_url(post)
        expect(response).to have_http_status(401)
      end
    end
  end
end

测试策略也很简单;我们有很多小方法,只有一个责任。

require 'rails_helper'

RSpec.describe PostsCreationPolicy do
  describe "when user is not allowed" do
    let(:user) { create(:user, editor: false, admin: false) }
    let(:user_editor) { create(:user, editor: true, email: verified) }
    let(:tag) { create(:tag) }
    let(:post) { create(:post, tag_id: tag.id) }

    describe ".create?" do
      context "when user is allowed" do
        it "creates a new post" do
          expect(described_class.create?(user_editor, post)).to eq(true)
        end
      end

      context "when user is not allowed" do
        it "does not create a new post" do
          expected(described_class.create?(user, post)).to eq(false)
        end
      end
    end

    # ...more test cases
  end
end

我们测试对象是否允许在每种情况下被创建。

总结

策略模式的概念很小,但产生了很大的效果。 考虑在你每次需要处理简单或复杂的权限时应用策略对象。当涉及到用RSpec进行测试时,你不需要使用数据库记录;你的策略是纯粹的Ruby对象,你的测试将变得简单而快速。