Недавно я написал обзор некоторых популярных решений для аутентификации в Rails . Однако во многих случаях самой аутентификации недостаточно — вероятно, вам нужен механизм авторизации для определения правил доступа для различных пользователей. Существует ли существующее решение, предпочтительно не очень сложное, но все же гибкое?
Встречайте CanCanCan , гибкое решение для авторизации на Rails. Этот проект начался как CanCan, автором которого является Райан Бейтс, создатель RailsCasts Однако пару лет назад этот проект стал неактивным, поэтому члены сообщества решили создать CanCanCan, продолжение первоначального решения.
В этой статье я интегрирую CanCanCan в простой демонстрационный проект, определяя правила доступа, рассматривая возможные варианты и обсуждая, как CanCanCan может уменьшить дублирование кода. Прочитав этот пост, вы будете хорошо понимать основные функции CanCanCan и будете готовы использовать его в реальных проектах.
Исходный код можно найти на GitHub .
Рабочая демоверсия доступна по адресу sitepoint-cancan.herokuapp.com .
Подготовка детской площадки
Планирование и закладка фундамента
Чтобы начать взламывать CanCanCan, мы должны сначала подготовить площадку для наших экспериментов. Я собираюсь позвонить
мое приложение iCan, потому что я могу (хи!):
$ rails new iCan -T
Я собираюсь придерживаться Rails 4.1, но CanCanCan также совместим с Rails 3.
Демонстрационное приложение представит пользователям список проектов, как текущих, так и завершенных. Пользователи с разными ролями будут иметь разный уровень доступа:
- Гости не будут иметь никакого доступа к проектам. Они будут видеть только главную страницу сайта.
- Пользователи смогут видеть только текущие проекты. Они не смогут ничего изменить или удалить.
- Модераторы будут иметь доступ ко всем проектам с возможностью редактировать текущие.
- Админы будут иметь полный доступ.
Наша задача — представить эти роли и определить для них надлежащие правила доступа.
Я предпочитаю начать с Bootstrap, чтобы стилизовать приложение:
Gemfile
[...]
gem 'bootstrap-sass'
[...]
Бегать
$ bundle install
Настройте корневой маршрут:
конфиг / routes.rb
[...]
root to: 'pages#index'
[...]
Создать контроллер:
pages_controller.rb
class PagesController < ApplicationController
def index
end
end
и вид
просмотров / страниц / index.html.erb
<div class="page-header"><h1>Welcome!</h1></div>
Измените макет, чтобы использовать преимущества стилей Bootstrap:
просмотров / макеты / application.html.erb
[...]
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<%= link_to 'iCan', root_path, class: 'navbar-brand' %>
</div>
<div id="navbar">
<ul class="nav navbar-nav">
</ul>
</div>
</div>
</nav>
<div class="container">
<% flash.each do |key, value| %>
<div class="alert alert-<%= key %>">
<%= value %>
</div>
<% end %>
<%= yield %>
</div>
[...]
Поддельная аутентификация
Итак, мы уже кратко обсудили роли, которые нужно добавить, и их уровни доступа, но сначала нам нужно ввести некоторую аутентификацию. CanCanCan на самом деле не волнует, какую систему аутентификации вы используете. Требуется только наличие метода current_user
nil
Недавно я написал серию статей об аутентификации в Rails , поэтому не стесняйтесь выбирать одно из описанных там решений. Однако для этой демонстрации я не буду использовать настоящую аутентификацию, чтобы упростить вещи и сосредоточиться только на авторизации. Вместо этого я введу базовый класс User
модели / user.rb
class User
ROLES = {0 => :guest, 1 => :user, 2 => :moderator, 3 => :admin}
attr_reader :role
def initialize(role_id = 0)
@role = ROLES.has_key?(role_id.to_i) ? ROLES[role_id.to_i] : ROLES[0]
end
def role?(role_name)
role == role_name
end
end
В основном, есть словарь со всеми доступными ролями. После инициализации назначьте пользователю одну из ролей
на основании предоставленного идентификатора. role?
это просто обычный метод, который мы будем использовать позже.
Теперь давайте определим действие контроллера для установки роли:
sessions_controller.rb
class SessionsController < ApplicationController
def update
id = params[:id].to_i
session[:id] = User::ROLES.has_key?(id) ? id : 0
flash[:success] = "Your new role #{User::ROLES[id]} was set!"
redirect_to root_path
end
end
Настройте маршрут:
конфиг / routes.rb
[...]
resources :sessions, only: [:update]
[...]
Добавьте ссылки, чтобы выбрать роль:
просмотров / макеты / application.html.erb
<nav class="navbar navbar-inverse">
<div class="container">
<div class="navbar-header">
<%= link_to 'iCan', root_path, class: 'navbar-brand' %>
</div>
<div id="navbar">
<ul class="nav navbar-nav">
</ul>
<ul class="nav navbar-nav pull-right">
<li class="dropdown">
<a class="dropdown-toggle" aria-expanded="false" role="button" data-toggle="dropdown" href="#">
Role
<span class="caret"></span>
</a>
<ul class="dropdown-menu" role="menu">
<% User::ROLES.each do |k, v| %>
<li>
<%= link_to session_path(k), method: :patch do %>
<%= v %>
<% if v == current_user.role %>
<small class="text-muted">(current)</small>
<% end %>
<% end %>
</li>
<% end %>
</ul>
</li>
</ul>
</div>
</div>
</nav>
Я полагаюсь на выпадающий виджет Bootstrap здесь, поэтому включите его:
application.js
[...]
//= require bootstrap/dropdown
[...]
Также, если вы используете Turbolinks, jquery-turbolinks
Gemfile
[...]
gem 'jquery-turbolinks'
[...]
application.js
[...]
//= require jquery.turbolinks
[...]
Наконец, представьте метод current_user
application_controller.rb
[...]
private
def current_user
User.new(session[:id])
end
helper_method :current_user
[...]
Большой! Загрузите сервер и проверьте правильность переключения ролей.
Добавление проектов
Последнее, что нужно сделать, это добавить модель Project
Каждый проект будет иметь только заголовок:
$ rails g model Project title:string
$ rake db:migrate
контроллер:
projects_controller.rb
class ProjectsController < ApplicationController
def index
@projects = Project.all
end
end
Маршруты:
конфиг / routes.rb
[...]
resources :projects
[...]
И мнение:
просмотров / проекты / index.html.erb
<div class="page-header"><h1>Projects</h1></div>
<% @projects.each do |project| %>
<div class="well well-sm">
<h2><%= project.title %></h2>
</div>
<% end %>
Давайте также воспользуемся seed.rb, чтобы добавить несколько демонстрационных записей в базу данных:
дБ / seeds.rb
20.times {|i| Project.create!({title: "Project #{i + 1}"}) }
Бегать
$ rake db:seed
заполнить таблицу ваших projects
Теперь детская площадка готова, и мы можем включить музыку и танцевать CanCanCan.
Интеграция CanCanCan и определение способностей
Вставьте CanCanCan в свой Gemfile
Gemfile
[...]
gem 'cancancan', '~> 1.10'
[...]
и беги
$ bundle install
Теперь нам нужно сгенерировать файл able.rb, который будет содержать все наши правила доступа:
$ rails g cancan:ability
Откройте этот файл:
модели / ability.rb
class Ability
include CanCan::Ability
def initialize(user)
end
end
Все ваши правила доступа (принадлежат нам …. Извините) должны быть помещены в метод initialize
Есть несколько закомментированных примеров, которые помогут вам начать работу.
user
current_user
Ability
def current_ability
@current_ability ||= Ability.new(current_user)
end
Если, например, вы не хотите называть этот метод current_user
Ability
current_ability
ApplicationController
Другой вариант переименования current_user
alias_method :current_user, :my_own_current_user
Таким образом, current_ability
Узнайте больше здесь .
В нашем случае current_user
В реальном сценарии аутентификации он, вероятно, вернет nil
модели / ability.rb
[...]
def initialize(user)
user ||= User.new
end
[...]
Теперь введем первое правило доступа, говорящее, что администратор имеет полный доступ везде:
модели / ability.rb
[...]
def initialize(user)
user ||= User.new
if user.role?(:admin)
can :manage, :all
end
end
[...]
can
:manage
:all
Проверка способностей
Давайте отобразим ссылку на главной странице, ведущую к списку проектов, и проверим, имеет ли пользователь надлежащий доступ:
просмотров / страниц / index.html.erb
<div class="page-header"><h1>Welcome!</h1></div>
<% if can? :index, Project %>
<%= link_to 'Projects', projects_path, class: 'btn btn-lg btn-primary' %>
<% end %>
Так can?
это метод проверки, имеет ли текущий пользователь разрешение на выполнение действия. :index
Project
Вы также можете предоставить объект вместо класса (пример мы увидим позже).
Существует также cannot?
метод, который, как вы, наверное, догадались, выполняет противоположную проверку can?
, Узнайте больше здесь .
К сожалению, ничто не мешает пользователю получить прямой доступ к странице проектов (например, «http: // localhost: 3000 / projects»). Следовательно, мы также должны обеспечить проверку авторизации внутри контроллера. Это легко:
projects_controller.rb
[...]
def index
@projects = Project.all
authorize! :index, @project
end
[...]
Пойдите и попробуйте получить доступ к этой странице непосредственно как не администратор. Приложение теперь выдаст ошибку, но это не очень удобно для пользователя. Нам нужно решить еще одну проблему: как спасти от ошибки «доступ запрещен»?
Спасение от ошибки «Отказано в доступе»
Rails предоставляет нам хороший метод rescue_from
ApplicationController
application_controller.rb
[...]
rescue_from CanCan::AccessDenied do |exception|
flash[:warning] = exception.message
redirect_to root_path
end
[...]
Таким образом, если CanCan::AccessDenied
Помимо message
action
:index
subject
Project
Вы можете вручную вызвать ошибку «Отказано в доступе» и указать свое сообщение, действие и тему:
raise CanCan::AccessDenied.new("You are not authorized to perform this action!", :custom_action, Project)
Попробуйте! Узнайте больше об обработке исключений здесь .
Добавляем больше способностей
Давайте добавим пару других действий контроллера для создания нового проекта и определим, кто может это делать:
projects_controller.rb
[...]
def new
@project = Project.new
authorize! :new, @project
end
def create
@project = Project.new(project_params)
if @project.save
flash[:success] = 'Project was saved!'
redirect_to root_path
else
render 'new'
end
authorize! :create, @project
end
private
def project_params
params.require(:project).permit(:title)
end
[...]
Виды:
просмотров / проекты / new.html.erb
<div class="page-header"><h1>New Project</h1></div>
<%= render 'form' %>
просмотров / проекты / _form.html.erb
<%= form_for @project do |f| %>
<div class="form-group">
<%= f.label :title %>
<%= f.text_field :title, class: 'form-control' %>
</div>
<%= f.submit 'Post', class: 'btn btn-primary' %>
<% end %>
Добавить новую ссылку в верхнее меню:
[...]
<ul class="nav navbar-nav">
<% if can? :create, Project %>
<li><%= link_to 'Add Project', new_project_path %></li>
<% end %>
</ul>
[...]
Как видите, я использую :create
:new
Теперь добавьте еще пару способностей:
модели / ability.rb
def initialize(user)
user ||= User.new
if user.role?(:admin)
can :manage, :all
elsif user.role?(:moderator)
can :create, Project
can :read, Project
elsif user.role?(:user)
can :read, Project
end
end
Подождите, что это значит :read
Как насчет :index
Похоже, что CanCanCan вводит некоторые псевдонимы действий по умолчанию:
alias_action :index, :show, :to => :read
alias_action :new, :to => :create
alias_action :edit, :to => :update
:read
:index
:show
:create
:new
Это действительно удобно, и вы можете легко определить свои собственные псевдонимы, используя тот же принцип:
alias_action :update, :destroy, :to => :modify
Узнайте больше здесь .
Наконец, давайте разберемся с действиями edit
update
destroy
projects_controller.rb
[...]
def edit
@project = Project.find(params[:id])
authorize! :edit, @project
end
def update
@project = Project.find(params[:id])
if @project.update_attributes(project_params)
flash[:success] = 'Project was updated!'
redirect_to root_path
else
render 'edit'
end
authorize! :update, @project
end
def destroy
@project = Project.find(params[:id])
if @project.destroy
flash[:success] = 'Project was destroyed!'
else
flash[:warning] = 'Cannot destroy this project...'
end
redirect_to root_path
authorize! :destroy, @project
end
[...]
Вид:
edit.html.erb
<div class="page-header"><h1>Edit Project</h1></div>
<%= render 'form' %>
Теперь добавьте две кнопки для редактирования и уничтожения проекта:
просмотров / проекты / index.html.erb
[...]
<% @projects.each do |project| %>
<div class="well well-sm">
<h2><%= project.title %></h2>
<% if can? :update, project %>
<%= link_to 'Edit', edit_project_path(project), class: 'btn btn-info' %>
<% end %>
<% if can? :destroy, project %>
<%= link_to 'Delete', project_path(project), class: 'btn btn-danger', method: :delete, data: {confirm: 'Are you sure?'} %>
<% end %>
</div>
<% end %>
Я передаю объект project
Project
Например, я могу добавить правило о том, что пользователь может редактировать только проект, который был добавлен менее 2 часов назад.
По способностям:
модели / ability.rb
if user.role?(:admin)
can :manage, :all
elsif user.role?(:moderator)
can [:create, :read, :update], Project
elsif user.role?(:user)
can :read, Project
end
Обратите внимание, что метод can
На самом деле, второй аргумент также может быть массивом:
can [:create, :read, :update], [Project, Task]
Мы можем переписать эту строку
can [:create, :read, :update], Project
по-другому, исключая некоторые разрешения:
can :manage, Project
cannot :destroy, Project
Это означает, что пользователь может делать все с проектами, но не может их уничтожать. Обратите внимание, что здесь важен порядок строк. Если вы cannot
любые действия над проектами. Вы можете прочитать больше о приоритете здесь .
Работа с дублированием кода
Тебе не кажется, что звонят, can
в действиях каждого контроллера это довольно утомительно? Более того, что если вы забудете включить его в какой-либо метод. CanCanCan справляется и с этим! Использование authorize!
projects_controller.rb
load_and_authorize_resource
На самом деле этот метод состоит из двух методов: class ProjectsController < ApplicationController
load_and_authorize_resource
[...]
endload_resource
authorize_resource
Вы можете позвонить им по отдельности, если вам нравится:
load_resource
authorize_resource
load_resource
authorize_resource
index
Но как ресурс загружается для разных действий?
- Для
Model.accessible_by(current_ability)
accessible_by
show
- Для
edit
update
destroy
find
Model.find(params[:id])
new
- Для
create
new
find
- Для пользовательских (не CRUD) действий ресурс будет загружен с использованием команды
class ProjectsController < ApplicationController
load_and_authorize_resource
[...]def update
if @project.update_attributes(project_params)
flash[:success] = 'Project was updated!'
redirect_to root_path
else
render 'edit'
end
enddef create
if @project.save
flash[:success] = 'Project was saved!'
redirect_to root_path
else
render 'new'
end
enddef destroy
if @project.destroy
flash[:success] = 'Project was destroyed!'
else
flash[:warning] = 'Cannot destroy this project...'
end
redirect_to root_path
end
end
Итак, наш контроллер может быть упрощен следующим образом:
projects_controller.rb
authorize!
Что изменилось?
- Я удалил
authorize_resource
вызовы метода, потому чтоindex
- Во-вторых, я полностью удалил действия
new
edit
load_resource
@project = Project.find(params[:id])
- В-третьих, я удалил
update
destroy
@project = Project.new(project_params)
create
load_resource
create_params
Да, я знаю, о чем ты думаешь. Как насчет сильных параметров? Как насчет сортировки и нумерации страниц? Что если я хочу пропустить загрузку и авторизацию ресурса для некоторых действий? Что делать, если мне нужен пользовательский искатель? Это замечательные вопросы, давайте обсудим их один за другим.
Сильные параметры . При инициализации ресурса CanCanCan проверяет, отвечает ли контроллер следующим методам:
-
update_params
create_params
CanCanCan собирается использовать один из этих методов для очистки входных данных в зависимости от текущего действия. Это круто, потому что вы можете определить различные правила очистки для создания и обновления. - Если метод
update_params
_params project_params
resource_params
- Наконец, CanCanCan будет искать метод со статическим именем
load_and_authorize_resource param_method: :my_sanitizer
- Вы также можете
[...]
before_action :load_projects, only: :index
load_and_authorize_resource[...]
private
def load_projects
@projects = Project.accessible_by(current_ability).order('created_at DESC')
end[...]
load_resource
- Если CanCanCan не смог найти ни один из этих методов в контроллере, и пользовательское дезинфицирующее средство не установлено, он будет инициализировать ресурс как обычно.
Переопределить загрузку ресурса . Например, я хочу, чтобы проекты на странице индекса сортировались по дате создания по убыванию. Вы можете сделать это легко:
projects_controller.rb
before_action
Идея состоит в том, что @projects
Пока я добавил load_resource
index
before_action
load_and_authorize_resource
Важно поместить load_projects
перед accessible_by
Обратите внимание, что внутри load_and_authorize_resource only: :index
# or
load_and_authorize_resource except: :indexload_resource
Пропуск загрузки и авторизация ресурса . Если по какой-то причине вы хотите пропустить эти действия, просто напишите:
find
Пользовательские искатели . Как мы уже видели, find_by
load_resource find_by: :title
Это легко изменить, предоставив параметр
authorize_resourceongoing
$ rails g migration add_ongoing_to_projects ongoing:boolean
CanCanCan действительно гибкий, и вы можете легко изменить его поведение по умолчанию! Полную информацию можно найти здесь .
Добавление условий к способностям
Теперь предположим, что каждый проект имеет [...]
Пользователи должны иметь доступ только к текущим проектам, а модераторы могут просматривать все проекты, но обновлять только текущие.
def change
add_column :projects, :ongoing, :boolean, default: true
add_index :projects, :ongoing
end
[...]
Прежде всего, добавьте новую миграцию:
$ rake db:migrate
Измените файл миграции:
xxx_add_ongoing_to_projects.rb
<%= form_for @project do |f| %>
<div class="form-group">
<%= f.label :title %>
<%= f.text_field :title, class: 'form-control' %>
</div>
<div class="form-group">
<%= f.label :ongoing %>
<%= f.check_box :ongoing %>
</div>
<%= f.submit 'Post', class: 'btn btn-primary' %>
<% end %>
Применить миграцию:
[...]
def project_params
params.require(:project).permit(:title, :ongoing)
end
[...]
Не забудьте обновить форму:
просмотров / проекты / _form.html.erb
[...]
if user.role?(:admin)
can :manage, :all
elsif user.role?(:moderator)
can :create, Project
can :update, Project do |project|
project.ongoing?
end
can :read, Project
elsif user.role?(:user)
can :read, Project, ongoing: true
end
[...]
И измените список разрешенных атрибутов:
projects_controller.rb
can
Теперь о способностях:
модели / ability.rb
accessible_by
user
Для хеша,
важно использовать только столбцы таблицы, потому что эти условия будут использоваться с методом SELECT "projects".* FROM "projects" WHERE "projects"."ongoing" = 't' ORDER BY created_at DESC
Узнайте больше здесь .
Теперь переключитесь на роль accessible_by
В консоли вы должны увидеть вывод, похожий на этот:
check_authorization
Это означает, что ApplicationController
файлеility.rb . Очень круто.
Принудительная авторизация
Если вы хотите проверить, что авторизация происходит на каждом контроллере, вы можете добавить class ApplicationController < ActionController::Base
check_authorization
[...]
endCanCan::AuthorizationNotPerformed
application_controller.rb
class PagesController < ApplicationController
skip_authorization_check
end
Если авторизация не выполняется в одном из действий, возникнет ошибка class SessionsController < ApplicationController
Тем не менее, мы хотим пропустить эту проверку для некоторых контроллеров, так как любой пользователь должен иметь доступ к главной странице и переключаться между ролями. Это легко:
skip_authorization_check
end
pages_controller.rb
skip_authorization_check
sessions_controller.rb
only
except
check_authorization
if
Кроме того, unless
check_authorization if: :admin_subdomain?
private
def admin_subdomain?
request.subdomain == "admin"
end
Узнайте больше здесь .
Вывод
В этой статье мы обсудили CanCanCan, отличное решение для авторизации в Rails. Я надеюсь, что теперь вы уверены в том, чтобы использовать его в своих проектах и реализовывать более сложные сценарии. Я настоятельно рекомендую вам просмотреть вики CanCanCan , так как она содержит действительно полезные примеры.
Спасибо за чтение! Как всегда, любой читатель имеет право прислать свой отзыв об этой статье :). Увидимся!