Oh!Coder

Coding Life

Devise Gem简介

| Comments

今天介绍的常用gem是关于用户认证管理的,名字叫Devise

简介

Devise是Rails中基于Warden演变而来的灵活的认证解决方案。它有如下特点:

  • 基于Rack;
  • 基于Rails engine,纯正的MVC解决方案;
  • 同一时间允许有多个model登入;
  • 基于模块化概念:只对所需模块可见。

由10个模块组成:

  • Database Authenticatable:在数据库中加密和存储密码,并在用户登陆时进行认证。认证可以通过POST请求或HTTP基础认证两种方式。
  • Omniauthable:添加OmniAuth支持。
  • Confirmable:向注册用户发送确认邮件,以此来确认当前用户是否已经登陆。
  • Recoverable:重置用户密码,并向用户发送重置引导。
  • Registerable:通过一个注册流程处理注册用户,允许它们编辑和删除账户。
  • Rememberable:管理登陆用户的cookie,生成和清除对应的token。
  • Trackable:跟踪登陆账户,时间戳以及IP地址。
  • Timeoutable:session的时间周期在特定的时间之后不再活跃。
  • Validatable:提供了email和password的认证方式。这些都是可定制的选项,所以你可以定制自己的认证方式。
  • Lockable:用户登陆失败特定次数后进行账户锁定。可以通过email或特定时间长度之后进行解锁。

Devise确保在YARV下线程安全。对于JRuby下线程安全的支持尚在进行中。

对于刚刚Rails起步的初学者,文档中给了一个小小的建议。如果你正在创建第一个Rails应用,不建议一开始就使用Devise。使用Devise需要对Rails框架有一个很好的理解。基于此,我们建议你使用一个简单的认证系统作为开始。如今,我们有三个资源应该能够帮助你作为开始:

一旦你了解了Rails框架以及认证机制,我们确保你使用Devise会非常的开心。

开始

Devise 3.0可以和Rails 3.2以后的版本很好的工作。你需要在Gemfile中添加如下这行:

1
gem 'devise'

运行bundle命令进行安装。

安装完毕之后执行generator命令:

1
rails generate devise:install

generator会安装初始化文件,并进行初始化的配置。完成这一步之后,就为使用Devise添加model做好了准备:

1
rails generate devise MODEL

使用任何你想用的类名替换MODEL(User使用的最频繁,但是也可以使用Admin)。执行完之后会使用Devise的默认配置创建一个model。generator还会在config/routes.rb 文件里进行配置,指向Devise的controller。

接下来核实一下MODEL,看是否需要添加其它可选配置,比如账户确认或账户锁定。如果你添加了可选项,一定要检查一下migration文件,撤销相应段落的注释。例如,如果你添加了账户确认可选项,你需要在migration文件中取消对Confirmable段落的注释。然后运行rake db:migrate

接下来,你需要在每一个开发环境中,为Devise的mailer设置默认的URL可选项。这里有一个在config/environment/development.rb下的可行的配置:

1
config.action_mailer.default_url_options = { host: 'localhost', port: 3000 }

配置完Devise的可选项之后,需要重启application。否则你会运行在一个无效的状态中,比如说,用户无法登陆以及route未定义。

Controller过滤和帮助方法

Devise将会创建一些帮助方法,在controller中可以使用。为了使用用户认证来设置controller,只需要添加如下这行(假定用devise创建的model是User):

1
before_action :authenticate_user!

如果你用devise创建的model名字为其它,使用_你的名字替换_user。对于下面提到的命令也是这个逻辑。

用户登陆认证,使用如下帮助方法:

1
user_signed_in?

对于当前登陆用户,如下帮助方法是可见的:

1
current_user

可以访问对应的session:

1
user_session

当一个用户登录后,证实当前账户或更新密码,Devise会查找一个root路径进行跳转。具体来说,当使用一个:user资源时,如果user_root_path存在,那么将会被使用;否则会使用默认的root_path。这也就意味着需要在route中设置root路径:

1
root to: "home#index"

你还可以覆盖after_sign_in_path_forafter_sign_out_path_for来自定义跳转回调。

注意,如果你用Devise定义的model叫Member而不是User。那么辅助方法将会变成如下形式:

1
2
3
4
5
6
7
before_action :authenticate_member!

member_signed_in?

current_member

member_session

配置Model

在用Devise生成的model中也可接受一些可选项配置。例如,可以选择如下加密算法:

1
devise :database_authenticatable, :registerable, :confirmable, :recoverable, stretches: 20

除了:stretches以外,你还可以从它们之间选择::pepper:encryptor:confirm_within:remember_for:timeout_in:unlock_in。更多更详细的内容,可以参见执行”devise:install”命令后生成的初始化配置文件上方的注释。这个文件通常位于/config/initializers/devise.rb

Strong Parameters

当你自定义view之后,你可能会为form添加一些新的字段。Rails 4把parameter的验证放到了controller,导致Devise也需要在controller中进行处理。

在Devise中有三个action需要对parameter的传递进行处理。它们的名字以及需要处理的参数默认为:

  • sign_inDevise::SessionsController#create)-只允许验证关键字(比如email
  • sign_upDevise::RegistrationsController#create)-只允许验证关键字外加passwordpassword_confirmation
  • account_updateDevise::RegistrationsController#update)-只允许验证关键字外加passwordpassword_confirmationcurrent_password

假如你想添加其它参数,可以在ApplicationController中使用一个简单的before filter进行自定义:

1
2
3
4
5
6
7
8
9
class ApplicationController < ActionController::Base
  before_action :configure_permitted_parameters, if: :devise_controller?

  protected

  def configure_permitted_parameters
    devise_parameter_sanitizer.for(:sign_up) << :username
  end
end

上面这种方法可以添加任何基本类型的参数。如果你想嵌套字段(例如你正在使用accepts_nested_attributes_for),那么你需要告诉devise这些嵌套和类型。Devise完全允许你对其默认配置进行更改或通过传递一个block对其行为进行自定义:

对于允许简单的username和email的基本类型,可以如下设置:

1
2
3
def configure_permitted_parameters
  devise_parameter_sanitizer.for(:sign_in) { |u| u.permit(:username, :email) }
end

如果在用户登陆的时候,你有一些可选项来确定用户角色,浏览器将会把选中的checkbox作为一个数组进行发送。这个数组不是Strong Parameter所允许的基本类型,那么我们需要在Devise中进行如下配置:

1
2
3
def configure_permitted_parameters
  devise_parameter_sanitizer.for(:sign_up) { |u| u.permit({ roles: [] }, :email, :password, :password_confirmation) }
end

对于验证基本类型的列表,这里声明了一些可被验证的嵌套hash和数组,详情可点击

如果你有多个用Devise创建的model,你可能想为不同的model设置不同的parameter。这种情况下,我们推荐你继承Devise::ParameterSanitizer并添加你自己的逻辑:

1
2
3
4
5
class User::ParameterSanitizer < Devise::ParameterSanitizer
  def sign_in
    default_params.permit(:username, :email)
  end
end

然后使用它对你的controller进行配置:

1
2
3
4
5
6
7
8
9
10
11
class ApplicationController < ActionController::Base
  protected

  def devise_parameter_sanitizer
    if resource_class == User
      User::ParameterSanitizer.new(User, :user, params)
    else
      super # Use the default one
    end
  end
end

上面的这段例子为user覆盖了:username:email这两个验证参数。非惰性(non-lazy)配置验证参数的方法应该定义在到自定义controller的before filter之上。我们会在接下来个别段落对如何配置和自定义controller进行详细描述。

配置View

我们创建了Devise,帮助你快速开发一个application的认证。然而,我们并不想让你用你自己的方式进行自定义。

既然Devise是一个engine,那么gem中包含了所有它的view。这些view将会帮助你开始,但是随着时间的推移,你可能希望做一些改变。如果确实是这样,你只需要借助下面的generator,devise将会拷贝所有的view到你的application:

1
rails generate devise:views

如果你有超过一个的model是用Devise创建的(例如UserAdmin),你将会注意到Devise对所有的model使用了相同的view。幸运的是,Devise提供了一个简单的方法来自定义view。所有你需要做的就是在config/initializers/devise.rb文件中对config.scoped_views = true进行设置。

这么做之后,你将能够拥有基于角色的view,比如users/sessions/newadmin/sessions/new。如果在对应的位置没有找到view,Devise将会使用位于devise/sessions/new的默认的view。你还可以使用generator来生成相应的view:

1
rails generate devise:views users

如果你只是想生成特定的view,比如对应于registerableconfirmable模块的view,你可以向generator传入一个模块的列表名,并在命令紧跟后面加一个-v标示。

1
rails generate devise:views -v registrations confirmations

配置controller

如果在view层面的自定义不能够满足需求,按照如下步骤可以对每一个controller进行自定义:

  1. 使用generator创建自定义的controller,命令行后面要加一个scope:
1
rails generate devise:controllers [scope]

如果你指定users作为scope,controller将会在app/controllers/users/下进行创建。然后名为sessions的controller看起来如下:

1
2
3
4
5
6
7
class Users::SessionsController < Devise::SessionsController
  # GET /resource/sign_in
  # def new
  #   super
  # end
  ...
end
  1. 告诉route使用这个controller:
1
devise_for :users, controllers: { sessions: "users/sessions" }
  1. devise/sessions下拷贝view到users/sessions。既然controller改变了,它也将不会使用位于devise/sessions下的view。
  2. 最后,更改或扩展想要改变的controller下的action:

你可以完整的覆盖对应的action:

1
2
3
4
5
class Users::SessionsController < Devise::SessionsController
  def create
    # custom sign-in code
  end
end

或者你可以简单的添加一个新的行为:

1
2
3
4
5
6
7
class Users::SessionsController < Devise::SessionsController
  def create
    super do |resource|
      BackgroundWorker.trigger(resource)
    end
  end
end

这对于触发后台任务或当特定action的事件触发时打log非常有帮助。

记住,Devise使用flash信息告诉用户登陆是否成功。Devise希望你能够在应用中调用flash[:notice]flash[:alert]。不要打印出整个flash的hash信息,只打印特定key对应的值。在一些状况下,Devise向flash的hash中添加了一个[:timedout]key,添加这个key意味着过期无法显示。如果你确实想要打印整个hash,那么就把整个key从hash中移除掉。

配置route

Devise还附带有默认的route。如果你需要进行自定义,多半应该能够通过devise_for方法来完成。它接受了几个可选项,比如:class_name:path_prefix等等,甚至包括为I18n更改路径名称:

1
devise_for :users, path: "auth", path_names: { sign_in: 'login', sign_out: 'logout', password: 'secret', confirmation: 'verification', unlock: 'unblock', registration: 'register', sign_up: 'cmon_let_me_in' }

如果你有更多深度定制的需要,对一个实例来说,除了”/users/sign_in”以外,还要允许”/sign_in”路径,所有满足这些需求所要做的,只是在route中创建并用block封装到devise_scope中:

1
2
3
devise_scope :user do
  get "sign_in", to: "devise/sessions#new"
end

使用这种方式,即可告诉Devise,当用户访问”/sign_in”成功之后,将会使用:user这个scope。注意devise_scope在route中依然是as的别名。

I18n

Devise在flash信息中使用了I18n,结合了flash中名为:notice:alert关键的消息。为了能够进行自定义你的app,需要设置本地文件:

1
2
3
4
en:
  devise:
    sessions:
      signed_in: 'Signed in successfully.'

基于resource你可以使用给定route的名字单数创建一些精确的消息:

1
2
3
4
5
6
7
en:
  devise:
    sessions:
      user:
        signed_in: 'Welcome user, you are signed in.'
      admin:
        signed_in: 'Hello admin!'

Devise的mailer使用类似的模式来创建主题信息:

1
2
3
4
5
6
7
8
en:
  devise:
    mailer:
      confirmation_instructions:
        subject: 'Hello everybody!'
        user_subject: 'Hello User! Please confirm your email'
      reset_password_instructions:
        subject: 'Reset instructions'

看一下我们提供的本地文件,查看下其中可用的消息。你也许会在我们的wiki上找到部分有用的翻译内容,查看wiki

Test帮助方法

Devise包括一些辅助性的功能测试方法。为了能够使用它们,需要在功能测试中,通过在test/test_helper.rb文件底部包含Devise:

1
2
3
class ActionController::TestCase
  include Devise::TestHelpers
end

如果你正在使用RSpec,可以把下面这段代码放入名为spec/support/devise.rb文件或是你的spec/spec_helper.rb(或者如果你正在使用rspec-rails,可以放到spec/rails_helper.rb文件中):

1
2
3
RSpec.configure do |config|
  config.include Devise::TestHelpers, type: :controller
end

但是要确保要直接放在require 'rspec/rails'之后。

现在你已经准备好使用sign_insign_out方法了。这些方法在controller中有相同的方法签名:

1
2
3
4
5
sign_in :user, @user   # sign_in(scope, resource)
sign_in @user          # sign_in(resource)

sign_out :user         # sign_out(scope)
sign_out @user         # sign_out(resource)

有两件重要的事情需要记住:

  1. 这些帮助方法不能整合进被Capybara或Webrat驱动的测试中。这也就意味着只能用于功能性的单元测试。相应的,要在session中填满form或精确的设置user;

  2. 如果你正在测试Devise内部的controller或者一个继承自Devise的controller,你需要告诉Devise在请求之前应该使用哪一个map。这是必须的,因为Devise是从route中得到这个信息的,但是既然功能测试并不通过route进行传递,它需要进行显示声明。举例来说,如果你正在测试user,简单的使用:

1
2
@request.env["devise.mapping"] = Devise.mappings[:user]
get :new

关于更多针对Rails 3-Rails 4的controller,使用RSpec进行测试的内容,可以参考相应wiki

OmniAuth

Devise来自于OmniAuth以及其它provider的支持。使用这个功能,可以简单的在config/initializers/devise.rb文件中对OmniAuth进行配置:

1
config.omniauth :github, 'APP_ID', 'APP_SECRET', scope: 'user,public_repo'

关于OmniAuth的支持,可参见相应的wiki

配置多个model

Devise允许你配置任意多的model。如果你想要一个只做认证和有过期时间特性的名为Admin的model,只需要在名为User的model上面添加如下代码,然后运行就可以:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Create a migration with the required fields
create_table :admins do |t|
  t.string :email
  t.string :encrypted_password
  t.timestamps null: false
end

# Inside your Admin model
devise :database_authenticatable, :timeoutable

# Inside your routes
devise_for :admins

# Inside your protected controller
before_filter :authenticate_admin!

# Inside your controllers and views
admin_signed_in?
current_admin
admin_session

或者,你还可以简单的运行Devise的generator。

记住这些model将会有不同的route。它们不能也无法共用相同的controller来做登入,登出等等功能。为了防止不同的角色来共享相同的action,我们推荐你创建一个角色列,来做基于角色的管理,或者干脆用一个相应的gem来做认证。

ActiveJob集成

如果你正在使用Rails 4.2和ActiveJob在后台分发ActionMailer消息,那么你可以通过在model中覆盖send_devise_notification方法,通过已经存在的队列发送Devise邮件。

1
2
3
def send_devise_notification(notification, *args)
  devise_mailer.send(notification, self, *args).deliver_later
end

密码重设token以及Rails的log

如果你开启了Recoverable模块,注意一个被盗的密码重设可能会让一个攻击者访问你的应用。Devise努力生成一个随机的,安全的token,只在数据库中存储token的摘要,永远不存储明文。然而Rails中默认的log行为会把明文token泄露在log文件中:

  1. 在DEBUG层次,Action Mailer的log记录了发送email的全部内容。密码重置的token在发送给用户的email中将会泄漏。
  2. 在INFO层次,Active Job记录了每个队列任务中的所有参数。如果你配置Devise使用deliver_later发送密码重置邮件,密码重置token将会被泄漏。

Rails对于production的log层次默认设置为DEBUG。如果你想避免token的消息泄漏到log中,考虑改变production的log层次为WARN。在config/environments/production.rb文件中:

1
config.log_leverl = :warn

其它ORM

Devise支持ActiveRecord(默认)和Mongoid。选择其它ORM,简单的在初始化文件中进行配置。

更多信息

更多信息可参考原文档

Comments