如何简单快速的完成用户角色管理和权限控制

2017-11-02 12:55:26来源:http://www.ibm.com/developerworks/cn/web/wa-lo-how-implement作者:IBM developerWorks中国人点击

分享
简介

Rails开发者在程序开发过程中, 经常需要做的就是为Web应用程序编写一个用户认证模块,同时还需要针对不同角色的用户给予不同的控制权限,对于初级开发者来说,自己编写比较困难且后期维护也较麻烦。Devise是Rails的一个权限认证组件,使用该组件可以在无编码的情况下快速生成一个带有登录、注册、权限认证和重置密码的用户认证模块。CanCan是Rails的一个权限管理组件,利用该组件可以根据不同角色限定用户能够访问的资源,所有的权限都在ablity类中定义,而不需要在controllers、views、数据库中复制。Rolify可以轻松的和Devise、CanCan结合起来,成为一套分角色的权限管理组件。


本文将会引导读者创建一个Rails项目并利用devise+rolify+cancan完成该项目的用户角色管理和资源权限控制功能。作者使用的是macOs Sierra,不同的系统在环境准备方面会有所差异,读者需要根据自己的系统来搭建开发环境。


环境准备

在macOs下搭建ruby开发环境步骤十分简单,只需要安装ruby环境后用gem安装rails即可。安装ruby有多种方式,本文采用rvm来管理和安装ruby。rvm 全称 Ruby Version Manager,顾名思义,它是用来做ruby版本管理的,利用rvm我们可以安装多个版本的ruby,在使用时可以指定版本,十分简便。


安装rvm

$ curl –sSL https://get.rvm.io | bash –s stable


安装指定版本的ruby并选择使用

$ rvm install 2.2.5


$ rvm use 2.2.5


修改gem源

由于ruby默认的gem源在国外,中国的开发者访问该源有可能会连不上,所以我们可以使用ruby-china的镜像源。


$ sudo gem update --system


$ gem sources --remove https://rubygems.org/


$ gem sources –a https://gems.ruby-china.org/


安装rails

$ sudo gem install rails


根据个人需求安装数据库,本文不另作介绍。
创建Rails程序
用 rails new 命令创建一个新程序

$ rails new DeveloperWork


用 rails generate scaffold 命令创建一个新资源

$ rails generate scaffold paper name:text content:text category:text owner:text


显示rails为资源分配的routes地址

$ rake routes | grep paper


清单1. rails为papers分配的routes

shirleydembp:DeveloperWork shirley$ rake routes | grep paper


papersGET /papers(.:format)papers#index
POST /papers(.:format)papers#create
new_paperGET /papers/new(.:format)papers#new
edit_paperGET /papers/:id/edit(.:format) papers#edit
paper GET /papers/:id(.:format)papers#show
PATCH /papers/:id(.:format)papers#update
PUT /papers/:id(.:format)papers#update
DELETE /papers/:id(.:format)papers#destroy
DB Migration

$ bin/rails db:migrate RAILS_ENV=development


为rails程序设置首页

编辑程序的config/routes.rb,添加程序首页的routes


清单2. routes.rb中的配置
Rails.application.routes.draw do
resources :papers
# Details to see http://guides.rubyonrails.org/routing.html
# get '/' => 'papers#index'

root 'papers#index'


end


为程序引入bootstrap,美化页面

在Gemfile中引入twitter-bootstrap-rails

gem twitter-bootstrap-rails


运行bundle install

$ bundle install


生成bootstrap的assets

$ rails generate bootstrap:install


创建布局文件

$ rails g bootstrap:layout


创建视图文件/重写视图文件

$ rails g bootstrap:themed papers


启动rails server,访问程序首页

$ rai ls server –h <hostIP> -p <port>


图1. 程序首页


安装 devise, rolify, cancan
编辑程序的Gemfile,添加devise,rolify,cancan的gem包
清单3. Gemfile中引入相关gem包
gem 'devise'
gem 'rolify'
gem 'cancan'
运行bundle命令进行安装

$ bundle install


运行devise generate来完成devise的安装

$ rails generate devise:install


清单4. 运行devise generate
shirleydembp:DeveloperWork shirley$ rails generate devise:install
Running via Spring preloader in process 60215
createconfig/initializers/devise.rb
create config/locales/devise.en.yml

==========================================================================


Some setup you must do manually if you haven't yet:


Ensure you have defined default url options in your environments files. Here is an example of default_url_options appropriate for a developme environment in config/environments/development.rb: config.action_mailer.default_url_options = { host: 'localhost', port: 3000 } In production, :host should be set to the actual host of your application.
Ensure you have defined root_url to *something* in your config/routes.rb. For example: root to: "home#index"
Ensure you have flash messages in app/views/layouts/application.html.erb. For example: <p class="notice"><%= notice %></p> <p class="alert"><%= alert %></p>
You can copy Devise views (for customization) to your app by running: rails g devise:views

需要针对你的项目,根据提示来进行相应的配置。如果你想要自定义devise的页面,需要先生成页面文件,然后进行重写,关于devise的定制化在本文的如何定制devise章节会介绍。


devise, rolify, cancan: 创建User, Roles, Ability模型

运行generate命令来生成User, Roles, Ability模型,我们可以看到在models文件夹下出现了user.rb,role.rb和ability.rb三个文件,在这些model类中都引入了需要的模块。在db/migrate下也出现了各model的migration文件。


清单5. 创建User, Roles, Ability模型

shirleydembp:DeveloperWork shirley$ rails generate devise User


Running via Spring preloader in process 60557


invoke active_record


createdb/migrate/20170906125504_devise_create_users.rb
create app/models/user.rb
invoke test_unit
create test/models/user_test.rb
create test/fixtures/users.yml
insert app/models/user.rb
route devise_for :users
shirleydembp:DeveloperWork shirley$ rails generate cancan:ability
Running via Spring preloader in process 60574
create app/models/ability.rb
shirleydembp:DeveloperWork shirley$ rails generate rolify Role User
Running via Spring preloader in process 60578
invoke active_record
create app/models/role.rb
invoke test_unit
create test/models/role_test.rb
create test/fixtures/roles.yml
insert app/models/role.rb
create db/migrate/20170906125636_rolify_create_roles.rb
insert app/models/user.rb
create config/initializers/rolify.rb

在真正的项目中,我们的用户角色往往并不是那么简单。可以打开db/migrate下面的migration文件进行编辑,往表中加入你想要的字段。


编辑完migration之后,我们需要执行DB migrate命令使其生效。


$ rake db:migrate


现在再让我们来查看一下rails程序为user分配的routes。


清单6. rails为users分配的routes

shirleydembp:DeveloperWork shirley$ rake routes | grep users


new_user_session GET/users/sign_in(.:format) devise/sessions#new
user_session POST/users/sign_in(.:format)devise/sessions#create
destroy_user_session DELETE/users/sign_out(.:format) devise/sessions#destroy
new_user_password GET/users/password/new(.:format)devise/passwords#new
edit_user_password GET/users/password/edit(.:format) devise/passwords#edit
user_password PATCH/users/password(.:format)devise/passwords#update
PUT/users/password(.:format)devise/passwords#update
POST /users/password(.:format)devise/passwords#create
cancel_user_registration GET/users/cancel(.:format)devise/registrations#cancel
new_user_registration GET/users/sign_up(.:format) devise/registrations#new
edit_user_registration GET/users/edit(.:format)devise/registrations#edit
user_registration PATCH/users(.:format) devise/registrations#update
PUT/users(.:format) devise/registrations#update
DELETE/users(.:format)devise/registrations#destroy
POST/users(.:format)devise/registrations#create

devise为我们提供了一整套齐全的登录注册修改密码功能。在config/routes中我们设置当用户登录后才能访问主页,不然则跳到devise的登录页面。


清单7. 设置系统的登录首页
devise_scope :user do
authenticated :user do
root 'papers#index', as: :authenticated_root
end
unauthenticated do
root 'devise/sessions#new', as: :unauthenticated_root
end
end

运行程序,就可以看到devise的登录页面,点击Sign up注册一个新用户。


图2. devise 登录界面


devise功能模块简介

打开model/user.rb,你会看到devise为该模型默认添加的模块,可以按需选择。


清单8. user.rb

class User < ApplicationRecord


rolify


# Include default devise modules. Others available are:

# :confirmable, :lockable, :timeoutable and :omniauthable


devise :database_authenticatable, :registerable,


:recoverable, :rememberable, :trackable, :validatable


end


devise功能模块:


Database Authenticatable: 登录时加密密码并在数据库中验证用户真实性。
Omniauthable: 添加Omniauth (github.com/intridea/omniauth) 支持。
Confirmable : 发送验证邮件,在登录时检查账户是否已经验证。
Recoverable : 重置用户密码并发送重制指令。
Rememberable : 管理产生和清除表示来自用户保存的cookie的token。
Trackable : 追踪登录的次数、时间戳和IP地址。
Timeoutable : 超时重新登录。
Validatable : 提供的电子邮件及密码鉴定。该功能可定制。
Encryptable: 除了内置的Bcrypt(默认),增加支持认证机制。
Lockable: 锁定一定数量的失败尝试登录。通过电子邮件验证后解锁。
Registerable : 处理用户注册过程,也可以让他们编辑和注销他们的帐户。
Token Authenticatable: 基于token的用户登录(也被称为 "single access token")。token可以通过查询字符串或HTTP基本身份认证。
如何定制devise

devise可以帮助你快速构建应用程序,同时也支持定制化。实际上devise是一个引擎,它的所有views和controllers都被打包在gem中,你只需要用generate命令,这些文件就会自动拷贝到你的项目中。你只需要重写这些文件,就可以实现devise的定制化。


另外需要配置config/initializers/devise.rb。


$ rails generate devise:views users


$ rails generate devise:controllers users


清单8. generate生成devise文件
shirleydembp:DeveloperWork shirley$ rails generate devise:views users
Running via Spring preloader in process 61411
invoke Devise::Generators::SharedViewsGenerator
create app/views/users/shared
create app/views/users/shared/_links.html.erb
invoke form_for
create app/views/users/confirmations
create app/views/users/confirmations/new.html.erb
create app/views/users/passwords
create app/views/users/passwords/edit.html.erb
create app/views/users/passwords/new.html.erb
create app/views/users/registrations
create app/views/users/registrations/edit.html.erb
create app/views/users/registrations/new.html.erb
create app/views/users/sessions
create app/views/users/sessions/new.html.erb
create app/views/users/unlocks
create app/views/users/unlocks/new.html.erb
invoke erb
create app/views/users/mailer
create app/views/users/mailer/confirmation_instructions.html.erb
create app/views/users/mailer/email_changed.html.erb
create app/views/users/mailer/password_change.html.erb
create app/views/users/mailer/reset_password_instructions.html.erb
create app/views/users/mailer/unlock_instructions.html.erb
清单9. 定制化views/users/sessions/new.html.erb
<h2>Log in Custom</h2>
<%= form_for(resource, as: resource_name, url:
session_path(resource_name)) do |f| %>
<div class="field"
<%= f.label :email %><br />

<%= f.email_field :email, :class => "form-control input-sm", :autofocus => true %>
</div>
<div class="field">
<%= f.label :password %><br />
<%= f.password_field :password, :class => "form-control input-sm",
autocomplete: "off" %>
</div>
<% if devise_mapping.rememberable? -%>
<div class="field">
<%= f.check_box :remember_me %>
<%= f.label :remember_me %>
</div>
<% end -%>
<div class="actions">
<%= f.submit "Log in", :class => "btn btn-sm btn-info" %>
</div>
<% end %>
<%= render "users/shared/links" %>

注意,还需要在config/initializers/devise.rb中配置config.scoped_views = true。


图3. 定制化登录页面


利用devise的sign_in 方法登录已认证的用户

如果你的程序已经完成了登录模块,那么在不使用devise提供的登录模块的基础上,你依旧可以使用devise来记录当前用户。devise提供了SignInOut模块,我们可以通过调用其中的方法来直接进行用户的登录和登出。使用sign_in登录用户后,我们就可以用current_user来获取当前登录的用户。


# sign_in(resource_or_scope, *args) ⇒ Object


该方法是用来登录一个已经通过验证的方法。一般情况下,我们会在用户注册之后调用该方法,来进行注册用户的首次登录。所有给sign_in的选项都将传递给warden的set_user方法。


sign_in :user, @user # sign_in(scope, resource)


sign_in @user # sign_in(resource)

sign_in @user, event: :authentication # sign_in(resource, options)


sign_in @user, store: false # sign_in(resource, options)


#sign_out(resource_or_scope = nil) ⇒ Object


该方法是用来登出指定用户或范围。一般情况下,我们会在删除用户之后调用该方法,用来登出被删除的用户。如果成功登出,会返回true。如果在指定范围没有该用户,则登出失败,会返回false。


sign_out :user # sign_out(scope)


sign_out @user # sign_out(resource)


判断用户是否登录

我们可以在controller中判断用户是否登录,devise提供了相当简便的方法,我们只需要在controller的before_action中添加authenticate_user!即可。


清单10. 判断用户是否登录
class PapersController < ApplicationController

before_action :authenticate_user!


before_action :set_paper, only: [:show, :edit, :update, :destroy]


def index


@papers = Paper.all


end


……


end


现在我们再访问/papers,controller就会先判断我们是否已经登录,未登录的话就会转到登录界面,需要登录才能访问资源。


图4. 未登录访问资源


已有用户模型的程序集成devise

如果你的rails程序已经创建了用户模型,又不想重新创建,devise可以帮你重写模型,你在用devise的生成器创建用户模型的时候,只要指定你的用户模型的名字即可。假设rails程序已经创建了用户模型Buser。


$ rails generate devise Buser


$ rails generate rolify Role Buser


由于自己创建的用户模型名字不是User,所以会导致登录后current_user不存在。这个问题其实是因为名字引起的,登录后获取当前用户应该是current_<用户模型名>,以buser举例,现在登录后的当前用户就是current_buser。


CanCan对你的应用程序做出两个假设:一个是假设你有一个类定义权限的能力,另一个是假设你有一个current_user方法的控制器返回当前用户模型。你可以覆盖这两个定义current_ability的方法。现在我们的当前用户是用current_buse获取,所以我们也必须重写cancan的默认方法。


重写的方法放在ApplicationController.rb中。


清单11. 重写cancan默认方法

def current_ability


@current_ability ||= Ability.new(current_buser)


end


The Ability class and current_user method can easily be changed to something else.


# in ApplicationController


def current_ability


@current_ability ||= AccountAbility.new(current_account)


end


利用rolify为用户添加角色

在创建用户之后,如果没有为用户创建角色,我们需要为用户分配一个默认角色。这个方法可以声明为user模型的after_create。


清单11. 为用户添加默认角色
class User < ApplicationRecord

after_create :assign_default_role


rolify


# Include default devise modules. Others available are:


# :confirmable, :lockable, :timeoutable and :omniauthable


devise :database_authenticatable, :registerable,


:recoverable, :rememberable, :trackable, :validatable

def assign_default_role


self.add_role(:author) if self.roles.blank?


end


end


然后在管理用户的时候,我们就可以调用一下方法来为用户分配角色。


注意后面的:admin就是角色名称,但是要用符号(Symbol)类型,不可以用字符串。


给用户添加角色:

@ user.add_role :admin


删除用户的角色:

@ user.remove_role :admin


搜索用户的角色:

user.has_role? :admin


在Ability.rb中定义每种角色的权限

cancan+rolify对权限的控制是基于角色的,且是针对资源的访问进行控制,具有相同角色的用户就有相同的权限,这带给我们非常大的便利。对于资源的访问权限,又分为许多种,基础的针对RESTful method有以下六种。


:manage 指这个 controller内所有的 action
:read 指这个controller内的 :index 和 :show
:update 指这个controller内的 :edit 和 :update
:destroy 指这个controller内的 :destroy
:create 指这个controller内的 :new 和 :crate
:al l 指这个controller内所有的 object (resource)

其他非RESTful的method(比如:search)也可以列上去,但是需要逐条列出来。


如果上面的基础权限无法满足你,你也可以使用alias_action来自定义权限组合。


假设要定义名为:modify的权限组合,包含 :update和 :destroy,代码如下:


alias_action :update, :destroy, :to => :modify


对于每种角色的权限,是在Ability.rb中进行定义的,定义方法非常简单,在can后面传入访问权限和指定资源即可,代码如下所示。


清单12. 在Ability.rb中定义每种角色的权限
class Ability

include CanCan::Ability


def initialize(user)


user ||= User.new
# 如果用户角色是admin,给完整权限,可操作所有资源

if user.has_role? :admin


can :manage, :all
# 如果用户角色是author,可以对Paper进行read, edit, update操作
elsif user.has_role? :author

can :read, Paper


can :update, Paper


# 否则只能够对Paper进行read操作

else


can :read, Paper


end


end


end


权限定义

在实际项目中,我们对于权限的定义较为复杂,比如某些情况下我们需要定义,用户只能修改他自己创建的资源。那么对于资源权限的定义,也就变得不那么简单,一般情况下,定义有以下几种,每种定义之间略有差异。


基础权限定义

基础权限定义就是在cancan原有的基础权限基础上进行定义,这种定义方式比较简单,示例代码为 清单12. 在Ability.rb中定义每种角色的权限 ,本章不再重复叙述。


自定义权限集合

自定义权限集合是基于用alias_action声明的自定义权限集合,示例代码如下:


清单13. 自定义权限集合

def initialize(user)


user ||= User.new


alias_action :create, :read, :update, :destroy, to: :crud


can :crud, Paper

end


利用hash条件筛选定义权限

对于需要多条件筛选的资源权限定义,我们可以利用hash条件来进行,示例代码如下:


清单14. 利用hash条件筛选定义权限

def initialize(user)


user ||= User.new


# 用户只能对owner是他们自己的 Paper资源进行read操作
can :read, Paper, owner: user.email

end


利用代码块定义权限

当hash条件筛选仍然满足不了需求的时候,我们就可以利用代码块来完成权限的定义,示例代码如下:


清单15. 利用代码块定义权限

def initialize(user)


user ||= User.new


# 用户只能对content长度小于500且内容不为空的Paper的资源进行 eidt 和 update 操作

can :update, Paper do |paper|


paper.content.length < 500 && paper.content != nil


end


end


在RESTful controller中进行权限控制

对于RESTful的controller,我们可以利用cancan提供的authorize_resource 或load_and_authorize_resource来做整个controller的权限控制,但也可以逐个在方法中利用authorize!来做权限控制。关于authorize!的使用将会在下一章 在Non-RESTful controller中进行权限控制 中进行介绍。


如果controller和resource的名字相同,那么就不需要为controller指定resource,cancan会自动将它们关联起来。load_and_authorize_resource方法会为controller中的每个方法都自动加载一个实例,并对这个controller作权限控制。而authorize_resource则不会自动加载实例。load_and_authorize_resource 其实就相当于是authorize_resource和load_resource的组合。


清单16. 在有同名资源的RESTful controller中进行权限控制
class PapersController < ActionController::Base

load_and_authorize_resource


end


class PapersController < ActionController::Base


load_resource


authorize_resource


end


但是如果controller没有同名的resource,那么我们就需要指定需要加载的资源类。


清单17. 在无同名资源的RESTful controller中进行权限控制
class LogController < ApplicationControlle

load_and_authorize_resource :class => "Papers"


end


在Non-RESTful controller中进行权限控制

由于Non-RESTful controller没有资源需要加载,所以我们就不可以用load_and_authorize_resource进行控制,我们需要在这个controller中的每一个方法中进行手动的权限控制。cancan提供了authorize!方法。


首先不要忘记在Ability.rb中定义好角色的权限。


注意authorize!后面的两个参数都必须是ruby的符号对象(Symbol)。


清单18. 在Non-RESTful controller中进行权限控制
class Ability
include CanCan::Ability
def initialize(user)user ||= User.new
if user.has_role? :admin
authorize! :info, :logs
end
end
end
class LogController < ActionController::Base def info_logsauthorize! :info, :logs

# roll the logs here end
end
在页面上进行权限控制

当定义好所有权限,我们在页面上也需要做权限控制,cancan提供了can?方法来判断用户的权限。假设我们在Ability.rb中对权限的定义为can :edit, Paper, owner: user.email,那么我们在页面上想对Paper的owner展示edit按钮,可以在页面上作如下控制。


清单19. 在页面上进行权限控制
<% if can? :edit, @Paper %>
<%= link_to "Edit Paper", edit_paper_path %>
<% end %>
参考资源 (resources)
参考 github: devise ,查看devise的用法。
参考 github: cancan ,查看cancan的用法。
参考 github: rolify ,查看rolify的用法。
参考 Devise CanCanCan rolify Tutorial ,查看devise,rolify,cancan的整合。
参考 Module: Devise::Controllers::SignInOut ,查看devise的SignInOut用法。
参考 Authorizing controller actions ,查看cancan的权限控制用法。
参考 cancan: Changing Defaults ,查看如何重写cancan的默认方法。
参考 cancan: Defining Abilities ,查看cancan的权限定义。

最新文章

123

最新摄影

闪念基因

微信扫一扫

第七城市微信公众平台