rails-authorization-cancancan

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Rails Authorization with CanCanCan

基于CanCanCan的Rails授权管理

CanCanCan is a popular authorization library for Rails that restricts what resources a given user is allowed to access. It centralizes all permission logic in a single Ability class, keeping authorization rules DRY and maintainable.
CanCanCan是Rails生态中一款热门的授权库,用于限制特定用户可访问的资源。它将所有权限逻辑集中在单个Ability类中,确保授权规则遵循DRY原则且易于维护。

Quick Setup

快速设置

bash
undefined
bash
undefined

Add to Gemfile

Add to Gemfile

bundle add cancancan
bundle add cancancan

Generate Ability class

Generate Ability class

rails generate cancan:ability

This creates `app/models/ability.rb` where all authorization rules are defined.
rails generate cancan:ability

这会创建`app/models/ability.rb`文件,所有授权规则都将在此定义。

Core Concepts

核心概念

Defining Abilities

定义操作权限

The
Ability
class centralizes all permission logic:
ruby
undefined
Ability
类集中管理所有权限逻辑:
ruby
undefined

app/models/ability.rb

app/models/ability.rb

class Ability include CanCan::Ability
def initialize(user) # Guest users (not signed in) can :read, Post, published: true can :read, Comment
# Signed-in users
return unless user.present?

can :read, Post
can :create, Post
can :update, Post, user_id: user.id
can :destroy, Post, user_id: user.id

can :create, Comment
can :update, Comment, user_id: user.id
can :destroy, Comment, user_id: user.id

# Admin users
return unless user.admin?

can :manage, :all  # Can do anything
end end

**Best Practice**: Structure rules hierarchically (guest → user → admin) for clarity.
class Ability include CanCan::Ability
def initialize(user) # Guest users (not signed in) can :read, Post, published: true can :read, Comment
# Signed-in users
return unless user.present?

can :read, Post
can :create, Post
can :update, Post, user_id: user.id
can :destroy, Post, user_id: user.id

can :create, Comment
can :update, Comment, user_id: user.id
can :destroy, Comment, user_id: user.id

# Admin users
return unless user.admin?

can :manage, :all  # Can do anything
end end

**最佳实践**:按层级结构定义规则(访客→普通用户→管理员),提升可读性。

Actions and Resources

操作与资源

Standard CRUD Actions

标准CRUD操作

ruby
:read    # :index and :show
:create  # :new and :create
:update  # :edit and :update
:destroy # :destroy

:manage  # All actions (use carefully!)
ruby
:read    # :index and :show
:create  # :new and :create
:update  # :edit and :update
:destroy # :destroy

:manage  # All actions (use carefully!)

Custom Actions

自定义操作

ruby
can :publish, Post
can :archive, Post
can :approve, Comment
ruby
can :publish, Post
can :archive, Post
can :approve, Comment

Multiple Resources

多资源配置

ruby
can :read, [Post, Comment, Category]
can :manage, [User, Post], user_id: user.id
ruby
can :read, [Post, Comment, Category]
can :manage, [User, Post], user_id: user.id

Ability Conditions

权限条件

Hash Conditions

哈希条件

ruby
undefined
ruby
undefined

Simple equality

Simple equality

can :update, Post, user_id: user.id
can :update, Post, user_id: user.id

Multiple conditions (AND logic)

Multiple conditions (AND logic)

can :read, Post, published: true, category_id: user.accessible_category_ids
can :read, Post, published: true, category_id: user.accessible_category_ids

SQL fragment (use sparingly)

SQL fragment (use sparingly)

can :read, Post, ["published_at <= ?", Time.zone.now]
undefined
can :read, Post, ["published_at <= ?", Time.zone.now]
undefined

Block Conditions

块条件

ruby
undefined
ruby
undefined

Complex logic

Complex logic

can :update, Post do |post| post.user_id == user.id || user.admin? end
can :update, Post do |post| post.user_id == user.id || user.admin? end

With associations

With associations

can :read, Post do |post| post.published? || post.user_id == user.id end
can :read, Post do |post| post.published? || post.user_id == user.id end

Accessing current user

Accessing current user

can :destroy, Comment do |comment| comment.user_id == user.id && comment.created_at > 15.minutes.ago end

**Important**: Block conditions cannot be used with `accessible_by` for database queries. Use hash conditions when you need to filter collections.
can :destroy, Comment do |comment| comment.user_id == user.id && comment.created_at > 15.minutes.ago end

**重要提示**:块条件无法与`accessible_by`配合用于数据库查询。当需要过滤集合时,请使用哈希条件。

Combining Conditions

条件组合

ruby
undefined
ruby
undefined

Multiple can statements are OR'd together

Multiple can statements are OR'd together

can :read, Post, published: true # Public posts can :read, Post, user_id: user.id # Own posts
can :read, Post, published: true # Public posts can :read, Post, user_id: user.id # Own posts

User can read posts that are EITHER published OR owned by them

User can read posts that are EITHER published OR owned by them

undefined
undefined

Controller Integration

控制器集成

Manual Authorization

手动授权

ruby
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
    authorize! :read, @post  # Raises CanCan::AccessDenied if not authorized
  end

  def update
    @post = Post.find(params[:id])
    authorize! :update, @post

    if @post.update(post_params)
      redirect_to @post
    else
      render :edit
    end
  end
end
ruby
class PostsController < ApplicationController
  def show
    @post = Post.find(params[:id])
    authorize! :read, @post  # Raises CanCan::AccessDenied if not authorized
  end

  def update
    @post = Post.find(params[:id])
    authorize! :update, @post

    if @post.update(post_params)
      redirect_to @post
    else
      render :edit
    end
  end
end

Automatic Loading and Authorization

自动加载与授权

ruby
class PostsController < ApplicationController
  load_and_authorize_resource

  def index
    # @posts automatically loaded with accessible_by
  end

  def show
    # @post automatically loaded and authorized
  end

  def create
    # @post initialized and authorized
    if @post.save
      redirect_to @post
    else
      render :new
    end
  end

  def update
    # @post loaded and authorized
    if @post.update(post_params)
      redirect_to @post
    else
      render :edit
    end
  end
end
Benefits: Eliminates repetitive authorization code across RESTful actions.
ruby
class PostsController < ApplicationController
  load_and_authorize_resource

  def index
    # @posts automatically loaded with accessible_by
  end

  def show
    # @post automatically loaded and authorized
  end

  def create
    # @post initialized and authorized
    if @post.save
      redirect_to @post
    else
      render :new
    end
  end

  def update
    # @post loaded and authorized
    if @post.update(post_params)
      redirect_to @post
    else
      render :edit
    end
  end
end
优势:消除RESTful操作中重复的授权代码。

Load and Authorize Options

加载与授权选项

ruby
undefined
ruby
undefined

Specific actions only

Specific actions only

load_and_authorize_resource only: [:show, :edit, :update, :destroy] load_and_authorize_resource except: [:index]
load_and_authorize_resource only: [:show, :edit, :update, :destroy] load_and_authorize_resource except: [:index]

Different resource name

Different resource name

load_and_authorize_resource :article
load_and_authorize_resource :article

Custom find method

Custom find method

load_and_authorize_resource find_by: :slug
load_and_authorize_resource find_by: :slug

Nested resources

Nested resources

class CommentsController < ApplicationController load_and_authorize_resource :post load_and_authorize_resource :comment, through: :post end
class CommentsController < ApplicationController load_and_authorize_resource :post load_and_authorize_resource :comment, through: :post end

Skip loading (only authorize)

Skip loading (only authorize)

authorize_resource
authorize_resource

Skip authorization for specific actions

Skip authorization for specific actions

skip_authorize_resource only: [:index]
undefined
skip_authorize_resource only: [:index]
undefined

Fetching Authorized Records

获取授权记录

accessible_by

accessible_by

Retrieve only records the user can access:
ruby
undefined
仅检索用户可访问的记录:
ruby
undefined

In controller

In controller

def index @posts = Post.accessible_by(current_ability) end
def index @posts = Post.accessible_by(current_ability) end

With specific action

With specific action

@posts = Post.accessible_by(current_ability, :read) @editable_posts = Post.accessible_by(current_ability, :update)
@posts = Post.accessible_by(current_ability, :read) @editable_posts = Post.accessible_by(current_ability, :update)

Chainable with ActiveRecord

Chainable with ActiveRecord

@published_posts = Post.published.accessible_by(current_ability) @posts = Post.accessible_by(current_ability).where(category_id: params[:category_id])

**Performance**: Uses SQL conditions from ability rules for efficient database queries.
@published_posts = Post.published.accessible_by(current_ability) @posts = Post.accessible_by(current_ability).where(category_id: params[:category_id])

**性能说明**:利用权限规则生成SQL条件,实现高效的数据库查询。

View Helpers

视图助手

Conditional UI Elements

条件化UI元素

ruby
undefined
ruby
undefined

Check single permission

Check single permission

<% if can? :update, @post %> <%= link_to 'Edit', edit_post_path(@post) %> <% end %>
<% if can? :destroy, @post %> <%= link_to 'Delete', @post, method: :delete, data: { confirm: 'Are you sure?' } %> <% end %>
<% if can? :update, @post %> <%= link_to 'Edit', edit_post_path(@post) %> <% end %>
<% if can? :destroy, @post %> <%= link_to 'Delete', @post, method: :delete, data: { confirm: 'Are you sure?' } %> <% end %>

Negative check

Negative check

<% if cannot? :update, @post %>
<p>You cannot edit this post</p> <% end %>
<% if cannot? :update, @post %>
<p>You cannot edit this post</p> <% end %>

Multiple permissions

Multiple permissions

<% if can?(:update, @post) || can?(:destroy, @post) %>
<div class="post-actions"> <%= link_to 'Edit', edit_post_path(@post) if can? :update, @post %> <%= link_to 'Delete', @post, method: :delete if can? :destroy, @post %> </div> <% end %>
<% if can?(:update, @post) || can?(:destroy, @post) %>
<div class="post-actions"> <%= link_to 'Edit', edit_post_path(@post) if can? :update, @post %> <%= link_to 'Delete', @post, method: :delete if can? :destroy, @post %> </div> <% end %>

Check on class (useful in index views)

Check on class (useful in index views)

<% if can? :create, Post %> <%= link_to 'New Post', new_post_path %> <% end %>
undefined
<% if can? :create, Post %> <%= link_to 'New Post', new_post_path %> <% end %>
undefined

Navigation Menus

导航菜单

ruby
<nav>
  <%= link_to 'Posts', posts_path if can? :read, Post %>
  <%= link_to 'New Post', new_post_path if can? :create, Post %>
  <%= link_to 'Admin', admin_path if can? :manage, :all %>
</nav>
ruby
<nav>
  <%= link_to 'Posts', posts_path if can? :read, Post %>
  <%= link_to 'New Post', new_post_path if can? :create, Post %>
  <%= link_to 'Admin', admin_path if can? :manage, :all %>
</nav>

Handling Unauthorized Access

处理未授权访问

Exception Rescue

异常捕获

ruby
undefined
ruby
undefined

app/controllers/application_controller.rb

app/controllers/application_controller.rb

class ApplicationController < ActionController::Base rescue_from CanCan::AccessDenied do |exception| respond_to do |format| format.html { redirect_to root_path, alert: exception.message } format.json { render json: { error: exception.message }, status: :forbidden } end end end
undefined
class ApplicationController < ActionController::Base rescue_from CanCan::AccessDenied do |exception| respond_to do |format| format.html { redirect_to root_path, alert: exception.message } format.json { render json: { error: exception.message }, status: :forbidden } end end end
undefined

Custom Error Messages

自定义错误信息

ruby
undefined
ruby
undefined

In Ability class

In Ability class

can :update, Post, user_id: user.id do |post| post.user_id == user.id end
can :update, Post, user_id: user.id do |post| post.user_id == user.id end

In controller with custom message

In controller with custom message

authorize! :update, @post, message: "You can only edit your own posts"
undefined
authorize! :update, @post, message: "You can only edit your own posts"
undefined

Flash Messages

提示消息

ruby
rescue_from CanCan::AccessDenied do |exception|
  redirect_to root_path, alert: "Access denied: #{exception.message}"
end
ruby
rescue_from CanCan::AccessDenied do |exception|
  redirect_to root_path, alert: "Access denied: #{exception.message}"
end

Common Patterns

常见模式

Role-Based Authorization

基于角色的授权

ruby
undefined
ruby
undefined

app/models/user.rb

app/models/user.rb

class User < ApplicationRecord ROLES = %w[guest user moderator admin].freeze
enum role: { guest: 0, user: 1, moderator: 2, admin: 3 }
def role?(check_role) role.to_sym == check_role.to_sym end end
class User < ApplicationRecord ROLES = %w[guest user moderator admin].freeze
enum role: { guest: 0, user: 1, moderator: 2, admin: 3 }
def role?(check_role) role.to_sym == check_role.to_sym end end

app/models/ability.rb

app/models/ability.rb

class Ability include CanCan::Ability
def initialize(user) user ||= User.new # Guest user
if user.admin?
  can :manage, :all
elsif user.moderator?
  can :manage, Post
  can :manage, Comment
  can :read, User
elsif user.user?
  can :read, :all
  can :create, Post
  can :manage, Post, user_id: user.id
  can :manage, Comment, user_id: user.id
else
  can :read, Post, published: true
end
end end
undefined
class Ability include CanCan::Ability
def initialize(user) user ||= User.new # Guest user
if user.admin?
  can :manage, :all
elsif user.moderator?
  can :manage, Post
  can :manage, Comment
  can :read, User
elsif user.user?
  can :read, :all
  can :create, Post
  can :manage, Post, user_id: user.id
  can :manage, Comment, user_id: user.id
else
  can :read, Post, published: true
end
end end
undefined

Organization/Tenant-Based Authorization

组织/租户级授权

ruby
class Ability
  include CanCan::Ability

  def initialize(user)
    return unless user.present?

    # User can manage resources in their organization
    can :manage, Post, organization_id: user.organization_id
    can :manage, Comment, post: { organization_id: user.organization_id }

    # Admin can manage organization settings
    can :manage, Organization, id: user.organization_id if user.admin?
  end
end
ruby
class Ability
  include CanCan::Ability

  def initialize(user)
    return unless user.present?

    # User can manage resources in their organization
    can :manage, Post, organization_id: user.organization_id
    can :manage, Comment, post: { organization_id: user.organization_id }

    # Admin can manage organization settings
    can :manage, Organization, id: user.organization_id if user.admin?
  end
end

Time-Based Authorization

基于时间的授权

ruby
class Ability
  include CanCan::Ability

  def initialize(user)
    return unless user.present?

    # Can edit posts within 1 hour of creation
    can :update, Post do |post|
      post.user_id == user.id && post.created_at > 1.hour.ago
    end

    # Can read posts after publication date
    can :read, Post, ["published_at <= ?", Time.current]
  end
end
ruby
class Ability
  include CanCan::Ability

  def initialize(user)
    return unless user.present?

    # Can edit posts within 1 hour of creation
    can :update, Post do |post|
      post.user_id == user.id && post.created_at > 1.hour.ago
    end

    # Can read posts after publication date
    can :read, Post, ["published_at <= ?", Time.current]
  end
end

Attribute-Based Authorization

基于属性的授权

ruby
class Ability
  include CanCan::Ability

  def initialize(user)
    return unless user.present?

    # Users can update specific attributes of their own posts
    can [:update], Post, user_id: user.id

    # Only admins can change published status
    cannot :update, Post, :published unless user.admin?

    # Users can update their profile but not role
    can :update, User, id: user.id
    cannot :update, User, :role
  end
end
ruby
class Ability
  include CanCan::Ability

  def initialize(user)
    return unless user.present?

    # Users can update specific attributes of their own posts
    can [:update], Post, user_id: user.id

    # Only admins can change published status
    cannot :update, Post, :published unless user.admin?

    # Users can update their profile but not role
    can :update, User, id: user.id
    cannot :update, User, :role
  end
end

Strong Parameters with CanCanCan

CanCanCan与强参数结合

ruby
undefined
ruby
undefined

app/controllers/posts_controller.rb

app/controllers/posts_controller.rb

def post_params params.require(:post).permit(:title, :body, :published) end
def post_params params.require(:post).permit(:title, :body, :published) end

Restrict based on abilities

Restrict based on abilities

def post_params params.require(:post).permit( current_user.admin? ? [:title, :body, :published] : [:title, :body] ) end
undefined
def post_params params.require(:post).permit( current_user.admin? ? [:title, :body, :published] : [:title, :body] ) end
undefined

Testing

测试

RSpec Setup

RSpec配置

ruby
undefined
ruby
undefined

spec/support/cancan.rb

spec/support/cancan.rb

RSpec.configure do |config| config.include CanCan::Ability end
undefined
RSpec.configure do |config| config.include CanCan::Ability end
undefined

Testing Abilities

测试权限规则

ruby
undefined
ruby
undefined

spec/models/ability_spec.rb

spec/models/ability_spec.rb

require 'rails_helper' require 'cancan/matchers'
RSpec.describe Ability, type: :model do subject(:ability) { Ability.new(user) }
describe 'Guest user' do let(:user) { nil }
it { is_expected.to be_able_to(:read, Post.new(published: true)) }
it { is_expected.not_to be_able_to(:create, Post) }
it { is_expected.not_to be_able_to(:update, Post) }
end
describe 'Regular user' do let(:user) { create(:user) } let(:own_post) { create(:post, user: user) } let(:other_post) { create(:post) }
it { is_expected.to be_able_to(:read, Post) }
it { is_expected.to be_able_to(:create, Post) }
it { is_expected.to be_able_to(:update, own_post) }
it { is_expected.not_to be_able_to(:update, other_post) }
it { is_expected.to be_able_to(:destroy, own_post) }
it { is_expected.not_to be_able_to(:destroy, other_post) }
end
describe 'Admin user' do let(:user) { create(:user, admin: true) }
it { is_expected.to be_able_to(:manage, :all) }
end end
undefined
require 'rails_helper' require 'cancan/matchers'
RSpec.describe Ability, type: :model do subject(:ability) { Ability.new(user) }
describe 'Guest user' do let(:user) { nil }
it { is_expected.to be_able_to(:read, Post.new(published: true)) }
it { is_expected.not_to be_able_to(:create, Post) }
it { is_expected.not_to be_able_to(:update, Post) }
end
describe 'Regular user' do let(:user) { create(:user) } let(:own_post) { create(:post, user: user) } let(:other_post) { create(:post) }
it { is_expected.to be_able_to(:read, Post) }
it { is_expected.to be_able_to(:create, Post) }
it { is_expected.to be_able_to(:update, own_post) }
it { is_expected.not_to be_able_to(:update, other_post) }
it { is_expected.to be_able_to(:destroy, own_post) }
it { is_expected.not_to be_able_to(:destroy, other_post) }
end
describe 'Admin user' do let(:user) { create(:user, admin: true) }
it { is_expected.to be_able_to(:manage, :all) }
end end
undefined

Testing Controllers

测试控制器

ruby
undefined
ruby
undefined

spec/controllers/posts_controller_spec.rb

spec/controllers/posts_controller_spec.rb

RSpec.describe PostsController, type: :controller do let(:user) { create(:user) } let(:other_user) { create(:user) } let(:post) { create(:post, user: user) }
before { sign_in user }
describe 'GET #edit' do context 'when editing own post' do it 'allows access' do get :edit, params: { id: post.id } expect(response).to have_http_status(:ok) end end
context 'when editing other user post' do
  let(:other_post) { create(:post, user: other_user) }

  it 'denies access' do
    expect {
      get :edit, params: { id: other_post.id }
    }.to raise_error(CanCan::AccessDenied)
  end
end
end end
undefined
RSpec.describe PostsController, type: :controller do let(:user) { create(:user) } let(:other_user) { create(:user) } let(:post) { create(:post, user: user) }
before { sign_in user }
describe 'GET #edit' do context 'when editing own post' do it 'allows access' do get :edit, params: { id: post.id } expect(response).to have_http_status(:ok) end end
context 'when editing other user post' do
  let(:other_post) { create(:post, user: other_user) }

  it 'denies access' do
    expect {
      get :edit, params: { id: other_post.id }
    }.to raise_error(CanCan::AccessDenied)
  end
end
end end
undefined

Testing accessible_by

测试accessible_by

ruby
RSpec.describe 'Post access', type: :model do
  let(:user) { create(:user) }
  let(:admin) { create(:user, admin: true) }
  let!(:published_post) { create(:post, published: true) }
  let!(:draft_post) { create(:post, published: false, user: user) }
  let!(:other_draft) { create(:post, published: false) }

  it 'returns correct posts for user' do
    ability = Ability.new(user)
    accessible = Post.accessible_by(ability)

    expect(accessible).to include(published_post, draft_post)
    expect(accessible).not_to include(other_draft)
  end

  it 'returns all posts for admin' do
    ability = Ability.new(admin)
    accessible = Post.accessible_by(ability)

    expect(accessible).to include(published_post, draft_post, other_draft)
  end
end
ruby
RSpec.describe 'Post access', type: :model do
  let(:user) { create(:user) }
  let(:admin) { create(:user, admin: true) }
  let!(:published_post) { create(:post, published: true) }
  let!(:draft_post) { create(:post, published: false, user: user) }
  let!(:other_draft) { create(:post, published: false) }

  it 'returns correct posts for user' do
    ability = Ability.new(user)
    accessible = Post.accessible_by(ability)

    expect(accessible).to include(published_post, draft_post)
    expect(accessible).not_to include(other_draft)
  end

  it 'returns all posts for admin' do
    ability = Ability.new(admin)
    accessible = Post.accessible_by(ability)

    expect(accessible).to include(published_post, draft_post, other_draft)
  end
end

Performance Considerations

性能优化建议

Use Hash Conditions for Collections

对集合使用哈希条件

ruby
undefined
ruby
undefined

Good - generates SQL query

Good - generates SQL query

can :read, Post, user_id: user.id @posts = Post.accessible_by(current_ability)
can :read, Post, user_id: user.id @posts = Post.accessible_by(current_ability)

Bad - cannot generate SQL, will raise error

Bad - cannot generate SQL, will raise error

can :read, Post do |post| post.user_id == user.id end @posts = Post.accessible_by(current_ability) # Error!
undefined
can :read, Post do |post| post.user_id == user.id end @posts = Post.accessible_by(current_ability) # Error!
undefined

Eager Loading

预加载关联

ruby
undefined
ruby
undefined

Prevent N+1 queries

Prevent N+1 queries

@posts = Post.accessible_by(current_ability).includes(:user, :comments)
undefined
@posts = Post.accessible_by(current_ability).includes(:user, :comments)
undefined

Caching Abilities

缓存权限实例

ruby
undefined
ruby
undefined

Cache ability checks in instance variable

Cache ability checks in instance variable

def current_ability @current_ability ||= Ability.new(current_user) end
undefined
def current_ability @current_ability ||= Ability.new(current_user) end
undefined

Integration with Pundit

与Pundit集成

If migrating from Pundit or using both:
ruby
undefined
如果从Pundit迁移或同时使用两者:
ruby
undefined

CanCanCan uses a single Ability class

CanCanCan uses a single Ability class

Pundit uses policy classes per model

Pundit uses policy classes per model

They can coexist, but choose one primary approach

They can coexist, but choose one primary approach

CanCanCan: Centralized, better for simple RBAC

CanCanCan: Centralized, better for simple RBAC

Pundit: Decentralized, better for complex domain logic

Pundit: Decentralized, better for complex domain logic

undefined
undefined

Advanced Patterns

进阶模式

For more complex scenarios, see:
  • Multi-tenancy: references/multi-tenancy.md
  • API authorization: references/api-authorization.md
  • Complex permissions: references/complex-permissions.md
针对更复杂的场景,可参考:
  • 多租户管理references/multi-tenancy.md
  • API授权references/api-authorization.md
  • 复杂权限配置references/complex-permissions.md

Resources

资源链接