Ruby Rails学习中:登陆
登陆
一. Sessions 控制器
登录和退出功能由 Sessions 控制器中相应的 REST 动作处理 : 登录表单在 new 动作中处理, 登录的过程是向 create 动作发送 POST 请求, 退出则是向 destroy 动作发送 DELETE 请求。
1.首先生成 Sessions 控制器, 以及其中的 new 动作:
$ rails generate controller Sessions new
Users 资源使用特殊的 resources 方法自动获得 REST 式路由, 而 Sessions 资源则只能使用具名路由, 处理发给 /login 地址的 GET 和 POST 请求, 以及发给 /logout 地址的 DELETE 请求, 如下图所示(删除了 rails generate controller 生成的无用路由)。
(1).添加一个资源, 获得会话的标准 REST 式动作 RED
打开文件:config/routes.rb
添加上图中的路由规则之后, 还要创建Session时生成的测试, 使用新的登录路由, 如下图所示:
(2).更新 Sessions 控制器的测试, 使用新的登录路由 GREEN
打开文件:test/controllers/sessions_controller_test.rb
Sessions 资源的路由规则会把 URL 和动作对应起来, 如下图所示:
至此, 我们添加了好几个自定义的具名路由, 现在最好看一下完整的路由列表。我们可以执行 rails routes 命令生成路由列表:
注:你没必要完全理解输出的这些路由。像这样查看路由能对应用支持的动作有个整体认识。
二. 登录表单
登录表单和注册表单之间的主要区别是, 会话不是模型, 因此不能创建类似 @user 的变量。所以, 构建登录表单时, 我们要为 form_for 稍微多提供一些信息。form_for(@user)的作用是让表单向 /users 发起 POST 请求。对会话来说, 我们需要指明资源的名称以及相应的 URL:form_for(:session, url: login_path)知道怎么调用 form_for 之后, 参照注册表单编写登陆表单, 如图所示:
1.登录表单的代码
打开文件:app/views/sessions/new.html.erb
注:为了操作方便, 我们还加入了指向“注册”页面的链接。(导航栏中的“Login”还没填写地址,所以你要在地址栏中输入 /login。稍后会修正这个问题。)
2.页面
三. 查找并验证用户的身份
首先,我们要为 Sessions 控制器编写一个最简单的 create 动作, 以及空的 new 动作和 destroy 动作, 如下图所示:
1.Sessions 控制器中 create 动作的初始版本
打开文件:app/controllers/sessions_controller.rb
create 动作现在只渲染 new 视图, 不过这为后续工作做好了准备。提交 /login 页面中的表单后, 显示的页面如下图所示:
2.查找并验证用户的身份
打开文件:app/controllers/sessions_controller.rb
上图中红框内的第一行使用提交的电子邮件地址从数据库中取出相应的用户。(我们前面说过, 电子邮件地址都是以小写字母形式保存的, 所以这里调用了 downcase 方法, 确保提交有效的地址后能查到相应的记录。)高亮显示的第二行看起来很怪, 但在 Rails 中经常使用:
user && user.authenticate(params[:session][:password])
我们使用 && (逻辑与)检测获取的用户是否有效。因为除了 nil 和 false 之外的所有对象都被视作真值, 所以上面这个语句可能出现的结果如下表所示。从表中可以看出, 当且仅当数据库中存在提交的电子邮件地址, 而且对应的密码和提交的密码匹配时, 这个语句才会返回 true 。
3.渲染闪现消息
布局中已经加入了显示闪现消息的局部视图, 所以无需其他修改, flash[:danger] 消息就能显示出来; 而且因为使用了 Bootstrap 提供的 CSS, 消息的样式也很美观,如图所示::
注:就跟代码中标注的一样, 现在的闪现消息有问题问题在于, 闪现消息在一个请求的生命周期内是持续存在的, 而重新渲染页面(使用 render 方法)与重定向不同, 不算是一次新请求, 所以你会发现这个闪现消息存在的时间比预期的要长很多。
例如:提交无效的登录信息, 然后访问首页, 还会显示这个闪现消息:
4.测试闪现消息
首先, 为应用的登录功能生成一个集成测试文件:
$ rails generate integration_test users_login
(1).捕获继续显示闪现消息的测试 RED
打开文件:test/integration/users_login_test.rb
让上图中的测试通过的方法是, 把 flash 换成特殊的 flash.now 。 flash.now 专门用于在重新渲染的页面中显示闪现消息。与 flash 不同的是, flash.now 中的内容会在下次请求时消失——这正是上图中的测试所需的行为。替换之后, 正确的应用代码如下图所示:
(2).处理登录失败正确的代码 GREEN
四. 登录
Rails 生成器很人性化, 生成 Sessions 控制器时自动生成了一个 Ses-sions 辅助模块。而且, 其中的辅助方法会自动引入 Rails 视图。如果在控制器的基类( ApplicationCon-troller )中引入辅助方法模块, 还可以在控制器中使用,如下图所示:
1.在 Application 控制器中引入 Sessions 辅助模块
打开文件:app/controllers/application_controller.rb
注:做好这些准备工作后,现在可以开始编写代码登入用户了。
2.login 方法
有 Rails 提供的 session 方法协助,登入用户很简单。( session 方法与前面生成的 Sessions 控制器没有关系。)我们可以把 session 视作一个散列, 按照下面的方式赋值:
session[:user_id] = user.id
这么做会在用户的浏览器中创建一个临时 cookie, 内容是加密后的用户 ID。在后续的请求中, 可以使用 ses-sion[:user_id] 取回这个 ID。到后面我们会使用的 cookies 方法创建的是持久 cookie, 而 session 方法创建的是临时会话, 浏览器关闭后立即失效。
我们想在多个不同的地方使用这个登录方式, 所以在 Sessions 辅助模块中定义一个名为 log_in 的方法, 如下图所示:
(1).login 方法
打开文件:app/helpers/sessions_helper.rb
session 方法创建的临时 cookie 会自动加密, 所以上图中的代码是安全的, 攻击者无法使用会话中的信息以该用户的身份登录。不过, 只有 session 方法创建的临时 cookie 是这样, cookies 方法创建的持久cookie 则有可能会受到会话劫持(session hijacking)攻击。所以在以后我们会小心处理存入用户浏览器中的信息。
定义好 log_in 方法后, 我们可以完成 Sessions 控制器中的 create 动作, 登入用户, 然后重定向到用户的资料页面, 如下图所示:
(2).登入用户
打开文件:app/controllers/sessions_controller.rb
注:简洁的重定向代码redirect_to user我们在前面节见过。Rails 会自动转换成用户资料页的地址: user_url(user)。
3.当前用户
把用户 ID 安全地存储在临时会话中之后,在后续的请求中可以将其读取出来。我们要定义一个名为 cur-rent_user 的方法,从数据库中取出用户 ID 对应的用户。 current_user 方法可用于编写类似下面的代码: <%= current_user.name %> 或是: redirect_to current_user 查找用户的方法之一是使用 find 方法,在用户资料页面就是这么做的: User.find(session[:user_id]) 前面说过,如果用户 ID 不存在, find 方法会抛出异常。在用户的资料页面可以使用这种行为,因为必须有相应的用户才能显示他的信息。但 session[:user_id] 的值经常是 nil (表示用户未登录),所以我们要使用 create 动作中通过电子邮件地址查找用户的 find_by 方法,通过 id 查找用户: User.find_by(id: session[:user_id]) 如果 ID 无效, find_by 方法返回 nil ,而不会抛出异常。因此,我们可以按照下面的方式定义 current_user 方法: def current_user User.find_by(id: session[:user_id]) end 这样定义应该可以,不过如果在一个页面中多次调用 current_user 方法,会多次查询数据库。所以,我们要使用一种 Ruby 习惯写法,把 User.find_by 的结果存储在实例变量中,只在第一次调用时查询数据库,后续再调用直接返回实例变量中存储的值: if @current_user.nil? @current_user = User.find_by(id: session[:user_id]) else @current_user end 使用前面节中介绍的“或”运算符 || ,可以把这段代码改写成: @current_user = @current_user || User.find_by(id: session[:user_id]) User 对象是真值,所以仅当 @current_user 没有赋值时才会执行 find_by 方法。 述代码虽然可以使用,但并不符合 Ruby 的习惯。 @current_user 赋值语句的正确写法是这样: @current_user ||= User.find_by(id: session[:user_id])
这种写法用到了容易让人困惑的 ||= (或等)运算符, 参见下图的说明:
(1).在会话中查找当前用户
打开文件:app/helpers/sessions_helper.rb
注:定义好 current_user 方法之后,可以根据用户的登录状态修改应用的布局了。
4.修改布局中的链接
修改网站布局中的链接时要在 ERb 中使用 if-else 语句,用户登录时显示一组链接,未登录时显示另一组链接: <% if logged_in? %> # 登录用户看到的链接 <% else %> # 未登录用户看到的链接 <% end %> 为了编写这种代码,我们要定义 logged_in? 方法,返回布尔值。 用户登录后,当前用户存储在会话中,即 current_user 不是 nil 。检测会话中有没有当前用户要使用“非”运算符。“非”运算符写做 ! ,经常读作“bang”。logged_in? 方法的定义如下图所示:
(1).logged_in? 辅助方法
打开文件:app/helpers/sessions_helper.rb
(2).修改布局中的链接
打开文件:app/views/layouts/_header.html.erb