<body><script type="text/javascript"> function setAttributeOnload(object, attribute, val) { if(window.addEventListener) { window.addEventListener('load', function(){ object[attribute] = val; }, false); } else { window.attachEvent('onload', function(){ object[attribute] = val; }); } } </script> <div id="navbar-iframe-container"></div> <script type="text/javascript" src="https://apis.google.com/js/platform.js"></script> <script type="text/javascript"> gapi.load("gapi.iframes:gapi.iframes.style.bubble", function() { if (gapi.iframes && gapi.iframes.getContext) { gapi.iframes.getContext().openChild({ url: 'https://www.blogger.com/navbar.g?targetBlogID\x3d3443179681250049771\x26blogName\x3dNewbie+On+Rails\x26publishMode\x3dPUBLISH_MODE_BLOGSPOT\x26navbarType\x3dBLACK\x26layoutType\x3dCLASSIC\x26searchRoot\x3dhttps://l404.blogspot.com/search\x26blogLocale\x3dzh_CN\x26v\x3d2\x26homepageUrl\x3dhttp://l404.blogspot.com/\x26vt\x3d-4382633446460227058', where: document.getElementById("navbar-iframe-container"), id: "navbar-iframe" }); } }); </script>

Newbie On Rails

方便初学Rails的朋友们快速上手并驶入BDD敏捷之道。


### 温故知新 ###


    在上一篇文章中,我们参照文章内容完成了用户登录功能的开发工作。此时,注册用户可以顺利登录站点,查看用户资料等等;但这一状态也只限于当前的浏览器窗口,如果浏览器关闭了,用户重新打开浏览器下次访问的时候,还是需要来到登录页面进行重新登录。若不是做交易支付型站点,为了追求好一点的用户体验,我们可以给用户预留一个可选项;用户在登录的时候可以勾选“记住我”,一段时间内用户将不必重新登录。要实现用户的这种持久登录状态,我们应该怎么做呢?不妨来了解我们接下来的活儿。

    为了获得更好的阅读体验,读者朋友们可以在这里下载源码:http://github.com/404/bdd_user_demo/tree/master


### 新建工作分支 ###

    $ git checkout -b remember_me

    在有效时间内,要保持用户的在线状态。那么第一个问题会是,我们得知道用户是否已经登录呢?按照我们之前的预期,比如用户的资料应该是受保护的,只有当用户登录以后才可以查看;那就有必要在程序上做一些访问控制。


### 实现简单的访问控制 ###

    使用 Rails的 before_filter 钩子方法可以非常方便地实现我们的目的。

    $ gedit app/controllers/users_controller.rb

    在 UserController 类中的任何方法之前加上如下一段代码:

    before_filter :login_required, :only => [:show]

    这样在查看用户资料的时候会检查用户是否已经登录,如果未登录会提供一张登录用的表单,如果已经登录了就会呈现用户资料。

    我们继续秉承测试先行这一理念,从编写“用户登录且勾选记住我”的故事开始。


### 用户登录之“记住我” ###

    $ gedit features/user_login.feature

    在文件尾部续添如下文本:

      场景: 用户已激活帐号且使用有效身份登录并勾选记住我
        假如 我已经使用<404/xuliicom@gmail.com/password>注册过且已经激活了帐号
        当 我以<xuliicom@gmail.com/password>这个身份登录并勾选<记住我>
        那么 我应该看到<登录成功>的提示信息
        而且 我应该成功登录网站
        当 我关闭网页下次再来访问的时候
        那么 我应该依然保持登录状态


    运行测试,

    $ ruby script/cucumber -l zh-CN features/user_login.feature

   

    测试失败,根据提示信息来看,我们需要添加一些故事情节运行所需的测试脚本。


### 添加用户驱动故事运行的测试脚本 ###

    $ gedit features/step_definitions/user_steps.rb

    续添如下脚本:

    When /^我关闭网页下次再来访问的时候$/ do
      当 %{session已经被清除}
      而且 %{我来到用户登录页面}
    end

    When /^session已经被清除$/ do
      request.reset_session
      request.session[:user_id].should be_nil
    end

    Then /^我应该依然保持登录状态$/ do
      # 很遗憾,在测试代码中,cookies里边放符号索引会返回nil对象
      # 用字符串来索引没问题
      # 更多信息可以查阅 http://dev.rubyonrails.org/ticket/5924
      cookies['remember_token'].should_not be_blank
      request.session[:user_id].should_not be_nil
    end


    修改 “When /^我以<(.+)\/(.+)>这个身份登录$/ do ... end” 这段代码如下:

    When /^我以<(.+)\/(.+)>这个身份登录(并勾选<记住我>)?$/ do |username_or_email, password, remember|
      当 %{我来到用户登录页面}
      而且 %{我在输入框<用户名或邮箱>中输入<#{username_or_email}>}
      而且 %{我在输入框<密码>中输入<#{password}>}
      而且 %{我勾选<记住我>} if remember
      而且 %{我按下<登录>按钮}
    end


    为 “当 我勾选<记住我>” 添加对应的运行脚本,

    When /^我勾选<(.+)>$/ do |field|
      check(field)
    end


    保存 user_steps.rb。运行测试,

    $ ruby script/cucumber -l zh-CN features/user_login.feature

   


### 观测试了解工作内容 ###

    测试结果返回 “Could not find field: "记住我" (Webrat::NotFoundError)”的相关信息,提示没有找到关于“记住我”的这个表单域,不用多想,这个“记住我”的多选框应该出现在用户登录页面。

    $ gedit app/views/sessions/new.html.erb

    修改后的登录页面代码如下,

    <% form_tag sessions_path do %>
      <p>
        <%= label_tag 'username_or_email', '用户名或邮箱' %><br />
        <%= text_field_tag 'username_or_email' %>
      </p>
      <p>
        <%= label_tag 'password', '密码' %><br />
        <%= password_field_tag 'password' %>
      </p>
      <p>
        <%= check_box_tag 'remember_me', 1, true %>
        <%= label_tag 'remember_me', '记住我' %>
      </p>
      <p>
        <%= submit_tag '登录' %>
      </p>
    <% end %>


    保存 app/views/sessions/new.html.erb。运行测试,

    $ ruby script/cucumber -l zh-CN features/user_login.feature

   

    问题出在 user_steps.rb 文件的第86行,我们来看看这行代码的内容,

   

    测试代码里边写明了,如果用户关闭网页再次访问的时候,cookies[:remember_token]的值应该不为空,这样就可以实现记住我的功能。测试结果告诉我们该值为空,为了达到我们想要的效果,我们来做些实际的编码工作。

    之前在用户登录页面的模板文件中,我们已经添加了供用户可选“记住我”的选项,下面添加一些处理业务流程的代码。

    $ gedit app/controllers/sessions_controller.rb

    修改 create 方法,在 sign_user_in(@user)之前添加如下一句代码,

    remember(@user) if remember?

    这样用户在勾选“记住我”选项之后,系统会自动设置记住用户的相关细节,不过这些具体细节还需要我们通过编码来完成。

    继续在 SessionsController 中编写刚才那段代码中用到的两个方法,

      private
     
      def remember?
        params[:remember_me] && params[:remember_me] == "1"
      end
       
      def remember(user)
        user.remember_me!
        cookies[:remember_token] = {
          :value   => user.remember_token,
          :expires => user.remember_token_expires_at
        }
      end


    我们将这两个方法设为私有仅供程序内部使用,上面的 remember 方法已经涉及到数据存取;我们还需要修改 User 模型类以衔接上述的业务逻辑。

    首先增添上述代码用到的两个数据字段,即 remember_token 和 remember_token_expires_at;之所以添加这两个字段,是因为服务端需要记录客户端自动登录且唯一的cookie标识,并用该标识来验证客户端的请求是否有效,如果有效就可以打破HTTP协议无状态的限制建立持久的会话连接,如果客户端的cookie headers是伪造或失效的,那么很遗憾地非法请求将不得逞,并导向用户登录页面,提示该用户登录。下面我们来添加数据迁移文件,

    $ ruby script/generate migration RememberMe

    $ gedit db/migrate/*_remember_me.rb


    class RememberMe < ActiveRecord::Migration
      def self.up
        add_column :users, :remember_token, :string
        add_column :users, :remember_token_expires_at, :datetime
      end

      def self.down
        remove_column :users, :remember_token_expires_at
        remove_column :users, :remember_token
      end
    end


    保存 db/migrate/*_remember_me.rb。然后执行迁移,

    $ rake db:migrate

    $ rake db:test:prepare


    接着修改 UserModel,

    $ gedit app/models/user.rb

    添加如下代码,

      # remember_token 是否失效
      def remember?
        remember_token_expires_at && Time.now < remember_token_expires_at
      end

      # 记住多长时间
      def remember_me!
        remember_me_until 2.weeks.from_now
      end

      # 保存“记住我”的相关设置
      def remember_me_until(time)
        self.remember_token_expires_at = time
        self.remember_token            = encrypt(time)
        save(false)
      end


    保存 UserModel ,最后在 ApplicationController 类中添加一个读取并校验 cookie 的方法。用户访问的时候检查cookie,如果该cookie有效就可以直接登录了。

    $ gedit app/controllers/application.rb

    在 user_from_session 方法之后添加如下方法,

      def user_from_cookie
        if cookies[:remember_token]
          user = User.find_by_remember_token(cookies[:remember_token])
          user && user.remember? ? user : nil               
        end
      end


    然后修改 current_user 方法,可以接受用户使用 cookie 的方式登录。

      def current_user
        @_current_user ||= (user_from_session || user_from_cookie)
      end


    保存 ApplicationController,运行测试看看;

    $ ruby script/cucumber -l zh-CN features/user_login.feature

   

    测试通过!:)


### 小结 ###

    这篇文章介绍的内容不是很多,功能也不算复杂。在用户已经能够登录站点的基础上,我们给登录用户加上了“记住我”的功能,这是一种持久登录状态。说到持久一词,就不得不提到HTTP协议是无状态的,无状态在这里意指在一般的B/S连接中,Server 无法识别特定的 Browser,因为一台 Web Server 响应的 Browsers 不计其数,Server 没办法知道当前所响应的Browser是谁,也不记得这Browser之前是否请求过。不过有了 cookie,就可以打破HTTP协议无状态的这一限制。cookie是一种在客户端存储数据并以此来跟踪和识别用户的机制。Server 响应 Browser 的请求时会发送一个带 set-cookie 的 http headers,Browser 会在本地记住这一cookies 数据;当 Browser 再次请求时,就会将这一 cookies 发送给 Server ,Server 在响应 Browser 请求的同时接受并读取此 cookies,以此来达到跟踪和识别用户的目的。如果服务端没有特别地设置cookies,客户端针对这一站点的cookies将随浏览器进程的关闭而失效。在之前我们使用session来做用户登录即是如此,因为Rails将会话数据(session data)存在客户端的cookies里边,cookies的有效期是随浏览器的关闭而失效的,所以当用户关闭浏览器后重新打开再次访问就需要登录;后来我们在程序里显示地配置了cookies的有效期为两周,当用户第一次登录后,cookies会在用户浏览器中保存两周,那么用户在这两周内就不需要重新登录了。


### 相关阅读 ###

    cookie机制的纯JavaScript实现:http://chinaonrails.com/topic/view/1449.html
    php系统和ror系统的用户登录授权问题:http://chinaonrails.com/topic/view/1711.html
    了解关于cookie和Rails交互的更多信息:http://chinaonrails.com/q/cookie


### 下节预告 ###

    接下来的一章里会向读者朋友们演示登录用户如何安全退出,敬请期待!


### 提交工作成果到GIT仓库 ###

    $ git status
    $ git add .
    $ git commit -m "A user can be login with remember me."
    $ git checkout master
    $ git merge remember_me
    $ git branch -d remember_me
    $ git tag v4


    (注意,真正的开发中可不是到功能开发完毕了才commit,而是边开发边add和commit。为了方便演示编码过程,文章中没有一一列举。)

标签: , , ,

0 条评论

» Leave a Reply

订阅 博文评论 [Atom]

« 主页