• 使用Cucumber+Rspec玩转BDD(4)——用户登录并“记住我”



    ### 温故知新 ###


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

        为了获得更好的阅读体验,读者朋友们可以在这里下载源码: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。为了方便演示编码过程,文章中没有一一列举。)

    标签: CucumberRailsRspecTDD

    作者: fandyst
    出处: http://www.cnblogs.com/todototry/
    关注语言: python、javascript(node.js)、objective-C、java、R、C++
    兴趣点: 互联网、大数据技术、大数据IO瓶颈、col-oriented DB、Key-Value DB、数据挖掘、模式识别、deep learning、开发与成本管理
    产品:
    本文版权归作者和博客园共有,欢迎转载,但未经作者同意必须保留此段声明,且在文章页面明显位置给出原文连接。
  • 相关阅读:
    LeetCode347 前k个高频元素
    剑指42 连续字数租的最大和
    hdu1540
    hdu4553 两棵线段树
    cdq分治
    负环
    最短路
    差分约束系统
    hdu3308
    hdu5862 树状数组+扫描线+离散化
  • 原文地址:https://www.cnblogs.com/ToDoToTry/p/2173386.html
Copyright © 2020-2023  润新知