Rails开发:购物车(5)

第11章 管理

再加上一个基本的用户管理系统,当进入站点管理功能时要求用户先登录。只需要根据用户名和密码识别用户即可。只要通过用户识别,该用户就可以使用所有的管理功能。

 

添加用户

不能直接以明文形式保存密码,而是要首先对其进行SHA1 加密,然后保存一个160 位的散列码。当用户再次登录时,我们会对他输入的密码做同样的加密处理,并将加密的结果与数据库中保存的散列码进行比较。为了让系统更加安全,我们还对密码做了salt 处理:当生成散列值时将密码与一个伪随机字符串组合之后再生成散列码

 

ruby script/generate scaffold user name:string hashed_password:string salt:string

 

rake db:migrate

 

class User < ActiveRecord::Base
 validates_presence_of :name
 validates_uniqueness_of :name
 attr_accessor :password_confirmation
 validates_confirmation_of :password
 validate :password_non_blank
 
private
 def password_non_blank
  errors.add(:password, "Missing password" ) if hashed_password.blank?
 end
end

 

神秘的validates_confirmation_of声明:


你肯定用过这样的表单:你要首先输入一遍密码,然后在另一个输入框中再输入一遍密码,以确保你所输入的正是你想输入的。Rails 可以自动地校验这两遍输入的密码是否相同。

 

最后,我们用一个校验钩子来检查密码已经被设上了值,但我们不会直接检查password 属性。为什么?因为这个属性并不真的存在——至少不存在于数据库里。所以我们需要检查它的替身——也就是经过散列加密的密码——存在。

 

depot_p/app/models/user.rb

private

 def self.encrypted_password(password, salt)
  string_to_hash = password + "wibble" + salt
  Digest::SHA1.hexdigest(string_to_hash)
 end
此外还必须引用digest/sha1库

 

将一个随机数与对象ID 组合起来,就得到了我们需要的salt 字符串—— salt 值是什么并不重要,只要它是无法预测的就行( 譬如说,用时间作为salt 值的熵*就不如用随机字符串来得高) 。最后,把这个salt 值放进模型对象的salt 属性中。

 

depot_p/app/models/user.rb
def create_new_salt
  self.salt = self.object_id.to_s + rand.to_s
end

在代码里写self.salt=...,以强制调用salt= 方法——准确地说是“调用当前对象的salt= 方法”。如果没有self. 的话, Ruby 会认为我们正在给一个局部变量赋值,那我们的代码就没有效果了 

 

每当明文密码被放进User 对象时,都会自动生成加密后的散列码( 后者将被存入数据库) 。为此,我们将把“明文密码”变成模型中的一个虚拟属性(virtualattribute)—— 它从应用程序的角度看上去像是个属性,但却不会被存入数据库

attr_accessor :password (标准的访问方式)

 

attr_accessor会在幕后生成两个属性访问方法:一个名叫password 的读方法,以及一个名叫password=的写方法——写方法的名称以等号结尾,意味着它是可以被赋值的。我们不打算使用标准的访问方法,而是自己动手来实现访问方法,在写方法中生成一个新的salt 值,然后用它来生成密码散列值。
depot_p/app/models/user.rb
def password
  @password
end
def password=(pwd)
  @password = pwd
  return if pwd.blank ?
  create_new_salt
  self.hashed_password = User.encrypted_password(self.password, self.salt)
end 

 

 def self.authenticate(name, password)
  user = self.find_by_name(name)
  if user
   expected_password = encrypted_password(password, user.salt)
   if user.hashed_password != expected_password
    user = nil
   end
  end
  user
 end 

 

方法的第一行调用了find_by_name 方法,但你哪儿都找不到这么一个代码。不过别担心, ActiveRecord 会注意到我们调用了一个未经定义的方法,并且该方法的名字以find_by开头、以一个字段名结尾,所以它会自动帮我们创建一个查找方法,并将其添加到模型类上。 

 

管理用户 

depot_p/app/controllers/users_controller.rb 

def index
@users = User.find(:all, :order => :name) 

 

def create 

respond_to do |format|
if @user.save
→ flash[:notice] = "User #{@user.name} was successfully created."
→ format.html { redirect_to(:action=>'index') } 

 

def update

respond_to do |format|
if @user.update_attributes(params[:user])
→ flash[:notice] = "User #{@user.name} was successfully updated."
→ format.html { redirect_to(:action=>'index') } 

 

depot_p/app/views/users/index.html.erb
<h1>Listing users</h1>
<table>
    <tr>
        <th>Name</th>
    </tr>
<% for user in @users %>
    <tr>
        <td><%=h user.name %></td>
        <td><%= link_to 'Show', user %></td>
        <td><%= link_to 'Edit', edit_user_path(user) %></td>
        <td><%= link_to 'Destroy', user, :confirm => 'Are you sure? ', :method => :delete %></td>
    </tr>
<% end %>
</table>
<br />
<%= link_to 'New user', new_user_path %>  

 

depot_p/app/views/users/new.html.erb
<div class="depot-form">
<% form_for(@user) do |f| %>
<%= f.error_messages %>

  <fieldset>
    <legend>Enter User Details</legend>
    <div>
      <%= f.label :name %>:
      <%= f.text_field :name, :size => 40 %>
    </div>
    <div>
      <%= f.label :user_password, 'Password' %>:
      <%= f.password_field :password, :size => 40 %>
    </div>
    <div>
      <%= f.label :user_password_confirmation, 'Confirm' %>:
      <%= f.password_field :password_confirmation, :size => 40 %>
    </div>
    <div>
      <%= f.submit "Add User" , :class => "submit" %>
    </div>
  </fieldset>
<% end %>
</div>

 

现在我们已经能够把用户信息存入数据库了。在我们尝试之前,先来将用户样式表链接起来,我们还是通过修改用户视图的布局模板来实现。 

 

depot_p/app/views/layouts/users.html.erb
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
<head>
<meta http-equiv="content-type" content="text/html;charset=UTF-8" />
<title>Users: <%= controller.action_name %></title>
→ <%= stylesheet_link_tag 'scaffold', 'depot' %>
</head> 

 

登录 

 我们需要提供一张表单,以便管理员输入用户名和密码。
 在管理员登录以后,我们须要在session中记录这一事实,直到他们登出为止。
 我们要限制对应用程序中管理端的访问:只允许以管理员身份登录的用户访问。 

 

我们先来定义一个admin 控制器,它包含三个方法—— login() 、logout() 和index()方法( 仅用来欢迎管理员) 。 

ruby script/generate controller admin login logout index 

 

depot_p/app/controllers/admin_controller.rb
  def login
    if request.post?
      user = User.authenticate(params[:name], params[:password])
      if user
        session[:user_id] = user.id
        redirect_to(:action => "index" )
      else
        flash.now[:notice] = "Invalid user/password combination"
      end
    end
  end

 

depot_p/app/views/admin/login.html.erb 

<div class="depot-form">
<% form_tag do %>
 <fieldset>
  <legend>Please Log In</legend>
  <div>
   <label for="name">Name:</label>
   <%= text_field_tag :name, params[:name] %>
  </div>
  <div>
   <label for="password">Password:</label>
   <%= password_field_tag :password, params[:password] %>
  </div>
  <div>
   <%= submit_tag "Login" %>
  </div>
 </fieldset>
<% end %>
</div> 

 

它没有使用form_for 方法,而是使用了form_tag—— 后者只是生成一个普通的HTML<form> 标记而已。 

我们可以把params 中的值直接与表单输入字段关联起来——不需要任何模型对象作为中介。在这里,我们直接使用了params 对象 

 

 

depot_p/app/views/admin/index.html.erb
<h1>Welcome</h1>
It's <%= Time.now %>
We have <%= pluralize(@total_orders, "order" ) %>. 

 

depot_p/app/controllers/admin_controller.rb
def index
@total_orders = Order.count
end 

 

访问控制 

 对于没有以管理员身份登录的人,我们希望阻止他们访问管理端的页面。用Rails 提供的过滤器(filter) 工具实现这一功能并不困难。

 

 Rails 的过滤器允许对action 方法调用进行拦截,在调用action 方法之前或之后加上我们自己的处理逻辑。

 

前置过滤器(before filter)来拦截所有对admin 控制器中action 的调用。拦截器将对session[:user_id]进行检查:如果这里有值,并且又能够与数据库中的用户对应上,就说明管理员已经登录了,调用就会继续进行;如果session[:user_id]没有值,拦截器就会发起一次重定向,将用户引导到登录页面。 

 

这个方法应该放在哪里?当然,可以将它直接放在admin 控制器中,但由于某些原因( 很快会解释) ,我们要把它放在ApplicationController中——这是所有控制器的父类,它位于app/controllers/application.rb 文件中。同时请注意,我们需要限制对这个方法的访问,因为application.rb中的方法会成为所有控制器的实例方法,其中所有的public 方法都会以action 的形式暴露给最终用户。 

 

class ApplicationController < ActionController::Base
before_filter :authorize, :except => :login 

 

→ protected
→ def authorize
→ unless User.find_by_id(session[:user_id])
→ flash[:notice] = "Please log in"
→ redirect_to :controller => 'admin', :action => 'login'
→ end
→ end
→ end 

 

需要注意,这样做有些过头了,管理员自己也无法访问store 了,这样做还不行。 我们再改一改,把需要身份认证的方法标注出来,这就是所谓的黑名单,它比较容易因遗漏而造成错误。更好的方法是把不需要认证的方法做成白名单,我们这里就采用这种方法。做法也很简单,只需将store 控制器中的authorize方法重写就可以了。

 

depot_q/app/controllers/store_controller.rb
class StoreController < ApplicationController
#...
protected
  def authorize
  end

end 

 

现在请删除session文件( 因为你已经登录了) 。
rake db:sessions:clear 

 

友好的登录系统 

按照目前的代码,如果管理员尝试在未登录的状态下访问受限的页面,他就会被引到登录页面上;在完成登录之后,接着出现的是统一的状态页面——用户最初的请求已经被遗忘了。如果你愿意的话,也可以对应用程序稍作修改,在用户登录之后将其引到最初请求的页面。 

 

首先,如果authorize()方法需要让用户去登录的话,应该同时将当前请求的URI记在session中。 

  def authorize
    unless User.find_by_id(session[:user_id])
      session[:original_uri] = request.request_uri
      flash[:notice] = "Please log in"
      redirect_to :controller => 'admin', :action => 'login'
    end
  end

 

一旦用户登录成功,我们就可以检查session中是否保存了一个请求URI:如果有的话,就将用户请求重定向到他原本请求的地址。 

      if user
        session[:user_id] = user.id
        uri = session[:original_uri]
        session[:original_uri] = nil
        redirect_to(uri || { :action => "index" }) 

 

增加边栏,以及更多的管理功能

我们再次编辑app/controllers/application.rb ,这次加上一个对布局的调用。

 

depot_q/app/controllers/application.rb
class ApplicationController < ActionController::Base
layout "store"
#...

 

现在访问http://localhost:3000/admin 的 话,会看到一个错误,那是视图尝试显示购物车造成的。现在,我们要做的是防止在与购物车无关的功能中将购物车显现出来,并在布局模板的边栏中增加不同管理功能的链接,并且保证只有session中存在:user_id 时才显示出来

 

depot_q/app/views/layouts/store.html.erb

 

<div id="columns">
<div id="side">
→ <% if @cart %>
<% hidden_div_if(@cart.items.empty ? , :id => "cart" ) do %>
<%= render(:partial => "cart" , :object => @cart) %>
<% end %>
→ <% end %>

 

→ <% if session[:user_id] %>
→ <br />
→ <%= link_to 'Orders', :controller => 'orders' %><br />
→ <%= link_to 'Products', :controller => 'products' %><br />
→ <%= link_to 'Users', :controller => 'users' %><br />
→ <br />
→ <%= link_to 'Logout', :controller => 'admin', :action => 'logout' %>
→ <% end %>

 

现在访问http://localhost:3000/admin的话,就能看到熟悉的Pragmatic Bookshelf标题和边栏了。但如果访问http://localhost:3000/users就看不到。那是由于我们还有一件事情没做:由脚手架产生的表单会将应用中缺省的布局模板覆盖掉,我们需要停用它,很简单,删掉就行了 。

 

del app/views/layouts/products.html.erb
del app/views/layouts/users.html.erb
del app/views/layouts/orders.html.erb
 

 

 

没有管理员了…… 

 点击用户“ dave” 旁边的destroy链接。没有任何问题,用户被删除了。但令我们吃惊的是,出现在眼前的却是登录页面。我们已经把唯一的管理员用户给删掉了,当进行随后的请求时,身份认证失败,因此应用程序拒绝让我们进入。现在的情形令人尴尬:我们必须首先登录才能使用管理功能:但数据库中已经没有任何管理员用户,所以我们无法登录。

 

ruby script/console 

 

User.create(:name => 'dave', :password => 'secret',:password_confirmation => 'secret') 

User.count 

 

我们将把“删除”动作放在一个事务的范围内。如果用户被删除之后数据库中不存在任何别的用户,就将事务回滚,将最后一个被删除的用户恢复回去。 

 

为此我们需要用到ActiveRecord 的钩子方法。在前面我们已经看过了一个钩子方法:当ActiveRecord 要校验模型对象的状态是否合法时,它就会调用该对象的validate 方法。实际上, ActiveRecord 总共定义了大概20 个钩子方法,分别在模型对象生命周期的不同时间点被调
。在这里我们要使用after_destroy钩子方法,该方法会在SQL delete 语句执行之后被调用。如果它是公有的,就会被和它在同一个事务中的delete 方法调用,因此只要在该方法里抛出异常,整个事务就会被回滚。 

 

depot_q/app/models/user.rb
def after_destroy
   if User.count.zero?
      raise "Can't delete last user"
   end
end 

 

这里的关键概念是:用异常来表示删除用户的过程中出现了错误。 

 

在事务内部,异常会导致自动回滚;如果在删除用户之后users 表为空,抛出异常就可以撤销删除操作,恢复最后一个用户。 

异常可以把错误信息带回给控制器。我们在控制器中使用begin/rescue 代码块来处理异常,并将错误信息放在flash 中报告给用户。如果你只想中断事务,并不想报告异常,那就抛出ActiveRecord::Rollback 异常,因为只有这个异常才不会传递给ActiveRecord::Base.transaction 。 

 

depot_q/app/controllers/users_controller.rb
def destroy
@user = User.find(params[:id])
→ begin
→ flash[:notice] = "User
#{@user.name} deleted"
→ @user.destroy
→ rescue Exception => e
→ flash[:notice] = e.message
→ end
 

 

用户登出 

depot_q/app/controllers/admin_controller.rb
def logout
session[:user_id] = nil
flash[:notice] = "Logged out"
redirect_to(:action => "login" )
end 

 

注意到,在StoreController 中有一点重复代码:除了empty_cart 之外,每个action 都要到session数据中去寻找用户的购物车。下面这行代码
@cart = find_cart 

 

在控制器中出现了好几次。现在我们知道,可以用过滤器来解决这个问题。于是,我们对find_cart()方法进行了修改,让它直接把找到的结果放进@cart 实例变量中。 

depot_q/app/controllers/store_controller.rb
def find_cart
@cart = (session[:cart] ||= Cart.new)
end
 

 


before_filter :find_cart, :except => :empty_cart  

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值