简洁的想法

仁爱、喜乐、和平、忍耐、恩慈、良善、信实、温柔、节制

MongoDB Scaffolding Devise Omniauth CanCan (2)

| Comments

1. Devise
2. Omniauth
3. CanCan

Devise

现在安装Devise, 和使用其它数据库一样, 首先在Gemfile加入devise, 然后安装Devise:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
$ vi Gemfile
...
gem 'devise','2.1.0'
...
$ bundle install
      create  config/initializers/devise.rb
      create  config/locales/devise.en.yml
===============================================================================

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

  1. Ensure you have defined default url options in your environments files. Here
     is an example of default_url_options appropriate for a development environment
     in config/environments/development.rb:

       config.action_mailer.default_url_options = { :host => 'localhost:3000' }

     In production, :host should be set to the actual host of your application.

  2. Ensure you have defined root_url to *something* in your config/routes.rb.
     For example:

       root :to => "home#index"

  3. Ensure you have flash messages in app/views/layouts/application.html.erb.
     For example:

       <p class="notice"><%= notice %></p>
       <p class="alert"><%= alert %></p>

  4. If you are deploying Rails 3.1 on Heroku, you may want to set:

       config.assets.initialize_on_precompile = false

     On config/application.rb forcing your application to not access the DB
     or load models when precompiling your assets.

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

Devise会知道我们已经安装了Mongoid, 所以它会在config/initializers/devise.rb中加入ORM的支持:

1
require 'devise/orm/mongoid'

下面建立User model

1
2
3
4
5
6
$ rails generate devise User
      invoke  mongoid
      create    app/models/user.rb
      insert    app/models/user.rb
      insert    app/models/user.rb
       route  devise_for :users

Devise做的工作在上面已经表现得很清楚了, 一方面在route中增加了devise_for :users,另一方面生成User Model,我们可以在app/models/user.rb中看到Devise已经使用Mongoid了:

app/models/user.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
class User
  include Mongoid::Document
  # Include default devise modules. Others available are:
  # :token_authenticatable, :confirmable,
  # :lockable, :timeoutable and :omniauthable
  devise :database_authenticatable, :registerable,
         :recoverable, :rememberable, :trackable, :validatable

  ## Database authenticatable
  field :email,              :type => String, :null => false, :default => ""
  field :encrypted_password, :type => String, :null => false, :default => ""

  ## Recoverable
  field :reset_password_token,   :type => String
  field :reset_password_sent_at, :type => Time

  ## Rememberable
  field :remember_created_at, :type => Time

  ## Trackable
  field :sign_in_count,      :type => Integer, :default => 0
  field :current_sign_in_at, :type => Time
  field :last_sign_in_at,    :type => Time
  field :current_sign_in_ip, :type => String
  field :last_sign_in_ip,    :type => String

  ## Confirmable
  # field :confirmation_token,   :type => String
  # field :confirmed_at,         :type => Time
  # field :confirmation_sent_at, :type => Time
  # field :unconfirmed_email,    :type => String # Only if using reconfirmable

  ## Lockable
  # field :failed_attempts, :type => Integer, :default => 0 # Only if lock strategy is :failed_attempts
  # field :unlock_token,    :type => String # Only if unlock strategy is :email or :both
  # field :locked_at,       :type => Time

  ## Token authenticatable
  # field :authentication_token, :type => String
end

Devise由12个Modules组成, 在上面的注释中就有体现, 下面我大致翻译一下各个Module的功用( 原文可以去看官方文档 ):

  1. Database Authenticatable: 用户验证和登录过程中, 把用户的密码加密然后把加密后的密码存在数据库中。
  2. Token Authenticatable: 基于验证令牌的用户登录方法。
  3. Omniauthable: 添加Omniauth支持。
  4. Confirmable: 通过发E-Mail的方式验证用户帐号是否通过通过认证。
  5. Recoverable: 通过发E-Mail的方式支持用户找回密码。
  6. Registerable: 处理用户注册过程,也可以让他们编辑和销毁他们的帐户。
  7. Rememberable: 通过Cookie来记住用户。
  8. Trackable: 跟踪用户的登录次数、时间和IP地址。
  9. Timeoutable: 假如用户一段时间没有活动, 自动用户处于不登录状态。
  10. Validatable: 用户通过E-Mail和密码登录, 可以自定义,比如使用用户名和密码登录。
  11. Lockable: 几次失败的登录尝试后,锁定用户, 可以在一段时间后通过E-Mail解锁。
  12. Encryptable: 除了内置的Bcrypt(默认),增加支持认证机制。

用E-Mail登录可能不是所有网站都想要的风格,如果想要用用户名登录,我们就可以加一个字段,就像上一篇所说的,如果用MySql或Sqlite之类的,还要rake migrate,但现在,只要添加这个字段就好了:

app/models/user.rb
1
2
3
...
field :user_name
...

用户表中有很多字段,我们希望在用户登录之前只有少量字段可以用来验证真实用户,所以attr_accessible就只包括用户名、邮件、密码和“记住我”,除此之外,用户名和邮件地址唯一性可以通过validates_uniqueness_of保障。

app/models/user.rb
1
2
3
validates_presence_of :user_name
validates_uniqueness_of :user_name, :email, :case_sensitive => false
attr_accessible :user_name, :email, :password, :password_confirmation, :remember_me

我们在User Model里面使用了Registerable, 这样我们就可以Devise来生成注册的Views:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
$ rails generate devise:views
      invoke  Devise::Generators::SharedViewsGenerator
      create    app/views/devise/shared
      create    app/views/devise/shared/_links.erb
      invoke  form_for
      create    app/views/devise/confirmations
      create    app/views/devise/confirmations/new.html.erb
      create    app/views/devise/passwords
      create    app/views/devise/passwords/edit.html.erb
      create    app/views/devise/passwords/new.html.erb
      create    app/views/devise/registrations
      create    app/views/devise/registrations/edit.html.erb
      create    app/views/devise/registrations/new.html.erb
      create    app/views/devise/sessions
      create    app/views/devise/sessions/new.html.erb
      create    app/views/devise/unlocks
      create    app/views/devise/unlocks/new.html.erb
      invoke  erb
      create    app/views/devise/mailer
      create    app/views/devise/mailer/confirmation_instructions.html.erb
      create    app/views/devise/mailer/reset_password_instructions.html.erb
      create    app/views/devise/mailer/unlock_instructions.html.erb

因为我们添加了user_name字段,所以相应地要在Views里面添加这个输入框:

app/views/devise/registrations/edit.html.erb
1
2
3
4
...
<div><%= f.label :user_name %><br />
<%= f.text_field :user_name %></div>
...
app/views/devise/registrations/new.html.erb
1
2
3
4
...
<div><%= f.label :user_name %><br />
<%= f.text_field :user_name %></div>
...

因为Devise已经集成了创建、编辑和删除用户的Controller,所以我们不用在自己的项目中添加任何代码。

为了在项目首页添加用户注册相关的链接,下面修改首页代码: 首先确定一下哪个是首页

config/routes.rb
1
2
3
...
root :to => 'articles#index'
...

然后再修改相应的View

app/views/articles/index.html.erb
1
2
3
4
5
6
7
8
9
<div id="user_nav">
  <% if user_signed_in? %>
    Signed in as <strong><%= current_user.user_name %></strong>. Not you?
    <%= link_to "Sign out", destroy_user_session_path, :method => :delete%>
  <% else %>
    <%= link_to "Sign up", new_user_registration_path %> or
    <%= link_to "Sign in", new_user_session_path %>
  <% end %>
</div>

把以下代码加入app/views/layouts/application.html.erb<body>下方

app/views/layouts/application.html.erb
1
2
<p class="notice"><%= notice %></p>
<p class="alert"><%= alert %></p>

到现在为止,Devise就可以正常动作了。如果还需要用户管理界面之类的,那就请大家自己接着开发吧。

Omniauth

下面继续为项目添加Omniauth。主要参考Omniauth-facebook

首先添加Omniauth到Gemfile,别忘了$ bundle install:

Gemfile
1
2
3
4
...
gem "omniauth", '1.1.0'
gem "omniauth-facebook", '1.3.0'
...

然后在Devise的初始化文件中定义facebook的一些参数:

config/initializers/devise.rb
1
2
3
4
5
6
7
8
...
# ==> OmniAuth
# Add a new OmniAuth provider. Check the wiki for more information on setting
# up on your models and hooks.
# config.omniauth :github, 'APP_ID', 'APP_SECRET', :scope => 'user,public_repo'
require "omniauth-facebook"
config.omniauth :facebook, "APP_ID", "APP_SECRET", :strategy_class => OmniAuth::Strategies::Facebook
...

其中”APP_ID”和”APP_SECRET”要去Facebook去申请。如果是本地测试,可以把Site URL设为http://localhost:3000/, Site Domain可设为localhost。

Devise 12个Modules之一的:omniauthable要从注释中解放出来了:

app/models/user.rb
1
devise :omniauthable

现在Devise已经在User Model中加入了两个方法:

1
2
user_omniauth_authorize_path(:facebook)
user_omniauth_callback_path(:facebook)

接下来把Sign in with Facebook的链接加到Sign upSign in的后面,这样点击这个链接就会把用户带到Facebook,如果用户成功登录Facebook,那Fackbook会把用户信息返回给开始设定好的Callback方法。

cat app/views/articles/index.html.erb
1
2
3
4
5
6
7
8
9
10
<div id="user_nav">
  <% if user_signed_in? %>
  Signed in as <strong><%= current_user.user_name %></strong>. Not you?
    <%= link_to "Sign out", destroy_user_session_path, :method => :delete%>
  <% else %>
    <%= link_to "Sign up", new_user_registration_path %> or
    <%= link_to "Sign in", new_user_session_path %>
      <%= link_to "Sign in with Facebook", user_omniauth_authorize_path(:facebook) %>
  <% end %>
</div>

Callback的方法在config/routes.rb中定义:

config/routes.rb
1
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks" }

既然定义了Callback是users/omniauth_callbacks,那就要建立相应的文件:

app/controllers/users/omniauth_callbacks_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Users::OmniauthCallbacksController < Devise::OmniauthCallbacksController
  def facebook
    # You need to implement the method below in your model
    @user = User.find_for_facebook_oauth(request.env["omniauth.auth"], current_user)

    if @user.persisted?
      flash[:notice] = I18n.t "devise.omniauth_callbacks.success", :kind => "Facebook"
      sign_in_and_redirect @user, :event => :authentication
    else
      session["devise.facebook_data"] = request.env["omniauth.auth"]
      redirect_to new_user_registration_url
    end
  end
end

从代码中不难发现,所有从Facebook中传来的信息都存在request.env[“omniauth.auth”], 从服务器日志来看,具体信息如下:

1
2
3
4
5
6
7
8
9
10
11
<Hashie::Mash email="zhangsan@gmail.com"
first_name="三"
gender="male"
id="100000531508888"
last_name="张"
link="http://www.facebook.com/profile.php?id=100000531508888"
locale="zh_CN"
name="张三"
timezone=2
updated_time="2011-06-01T20:26:05+0000"
verified=true>

上面这个Controller中的find_for_facebook_oauth还没有定义呢,把下面的代码添加到User Model里面吧:

app/models/user.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  def self.find_by_email(email)
    where(:email => email).first
  end

  def self.find_for_facebook_oauth(access_token, signed_in_resource=nil)
    data = access_token.extra.raw_info
    if user = self.find_by_email(data.email)
      user
    else # Create a user with a stub password.
      self.create!(:email => data.email, :password => Devise.friendly_token[0,20])
    end
  end

  def self.new_with_session(params, session)
    super.tap do |user|
      if data = session["devise.facebook_data"] && session["devise.facebook_data"]["extra"]["raw_info"]
        user.email = data["email"]
      end
    end
  end

好了,重启Web服务,测试一下用Facebook登录吧。

CanCan

现在进入最后一个主题CanCan,也就是权限系统。 第一件事照旧就是Gemfile和$ bundle install,CanCan从版本1.5开始就支持Mongoid了,但在Gemfile中要在CanCan之前包含Mongoid。

Gemfile
1
2
gem "mongoid", "~> 2.4"
gem 'cancan','1.6.7'

然后生成ability.rb文件。

1
2
$ rails g cancan:ability
      create  app/models/ability.rb

接下来参考这篇文章,在User Model中添加一个字段roles_mask, 然后把用户角色的定义加入User Model。

app/models/user.rb
1
2
field :roles_mask
ROLES = %w[admin moderator author]

下面的代码是为了给用户getting 和 setting角色的

app/models/user.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
def roles=(roles)
  self.roles_mask = (roles & ROLES).map { |r| 2**ROLES.index(r) }.sum
end

def roles
  ROLES.reject do |r|
    ((roles_mask || 0) & 2**ROLES.index(r)).zero?
  end
end

def role?(role)
  roles.include?(role.to_s)
end

因为项目集成了Devise,所以要把roles这个属性设置为accessible

app/models/user.rb
1
attr_accessible :user_name, :email, :password, :password_confirmation, :remember_me, :roles

application.html.erb 中可以加上如下代码,可以用来检查current_user

app/views/layouts/application.html.erb
1
2
3
4
5
6
7
8
<br />
<% if current_user != nil %>
user:<%= current_user.user_name %><br />
user_id:<%= current_user.id %><br />
user_role:<%= current_user.roles_mask %><br />
<% else %>
<%= "nil user" %>
<% end %>

因为前面Article Model没有和User Model里面的user_name联系起来,所以只能用Article里面引用关联 Reference Type Associations的Author Model中的author_id和User Model的user_name比较,如果一样,就让用户可以编辑文章。这只是个CanCan示例,真用起来千万别这么干。

app/models/ability.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Ability
  include CanCan::Ability

  def initialize(user)
    user ||= User.new # guest user (not logged in)
    if user.role? :admin
      can :manage, :all
    else
      can :read, :all
      can :create, Comment
      if user.role?(:author)
        can :create, Article
        can :update, Article do |article|
          article.try(:author_id) == user.user_name
        end
      end
    end
  end
end

最后在Article Controller加上一句就可以实现权限控制了

app/controllers/articles_controller.rb
1
2
3
4
class ArticlesController < ApplicationController
   load_and_authorize_resource
   ...
end

为了版面好看,在view中加入权限控制也很简单

app/views/articles/index.html.erb
1
2
3
4
5
6
<% if can? :update, article %>
  <td><%= link_to 'Edit', edit_article_path(article) %></td>
<% end %>
<% if can? :destroy, article %>
  <td><%= link_to 'Destroy', article, confirm: 'Are you sure?', method: :delete %></td>
<% end %>

至此,这个练习的小站就建好了,比较@_@的是 首先要在http://localhost:3000/authors/ 添加一个用户名,比如neten 然后在http://localhost:3000/articles/new 添加文章的时候选择neten作为Author 最后注册一个Role为Author的用户neten, 这样,neten登录的时候,在http://localhost:3000/看到的文章列表中,他自己发的文章都可以Show,也可以Edit,但不可以destroy,如果用户是admin,那所有的链接都是有效的。

这两篇blog只是自己的学习记录,希望对大家有所帮助,如果设计真实项目还要精细思考一下。

Comments