Logo

CanCanCan实战指南:让你的权限控制更智能

avatar mc 22 Aug 2024

问题描述

在大多数项目中,通常会涉及多个角色,不同角色对同一资源具有不同的读写权限。例如,管理员可以查看所有商品,而普通用户只能看到已上架的商品。如果要提供一个商品列表API,在引入权限管理模块之前,常见做法有:

  1. 判断当前角色,并返回相应权限所允许的内容。
  2. 拆分不同的命名空间,如app_apiadmin_api,并返回不同的内容。

然而,如果后续需要添加更多角色,这两种做法都会导致代码难以维护。

解决方案

引入权限管理模块,下面要介绍的是cancancan这个gem,通过各种必要属性构造一个Ability对象,后续有关权限操作都通过该对象去处理,而非当前登录用户的角色

优点

  • 易于扩展:项目后续需要增减角色,权限调整,开放Oauth授权等情况,均只需要调整权限配置的代码,基本不需要调整API代码
  • 易于维护:权限配置主要集中在 app/models/ability.rb 文件里,遇到权限相关问题时,只要看这个文件并调整就好了

缺点

  • 对于十分简单的项目或者单一角色的项目来说,该解决方案会有点繁琐

解决场景描述

下面分别针对以下场景介绍如何使用cancancan

控制可访问内容

需求
  • 管理员可以看到所有商品
  • 普通用户只能看到已上架商品
代码实现
# app/models/ability.rb
class Ability
  include CanCan::Ability
  
  def initialize(user = nil)
    case user
    when AdminUser
      can :read, Product
    when User
      can :read, Product, published_at: ..Time.current
    end
  end
end

# app/services/api/v1/products
namespace :products do
  desc "列表"
  get do
    ability = Ability.new(user)
    collection = Product.accessible_by(ability, :read)
    
    present collection, with: API::Entities::Product
  end
end

控制请求参数

需求
  • 管理员可更新分组名称和分组是否热门
  • 普通用户只可以更新分组名称

如管理员和用户都又更新分组(Group)的权限,但管理员可修改的内容更广泛,这里也可以通过Ability去控制

代码实现
# app/models/ability.rb
class Ability
  include CanCan::Ability
  
  def initialize(user = nil)
    case user
    when AdminUser
      can :update, Group
      can :update_hot, Group
    when User
      can :update, Group, owner: user
    end
  end
end


# app/services/api/v1/groups
class API::V1::Groups < Grape::API
  namespace :groups do
    route_param :id do
      desc "更新"
      params do
        optional :title
        optional :is_hot, type: Boolean # 是否热门分组
      end
      put do
        ability = Ability.new(user)
        resource = Group.find(params[:id])
        # 判断是否有权限更新
        ability.authorize! :update, resource
        # 通过grape的declared把没在params里定义的属性过滤掉
        data = declared(params)
        # 如果当前登录用户不能编辑is_hot属性,则移除掉
        data.delete(:is_hot) unless abilitiy.can?(:update_hot, resource)

        if resource.update(data)
          present resouce, with: API::Entities::Group
        else
          # 处理失败情况
        end
      end
    end
  end
end

控制响应参数

需求
  • 管理员可查看商品基础信息以及成本价
  • 普通用户可查看商品基础信息
代码实现
# app/models/ability.rb
class Ability
  include CanCan::Ability
  
  def initialize(user = nil)
    case user
    when AdminUser
      can :read, Product
      can :read_cost_price, Product
    when User
      can :read, Product, published_at: ..Time.current
    end
  end
end


# app/services/api/v1/products
class API::V1::Products < Grape::API
  namespace :products do
    route_param :id do
      desc "详情"
      get do
        ability = Ability.new(user)
	resource = Group.find(params[:id])
	only = [:name, :description, :price]
	only += [:cost_price] if ability.can?(:read_cost_price, resource)
	   	
	present resouce, with: API::Entities::Product, only: only
      end
    end
  end
end

这些示例展示了如何使用CanCanCan进行不同场景的权限控制,从而简化和标准化权限管理过程。

Tags
rails
cancancan
权限管理