rails-authorization-cancancan
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseRails 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
undefinedbash
undefinedAdd 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 class centralizes all permission logic:
Abilityruby
undefinedAbilityruby
undefinedapp/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 anythingend
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 anythingend
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, Commentruby
can :publish, Post
can :archive, Post
can :approve, CommentMultiple Resources
多资源配置
ruby
can :read, [Post, Comment, Category]
can :manage, [User, Post], user_id: user.idruby
can :read, [Post, Comment, Category]
can :manage, [User, Post], user_id: user.idAbility Conditions
权限条件
Hash Conditions
哈希条件
ruby
undefinedruby
undefinedSimple 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]
undefinedcan :read, Post, ["published_at <= ?", Time.zone.now]
undefinedBlock Conditions
块条件
ruby
undefinedruby
undefinedComplex 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
undefinedruby
undefinedMultiple 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
undefinedundefinedController 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
endruby
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
endAutomatic 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
endBenefits: 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
undefinedruby
undefinedSpecific 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]
undefinedskip_authorize_resource only: [:index]
undefinedFetching Authorized Records
获取授权记录
accessible_by
accessible_by
Retrieve only records the user can access:
ruby
undefined仅检索用户可访问的记录:
ruby
undefinedIn 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
undefinedruby
undefinedCheck 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 %>
undefinedNavigation 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
undefinedruby
undefinedapp/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
undefinedclass 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
undefinedCustom Error Messages
自定义错误信息
ruby
undefinedruby
undefinedIn 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"
undefinedauthorize! :update, @post, message: "You can only edit your own posts"
undefinedFlash Messages
提示消息
ruby
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_path, alert: "Access denied: #{exception.message}"
endruby
rescue_from CanCan::AccessDenied do |exception|
redirect_to root_path, alert: "Access denied: #{exception.message}"
endCommon Patterns
常见模式
Role-Based Authorization
基于角色的授权
ruby
undefinedruby
undefinedapp/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
endend
end
undefinedclass 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
endend
end
undefinedOrganization/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
endruby
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
endTime-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
endruby
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
endAttribute-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
endruby
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
endStrong Parameters with CanCanCan
CanCanCan与强参数结合
ruby
undefinedruby
undefinedapp/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
undefineddef post_params
params.require(:post).permit(
current_user.admin? ? [:title, :body, :published] : [:title, :body]
)
end
undefinedTesting
测试
RSpec Setup
RSpec配置
ruby
undefinedruby
undefinedspec/support/cancan.rb
spec/support/cancan.rb
RSpec.configure do |config|
config.include CanCan::Ability
end
undefinedRSpec.configure do |config|
config.include CanCan::Ability
end
undefinedTesting Abilities
测试权限规则
ruby
undefinedruby
undefinedspec/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
undefinedrequire '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
undefinedTesting Controllers
测试控制器
ruby
undefinedruby
undefinedspec/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
endend
end
undefinedRSpec.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
endend
end
undefinedTesting 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
endruby
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
endPerformance Considerations
性能优化建议
Use Hash Conditions for Collections
对集合使用哈希条件
ruby
undefinedruby
undefinedGood - 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!
undefinedcan :read, Post do |post|
post.user_id == user.id
end
@posts = Post.accessible_by(current_ability) # Error!
undefinedEager Loading
预加载关联
ruby
undefinedruby
undefinedPrevent 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)
undefinedCaching Abilities
缓存权限实例
ruby
undefinedruby
undefinedCache ability checks in instance variable
Cache ability checks in instance variable
def current_ability
@current_ability ||= Ability.new(current_user)
end
undefineddef current_ability
@current_ability ||= Ability.new(current_user)
end
undefinedIntegration with Pundit
与Pundit集成
If migrating from Pundit or using both:
ruby
undefined如果从Pundit迁移或同时使用两者:
ruby
undefinedCanCanCan 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
undefinedundefinedAdvanced 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