权限控制在Rails中的常见场景
开发一个后台管理系统时,不同角色的用户需要看到不同的功能。比如管理员能删数据,普通员工只能查看。这时候,权限控制就成了绕不开的一环。Ruby on Rails 虽然没有内置完整的权限系统,但社区提供了像 Pundit 和 CanCanCan 这样的工具,让权限管理变得清晰又灵活。
Pundit:基于策略的权限设计
很多人一开始会直接在控制器里写一堆 if 判断,比如判断当前用户是不是管理员。时间一长,代码就散落在各处,改起来费劲。Pundit 的思路是把权限逻辑抽出来,变成独立的“策略”文件。
先在 Gemfile 加上:
gem 'pundit'然后执行 bundle install。接着生成一个应用级的策略基类:
rails generate pundit:install现在可以为某个资源创建策略。比如你有一个 Post 模型,想控制谁能编辑、删除,运行:
rails generate pundit:policy post会生成 app/policies/post_policy.rb,内容类似这样:
class PostPolicy < ApplicationPolicy
def update?
user.admin? || record.user == user
end
def destroy?
user.admin?
end
end这里的 update? 允许作者自己或管理员修改文章,而 destroy? 只给管理员开权限。控制器中调用起来也很干净:
class PostsController < ApplicationController
def update
@post = Post.find(params[:id])
authorize @post
# ...
end
end只要调用 authorize,Pundit 就会自动找对应的 PostPolicy 并执行 update? 方法。不通过就抛异常,可以在 ApplicationController 统一处理。
CanCanCan:声明式权限配置
如果你更喜欢集中定义权限,CanCanCan 是另一个流行选择。它主张在 Ability 类里一次性写清楚每个角色能干什么。
同样先加 gem:
gem 'cancancan', '~> 3.0'安装后创建 ability.rb:
rails generate cancan:ability打开 app/models/ability.rb,可以这样写:
class Ability
include CanCan::Ability
def initialize(user)
user ||= User.new # guest user (not logged in)
if user.admin?
can :manage, :all
else
can :read, Post
can :create, Post
can :update, Post do |post|
post.user == user
end
end
end
end:manage 代表所有操作,:read、:create、:update 是具体动作。普通用户只能更新自己写的 Post。控制器中使用:
def show
@post = Post.find(params[:id])
authorize! :read, @post
end或者直接用 load_and_authorize_resource 自动加载并鉴权。
视图中的权限判断
不只是接口要控制,页面上的按钮也得动态显示。比如普通用户不该看到“删除”按钮。
用 Pundit 时可以这样写:
<% if policy(@post).destroy? %>
<= link_to '删除', @post, method: :delete %>
<% end %>CanCanCan 则用 can? 辅助方法:
<% if can? :destroy, @post %>
<= link_to '删除', @post, method: :delete %>
<% end %>这样前端就不会暴露不该有的操作入口。
细粒度控制与实际项目经验
真实项目中,权限往往更复杂。比如一个企业协作平台,团队成员能编辑项目文档,但只有创建者能转让项目。这种逻辑放在策略类里就很合适,保持控制器轻量。
另外,别忘了 API 场景。如果是 JSON 接口,在未授权时应该返回 403 状态码,而不是跳转到登录页。ApplicationController 里可以针对 API 请求做特殊响应:
rescue_from Pundit::NotAuthorizedError, with: :user_not_authorized
private
def user_not_authorized
if request.format.json?
render json: { error: 'Forbidden' }, status: :forbidden
else
redirect_to(request.referrer || root_path, alert: '权限不足')
end
end这类细节处理好了,系统才显得专业。