Ruby on Rails教材中文译文 第六章 建立用户模型

在第5章中

我们以创建新用户的页面结束(第5.4节)

在接下来的六章中

我们将履行这个初始注册页面中隐含的承诺

在本章中

我们将通过为我们网站的用户创建数据模型以及存储该数据的方法来迈出第一步

在第7章中

我们将为用户提供注册我们网站和创建用户个人资料页面的功能

一旦用户可以注册

我们也会让他们登录并注销(第8章和第9章)

在第10章(第10.2.1节)中

我们将学习如何保护页面免受不正当访问

最后,在第11章和第12章中

我们将添加帐户激活(从而确认有效的电子邮件地址)和密码重置

总之,第6章到第12章中的材料开发了一个完整的Rails登录和身份验证系统

您可能知道

Rails有各种预构建的身份验证解决方案

方框6.1解释了为什么

在一开始自己造个轮子可能是一个更好的主意

实际上,所有Web应用程序都需要某种登录和身份验证系统

因此,大多数Web框架都有很多实现此类系统的选项

Rails也不例外

身份验证和授权系统的示例包括

Clearance,Authlogic,Devise和CanCan

以及基于OpenID或OAuth构建的非Rails特定解决方案

有人问我们为什么要重新发明轮子

为什么不使用现成的解决方案而是自己动手?实践经验表明

大多数站点上的身份验证需要进行大量自定义

而修改第三方产品通常比从头开始编写系统更现实

此外,现成的系统可能是“黑匣子”

可能是神秘的内脏

当你编写自己的系统时

你更有可能理解它

第三,最新的Rails版本(第6.3节)使编写自定义身份验证系统变得容易

最后,如果您使用第三方系统

并且同时拥有自己开发登录系统的经验

那么您将能够更好地理解和修改第三方的配置

方框6.1 建立您自己的身份验证系统

6.1 用户模型

虽然接下来的三章的最终目标是为我们的网站制作一个注册页面(如图6.1所示)

但现在接受新用户的信息几乎没有用处

我们目前没有任何存储他们的地方

因此,注册用户的第一步是创建一个数据结构来捕获和存储他们的信息

在Rails中

数据模型的默认数据结构自然被称为模型

即第1.3.3节中的MVC中的M

针对持久性问题的默认Rails解决方案是使用数据库进行长期数据存储

并且用于与数据库交互的默认库称为Active Record

Active Record附带了许多用于创建,保存和查找数据对象

无需使用关系数据库使用的结构化查询语言(SQL)

此外,Rails还有一个称为迁移的功能

允许使用纯Ruby编写数据定义

而无需学习SQL数据定义语言(DDL)

结果是Rails几乎完全隔离了数据库的细节

在本书中

通过使用SQLite进行开发和使用PostgreSQL

我们进一步开发了这个主题

以至于我们几乎不必考虑Rails如何存储数据

即使对于生产应用程序也是如此

像往常一样

如果您正在使用Git进行版本控制

那么现在是制作用于建模用户的主题分支的好时机

git checkout -b modeling-users

6.1.1 数据库迁移

您可以回忆一下4.4.5节

我们已经通过自定义构建的User类遇到了具有名称和电子邮件属性的用户对象

该类作为一个有用的示例

但它缺乏持久性的关键属性

当我们在Rails控制台上创建一个User对象时

它会在我们退出后立即消失

我们在本节中的目标是为用户创建一个不会轻易消失的模型

与第4.4.5节中的User类一样

我们首先要为用户建模

使用两个属性,一个名称和一个电子邮件地址

后者我们将用作唯一的用户名

我们将添加一个第6.3节中的密码属性

在代码清单4.17中

我们使用Ruby的attr_accessor方法完成了这个操作

相反,当使用Rails为用户建模时

我们不需要明确地识别属性。

如上所述,存储数据Rails默认使用关系数据库

它由数据行组成的表组成

每行包含数据属性列

例如,要存储具有名称和电子邮件地址的用户

我们将创建一个包含名称和电子邮件列的用户表

每行对应一个用户

这种表的一个例子如图6.2所示

对应于图6.3所示的数据模型

图6.3只是一个草图

完整的数据模型如图6.4所示

通过命名列名和电子邮件

我们将让Active Record为我们找出User对象属性

您可以从清单5.38中回忆一下

我们使用该命令创建了一个Users控制器

rails generate controller Users new

用于创建模型的类似命令是生成模型

我们可以使用它来生成具有名称和电子邮件属性的用户模型

如清单6.1所示

$ rails generate model User name:string email:string

invoke active_record

create db/migrate/20160523010738_create_users.rb

create app/models/user.rb

invoke test_unit

create test/models/user_test.rb

create test/fixtures/users.yml

Listing 6.1: Generating a User model.

注意,与控制器名称的多个约定相反

模型名称是单数的

Users控制器

User模型

通过传递可选参数name:string和email:string

我们告诉Rails我们想要的两个属性

以及这些属性应该是哪些类型(在这种情况下,字符串)

将此与清单3.6和清单5.38中的操作名称进行比较

清单6.1中的generate命令的结果之一

是一个名为migration的新文件

migration提供了一种逐步改变数据库结构的方法

以便我们的数据模型能够适应不断变化的需求

对于User模型

migration由模型生成脚本自动创建

它创建了一个包含两列(名称和电子邮件)的用户表

如清单6.2所示

我们将从6.2.5节开始,从头开始进行迁移

请注意

migration文件的名称前缀为基于生成迁移的时间戳的时间戳

在迁移的早期

文件名以增量整数作为前缀

如果多个程序员具有相同数量的迁移

则会导致协作团队发生冲突

除了不可能的迁移场景产生相同的时间

使用时间戳可以方便地避免这种冲突

迁移本身由一个更改方法组成

该方法确定要对数据库进行的更改

在代码清单6.2的情况下

更改使用名为create_table的Rails方法

在数据库中创建用于存储用户的表

create_table方法接受一个块(第4.3.2节)

其中包含一个块变量

在本例中称为t(“table”)

在块内

create_table方法使用t对象在数据库中创建名称和电子邮件列

两者都是string类型

这里表名是复数(用户)

即使模型名称是单数(User)

它反映了Rails后面的语言约定

模型表示单个用户

而数据库表由许多用户组成

块中的最后一行t.timestamps是一个特殊命令

它创建两个名为created_at和updated_at的神奇的列

这些列是在创建和更新给定用户时自动记录的时间戳

我们将在6.1.3节中看到神奇列的具体示例

清单6.2中的迁移所代表的完整数据模型如图6.4所示

注意自动添加的神奇两列

图6.3中显示的草图中没有

我们可以使用db:migrate命令运行迁移

称为“迁移”,如下所示:

rails db:migrate

您可能还记得我们在2.2节中的

类似上下文中运行了此命令

第一次运行db:migrate时

它会创建一个名为db/development.sqlite3的文件

这是一个SQLite数据库

我们可以通过使用DB Browser for SQLite

打开development.sqlite3来查看数据库的结构

如果您使用的是云IDE

则应首先将数据库文件下载到本地磁盘

如图6.5所示

结果如图6.6所示

与图6.4中的图表进行比较

您可能会注意到图6.6中的一列未在迁移中考虑:id列

正如2.2节中简要提到的,该列是自动创建的,Rails使用它来唯一地标识每一行。

练习

1. Rails使用db目录中名为schema.rb的文件来跟踪数据库的结构

称为模式,并以此为文件名

检查db/schema.rb的本地副本

并将其内容与清单6.2中的迁移代码进行比较

2. 大多数迁移(包括本教程中的所有迁移)都是可逆的

这意味着我们可以“向下迁移”并使用一个名为db:rollback的命令撤消它们

rails db:rollback

运行此命令后

检查db/schema.rb以确认回滚是否成功

有关逆转迁移的另一种技术

请参见专栏3.1

在此过程中

此命令执行drop_table命令以从数据库中删除users表

这样做的原因是change方法知道drop_table是create_table的反转

这意味着可以很容易地推断回滚迁移

在不可逆迁移的情况下

例如删除数据库列

必须定义单独的向上和向下方法来代替单个更改方法

有关详细信息,请参阅Rails指南中的迁移

3. 通过再次执行rails db:migrate重新运行迁移

确认已恢复db/schema.rb的内容

6.1.2 模型文件

我们已经看到了清单6.1中的用户模型生成如何生成迁移文件(清单6.2)

我们在图6.6中看到了运行此迁移的结果

它通过创建表user更新了名为development.sqlite3的文件

包含了id,name,email,created_at和updated_at

清单6.1还创建了模型本身

本节的其余部分致力于理解它

我们首先查看User模型的代码

该模型位于app/models/目录下的user.rb文件中

目前看来,它非常紧凑(代码清单6.3)

回想一下4.4.2节

语法class User

意味着User类继承自ApplicationRecord类

而ApplicationRecord类继承自ActiveRecord :: Base(图2.18)

因此User模型自动拥有了ActiveRecord :: Base类所有的功能

当然,除非我们知道ActiveRecord :: Base包含什么

否则这些知识对我们没有好处

所以让我们先来看一些具体的例子

练习

1. 在Rails控制台中

使用第4.4.4节中的技术确认User.new是User类并继承自ApplicationRecord

2. 确认ApplicationRecord继承自ActiveRecord :: Base

6.1.3 创建用户对象

与第4章一样

我们选择探索数据模型的工具是Rails控制台

由于我们尚未(仍)想对数据库进行任何更改

因此我们将在沙箱中启动控制台

$ rails console –sandbox

Loading development environment in sandbox

Any modifications you make will be rolled back on exit

正如上面消息提示的

您所做的任何修改将在退出时回滚

当在沙箱中启动时

控制台将“回滚”(即撤消)会话期间引入的任何数据库更改

在4.4.5节的控制台会话中

我们使用User.new创建了一个新的用户对象

只有在需要代码清单4.17中的示例用户文件之后才能访问它

有了模型,情况就不同了

您可能还记得第4.4.4节

Rails控制台会自动加载Rails环境

其中包括模型

这意味着我们可以创建一个新的用户对象而无需进一步的工作

我们在这里看到用户对象的默认控制台表示

在没有参数的情况下调用时

User.new返回一个包含所有nil属性的对象

在4.4.5节中

我们设计了示例User类来获取初始化哈希来设置对象属性

设计选择是由Active Record激发的

它允许以相同的方式初始化对象

如果创建的时候赋值

我们看到名称和电子邮件属性已按预期设置。

有效性概念对于理解Active Record模型对象很重要

我们将在6.2节中更深入地探讨这个主题

但是现在值得注意的是我们的初始用户对象是有效的

我们可以通过调用布尔值来验证它? 方法

>> user.valid?

true

到目前为止

我们还没有碰过数据库

User.new只在内存中创建一个对象

而user.valid?只是检查对象是否有效

为了将User对象保存到数据库

我们需要在用户变量上调用save方法

如果成功则save方法返回true

否则返回false

目前,所有保存都应该成功

因为还没有验证

当部分失败时,我们将看到6.2节中的情况

作为参考

Rails控制台还显示对应于user.save的SQL命令(即INSERT INTO)“用户”……

我们在本书中几乎不需要原始SQL

从现在开始我将省略对SQL命令的讨论

但是通过阅读与Active Record命令相对应的SQL可以学到很多东西

您可能已经注意到新用户对象的id和神奇列created_at和updated_at属性的值为nil

让我们看看我们的保存是否改变了什么

我们看到id已被赋值为1

而神奇列已被分配了当前时间和日期

目前,创建和更新的时间戳是相同的

我们将在第6.1.5节中看到它们的不同

与第4.4.5节中的User类一样

User模型的实例允许使用点表示法访问其属性

正如我们在第7章中所看到的

如上所述,通常可以通过两个步骤制作和保存模型

但Active Record还允许您将它们与User.create合并为一个步骤

请注意,User.create,返回User对象本身

而不是返回true或false

我们可以选择将其分配给变量(例如上面第二个命令中的foo)

create的反转是destroy

那么我们怎么知道我们是否真的摧毁了一个物体呢

对于已保存和未销毁的对象

我们如何从数据库中检索用户

要回答这些问题

我们需要学习如何使用Active Record来查找用户对象

练习

1. 确认user.name和user.email是String类。

2. create_at和updated_at属性是什么类?

6.1.4 查找用户对象

Active Record提供了几种查找对象的选项

让我们使用它们来查找我们创建的第一个用户

同时验证第三个用户(foo)是否已被销毁

我们将从现有用户开始:

这里我们将用户的id传递给User.find

Active Record返回具有该id的用户

让我们看看数据库中是否仍存在id为3的用户

并没有

由于我们在第6.1.3节中销毁了第三个用户

因此Active Record无法在数据库中找到它

相反,find会引发异常

这是一种在程序执行中指示异常事件的方法

在这种情况下

是一个不存在的Active Record id

它会导致find引发ActiveRecord :: RecordNotFound异常

除了通用查找之外

Active Record还允许我们按特定属性查找用户

由于我们将使用电子邮件地址作为用户名

因此当我们学习如何让用户登录我们的站点时

这种查找将非常有用(第7章)

如果你担心如果有大量用户

find_by会效率低下

你学得很有预见性。。。

我们将在6.2.5节中介绍这个问题

以及它通过数据库索引的解决方案

我们将以一些更通用的方式找到用户

首先,有第一个:

当然,first只返回数据库中的第一个用户

all将数据库中的所有用户作为ActiveRecord :: Relation类的对象返回

它实际上是一个数组(第4.3.1节)

练习

1. 按名称查找用户

确认find_by_name也可以正常工作

在传统的Rails应用程序中,您经常会遇到这种旧式的find_by

2. 对于大多数实际应用,User.all就像一个数组

但它实际上是User :: ActiveRecord_Relation类

3. 通过传递长度方法确认您可以找到User.all的长度(第4.2.3节)

Ruby根据它们的行为而不是正式的类型来操纵对象的能力被称为鸭子打字

基于格言:“如果它看起来像一只鸭子,它像鸭子一样嘎嘎叫,它可能是一只鸭子。”

6.1.5 更新用户对象

一旦我们创建了对象

我们通常会想要更新它们

有两种基本方法可以做到这一点

首先,我们可以单独分配属性

如第4.4.5节所述

请注意,最后一步是将更改写入数据库

我们可以通过使用reload看到没有保存的情况

reload根据数据库信息重新加载对象

现在我们已经通过运行user.save更新了用户

神奇列有所不同,如第6.1.3节所述

更新多个属性的第二种主要方法是使用update_attributes

update_attributes方法接受属性的散列

并且成功执行更新和保存

返回true表示保存已通过

请注意,如果任何验证失败

例如需要密码来保存记录(如第6.3节中所述)

则对update_attributes的调用将失败

如果我们只需要更新单个属性

则使用单数update_attribute可以跳过验证来绕过此限制

练习

1. 使用分配和保存调用更新用户名。

2. 使用对update_attributes的调用更新用户的电子邮件地址。

3. 通过使用赋值和保存更新created_at列

确认您可以直接更改神奇列

使用值1.year.ago

这是一种在当前时间之前一年创建时间戳的Rails方法

6.2 用户验证

我们在6.1节中创建的用户模型现在具有工作名称和电子邮件属性

但它们是完全通用的

任何字符串

包括空字符串

在当前都有效

然而,名称和电子邮件地址比这更具体

例如,名称应为非空白

并且电子邮件应与电子邮件地址的特定格式特征相匹配

此外

由于我们将在用户登录时将电子邮件地址用作唯一用户名

因此我们不应允许数据库中的电子邮件重复

简而言之

我们不应该允许姓名和电子邮件只是任何字符串

我们应该对他们的价值观施加某些限制

Active Record允许我们使用验证来强加这些约束

在第2.3.2节中简要介绍过

在本节中,我们将介绍几种最常见的情况

验证存在,长度,格式和唯一性

在6.3.2节中

我们将添加最终的通用验证,确认

我们将在7.3节中看到

当用户提交不合规定数据时时

验证如何为我们提供方便的错误消息

6.2.1 有效性测试

如专栏3.3所述

测试驱动开发并不总是适合这项工作的工具

但模型验证正是TDD最适合的功能

如果没有编写失败的测试然后让它通过

那么很难确信给定的验证正在完全按照我们的预期进行

我们的方法是从一个有效的模型对象开始

将其中一个属性设置为我们想要无效的东西

然后测试它实际上是无效的

作为一个安全网站

我们首先编写一个测试来确保初始模型对象是有效的

这样,当验证测试失败时

我们会知道具体的失败的原因

而不是因为初始对象首先是无效的

清单6.1中的命令产生了一个测试用户的初始测试

尽管在这种情况下它实际上是空白的(代码清单6.4)

要为有效对象编写测试

我们将使用特殊设置方法

在第3章练习中简要讨论

创建一个最初有效的用户模型对象@user

该方法在每次测试之前自动运行

因为@user是一个实例变量

它在所有测试中自动可用

我们可以使用有效的测试其是否valid? 方法(第6.1.3节)

结果如清单6.5所示

清单6.5使用了明确的断言方法

在这种情况下如果@ user.valid?返回true则成功

如果返回false则失败

由于我们的用户模型目前没有任何验证

因此初始测试应该通过Listing 6.6:green

$ rails test:models

在这里

我们使用了rails test:models来运行模型测试

可以对比一下第5.3.4节中的集成测试

练习

1. 在控制台中,确认新用户当前有效

2. 确认在第6.1.3节中创建的用户也是有效的

6.2.2验证存在

也许最基本的验证是存在性验证

它只是验证给定的属性是否存在

例如,在本节中

我们将确保在用户保存到数据库之前存在名称和电子邮件字段

在7.3.3节中

我们将看到如何将此需求传播到注册表单以创建新用户

我们将通过构建清单6.5中的测试来测试是否存在name属性

如清单6.7所示

我们需要做的就是将@user变量的name属性设置为空字符串

在本例中为空格字符串

然后检查(使用assert_not方法)生成的User对象无效

此时,模型测试应为红色

正如我们在第2章练习中简要介绍的那样

验证name属性是否存在的方法

是使用带有参数presence:true的validates方法

如代码清单6.9所示

presence:true参数是一个单元素选项哈希

回想一下4.3.4节

当将哈希作为方法中的最终参数传递时

花括号是可选的

如第5.1.1节所述

选项哈希的使用是Rails中反复出现的主题

清单6.9可能看起来很神奇

但验证只是一种方法。

使用带括号的清单6.9的等效公式也行

让我们进入控制台

看看为我们的用户模型添加验证的效果

这里我们使用有效性验证的方法检查用户变量的有效性

当对象失败一个或多个验证时返回false

当所有验证通过时返回true

在这种情况下

我们只有一个验证

因此我们知道哪个失败了

但使用失败时生成的错误对象检查仍然有帮助

错误消息提示Rails使用blank?方法验证属性的存在

我们在4.4.3节末尾看到

由于用户无效

尝试将用户保存到数据库会自动失败

按照清单6.7中的模型

编写电子邮件属性存在的测试很容易(代码清单6.11)

应用程序代码也是如此(代码清单6.12)

此时,状态验证已完成

测试结果应为绿色

练习

创建一个名为u的新用户并确认它最初无效,输出完整的错误消息

确认u.errors.messages是哈希格式的。 如何仅输出电子邮件错误信息

user.errors.messages[:email]

6.2.3 长度验证

我们已经限制我们的用户模型要求每个用户的名称

但我们应该更进一步

用户的名称将显示在示例站点上

因此我们应该对其长度施加一些限制

通过我们在6.2.2节中所做的所有工作

这一步很简单

选择最大长度并没有什么科学依据

不妨就选择50

这意味着要验证51个字符的名字是否太长

此外,尽管不太可能出现问题

但是用户的电子邮件地址可能会超出字符串的最大长度

对于许多数据库而言

这可能是255

因为第6.2.4节中的格式验证不会强制执行此类约束

我们将在本节中添加,使代码完整

代码清单6.14显示了生成的测试

为方便起见

我们在代码清单6.14中使用了“字符串乘法”

来创建一个长51个字符的字符串

我们可以使用控制台查看其工作原理

为了让它们通过

我们需要使用validation参数来约束长度

我们只需要用到长度

以及强制上限的最大参数(代码清单6.16)

随着我们的测试套件再次通过

我们可以继续进行更具挑战性的验证:电子邮件格式

练习

使用太长的名称和电子邮件创建一个新用户并确认它无效

长度验证生成的错误消息是什么?

6.2.4 格式验证

我们对name属性的验证仅强制执行最小约束

任何51个字符以下的非空白名称都可以

但当然,email属性必须满足作为有效电子邮件地址的更严格要求

到目前为止,我们只拒绝了空白电子邮件地址

在本节中,我们将要求电子邮件地址符合熟悉的模式user@example.com

测试和验证都不是详尽无遗的

只要足以接受大多数有效的电子邮件地址

并拒绝大多数无效的电子邮件地址

我们将从几个涉及有效和无效地址集合的测试开始

要创建这些集合

我们有必要了解用于创建字符串数组的有用的%w []技术

如此控制台会话中所示

在这里

我们使用每种方法迭代地址数组的元素(第4.3.2节)

有了这个技术

我们准备编写一些基本的电子邮件格式验证测试

因为电子邮件格式验证很棘手且容易出错

所以我们将从有效电子邮件地址的一些传递测试开始

以捕获验证中的任何错误

换句话说

我们不仅要确保拒绝像user@example,com这样的无效电子邮件地址

而且还要接受像user@example.com这样的有效地址

即使我们强制执行验证约束也是如此

目前,当然,它们将被接受

因为所有非空白电子邮件地址当前都是有效的

有效电子邮件地址的代表性样本的结果如代码清单6.18所示

请注意

我们在断言中包含了一个可选的第二个参数

其中包含一个自定义错误消息

在这种情况下

它会标识导致测试失败的地址

这使用第4.3.3节中提到的插值检查方法

包含导致任何失败的特定地址

在使用如代码清单6.18的每个循环的测试中特别有用

否则,任何失败只会识别行号

这对于所有电子邮件地址都是相同的

并且不足以识别问题的根源

接下来

我们将添加各种无效电子邮件地址无效的测试

例如user@example,com(逗号代替dot)

和user_at_foo.org(缺少’@’符号)

如代码清单6.18所示

代码清单6.19包含一个自定义错误消息

用于标识导致任何失败的确切地址

此时,测试应为红色

电子邮件格式验证的应用程序代码使用格式验证

其工作方式如下:

validates :email, format: { with: // }

这会使用给定的正则表达式(或正则表达式)验证属性

这是一种用于匹配字符串模式的强大(通常是神秘的)语言

这意味着我们需要构造一个正则表达式来匹配有效的电子邮件地址

而不匹配无效的。

根据官方电子邮件标准

实际上存在用于匹配电子邮件地址的完整正则表达式

但它是庞大的,模糊的,并且可能适得其反

在本教程中

我们将采用更实用的正则表达式

并且已被证明是强大、实用的

这是它的样子:

VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i

为了帮助理解

表6.1将其逐一细化分解

虽然通过学习表6.1可以学到很多东西

但为了真正理解正则表达式

我认为使用像Rubular这样的交互式正则表达式匹配器是必不可少的(图6.7)

Rubular网站有一个漂亮的交互式界面

用于制作正则表达式

以及 一个方便的正则表达式快速参考

我们推荐通过浏览器窗口打开Rubular来学习表6.1

书读百遍不如实际操作一遍

注意:如果你在Rubular中使用表6.1中的正则表达式

我建议不要使用\ A和\ z字符

这样你就可以在给定的测试字符串中一次匹配多个电子邮件地址

另请注意,正则表达式只包括 斜杠/…/中的字符

因此在使用Rubular时应省略它们

将表6.1中的正则表达式应用于电子邮件格式验证会生成代码清单6.21中的代码

这里的正则表达式VALID_EMAIL_REGEX是一个常量

在Ruby中用大写字母开头的名称表示

确保只有与该模式匹配的电子邮件地址才会被视为有效

上面的表达式有一个小弱点

它允许包含连续点的无效地址

例如foo @ bar..com

更新代码清单6.21中的正则表达式以修复此缺陷留作练习(第6.2.4.1节)

此时,测试应为绿色

这意味着只剩下一个约束:强制执行电子邮件唯一性

练习

1.通过将代码清单6.18中的有效地址

和代码清单6.19中的无效地址

粘贴到Rubular的测试字符串区域

确认清单6.21中的正则表达式

匹配所有有效地址而不匹配任何无效地址。

2. 如上所述

代码清单6.21中的电子邮件正则表达式

允许在域名中具有连续点的无效电子邮件地址

即foo@bar..com形式的地址

将此地址添加到代码清单6.19中的无效地址列表中以获得失败的测试

然后使用代码清单6.23中显示的更复杂的正则表达式来通过测试

3. 将foo@bar..com添加到Rubular的地址列表中

并确认代码清单6.23中显示的正则表达式匹配所有有效地址

而不是任何无效地址

6.2.5 唯一性验证

为了强制执行电子邮件地址的唯一性

以便我们可以将它们用作用户名

我们将使用validates方法的:unique选项

但要注意

有一个重要的警告

所以不要只是略读这一部分

你需要仔细阅读

我们将从一些简短的测试开始

在我们之前的模型测试中

我们主要使用User.new

它只是在内存中创建一个Ruby对象

但是对于唯一性测试

我们实际上需要将记录放入数据库

最初的重复电子邮件测试见清单6.24

这里的方法是使用@user.dup使用户具有与@user相同的电子邮件地址

这将创建具有相同属性的重复用户

由于我们然后保存@user

因为重复的用户具有已存在于数据库中的电子邮件地址

所以不应该有效

我们可以通过添加uniqueness:true来验证电子邮件唯一性

如代码清单6.25所示

但是,这还不够

电子邮件地址通常被处理为好像不区分大小写

即,foo@bar.com的处理方式与FOO@BAR.COM或FoO@BAr.coM相同

因此我们的验证也应该包含此内容

因此,重要的是测试不区分大小写

我们使用代码清单6.26中的代码

这里我们在字符串上使用upcase方法,见4.3.2节

此测试与初始重复的电子邮件测试执行相同的操作

但使用大写的电子邮件地址

如果这个测试感觉有点抽象

请继续启动控制台:

当然,duplicate_user.valid? 目前是正确的

因为唯一性验证区分大小写

但我们希望它是错误的

幸运的是:uniqueness接受一个选项:case_sensitive

仅用于此目的(代码清单6.27)

请注意

我们在代码清单6.25中用case_sensitive:false替换了清单6.25中的true

Rails推断,唯一性也应该是正确的

此时

我们的应用程序

成功添加了邮件地址的唯一性验证

我们的测试套件应该通过

只有一个小问题

即Active Record唯一性验证不保证数据库级别的唯一性

例如:

Alice报名参加示例应用程序,地址为alice@wonderland.com

爱丽丝不小心点击“提交”两次,快速连续发送两个请求

发生以下序列:请求1在内存中创建通过验证的用户

请求2执行相同的操作

请求1的用户被保存,请求2的用户被保存

结果:尽管具有唯一性验证

但具有完全相同的电子邮件地址的两个用户记录

如果上面的序列似乎难以置信

请相信我,它不是

它可能发生在任何具有大量流量的Rails网站上

幸运的是,该解决方案很容易实现

我们只需要在数据库级别和模型级别强制执行唯一性

我们的方法是在电子邮件列上创建数据库索引(方框6.2)

然后要求索引是唯一的

方框6.2 数据库索引

在数据库中创建列时

考虑是否需要按该列查找记录非常重要

例如,考虑清单6.2中迁移创建的email属性

当我们允许用户从第7章开始登录sample_app时

我们需要找到与提交的电子邮件地址相对应的用户记录

不幸的是,基于目前幼稚的数据模型

通过电子邮件地址查找用户的唯一方法

是查看数据库中的每个用户行

并将其电子邮件属性与给定的电子邮件进行比较

这意味着我们可能必须检查每一行

因为用户可能是数据库中的最后一个

这在数据库业务中称为全表扫描

对于具有数千个用户的真实站点而言

这是一件坏事

在电子邮件列上添加索引可以解决问题

要理解数据库索引

了解一下书籍索引的类比是有帮助的

在一本书中

为了找到给定字符串的所有出现

比如说“foobar”

你必须扫描每一页的“foobar”

全表扫描的纸质版本

另一方面

使用书籍索引

您只需在索引中查找“foobar”即可查看包含“foobar”的所有页面

数据库索引的工作方式基本相同

电子邮件索引表示我们的数据建模要求的更新

其中(如第6.1.1节所述)是使用迁移在Rails中处理的

我们在6.1.1节中看到

生成User模型会自动创建一个新的迁移(代码清单6.2)

在本例中,我们正在向现有模型添加结构

因此我们需要使用迁移生成器直接创建迁移

$rails generate migration add_index_to_users_email

与用户迁移不同

电子邮件唯一性迁移不是预定义的

因此我们需要使用代码清单6.29填写其内容

这使用名为add_index的Rails方法在users表的email列上添加索引

索引本身并不强制唯一性

但选项unique:true强制了唯一

最后一步是迁移数据库:

$ rails db:migrate

如果此操作失败

请尝试退出任何正在运行的沙盒控制台会话

因为他们会阻止数据库迁移

此时,测试套件应该是红色的

因为违反了数据中的唯一性约束

其中包含测试数据库的样本数据

用户工具在清单6.1中自动生成

如清单6.30所示

电子邮件地址不是唯一的

它们也无效,但示例数据无法通过验证

因为在第8章之前我们不需要示例数据

现在我们只需删除它们

留下一个空的示例数据文件(代码清单6.31)

解决了唯一性问题

我们还需要做出一项改进

以确保电子邮件的唯一性

一些数据库适配器使用区分大小写的索引

考虑到字符串“Foo@ExAMPle.CoM”和“foo@example.com”是不同的

但我们的应用程序将这些地址视为相同

为了避免这种不兼容性

我们将对所有小写地址进行标准化

将“Foo@ExAMPle.CoM”转换为“foo@example.com”

然后再将其保存到数据库中

执行此操作的方法是使用回调

这是一种在Active Record对象的生命周期中的特定点调用的方法

在本例中,该点是在保存对象之前

因此我们将在保存用户之前使用before_save回调来封装email属性

结果如代码清单6.32所示

这只是第一个实现

我们将在第11.1节再次讨论这个主题

我们将使用首选的方法引用约定来定义回调

代码清单6.32中的代码将一个块传递给before_save回调

并使用downcase字符串方法将用户的电子邮件地址设置为其当前值的小写版本

编写电子邮件地址小写的测试留待练习(第6.2.5.1节)

在代码清单6.32中,我们可以将赋值写为

self.email = self.email.downcase

(其中self指的是当前用户)

但在User模型中,self关键字在右侧是可选的

self.email = email.downcase

我们在回文方法(第4.4.2节)的反向背景下简要地遇到了这个想法

该方法也指出self在赋值中不是可选的,所以

email = email.downcase

不行

我们将在9.1节中更深入地讨论这个主题

此时,上面的Alice场景将正常工作

数据库将根据第一个请求保存用户记录

并且它将拒绝第二个保存

因为重复的电子邮件地址违反了唯一性约束

错误将出现在Rails日志中,但这不会造成任何伤害

此外,在电子邮件属性上添加此索引可实现第二个目标

在第6.1.4节中简要提及

如框6.2所示

电子邮件属性上的索引通过在通过电子邮件地址查找用户时阻止全表扫描来修复潜在的效率问题

练习

1.从代码清单6.32中为电子邮件小写化添加一个测试

如代码清单6.33所示

此测试使用reload方法从数据库重新加载值

使用assert_equal方法测试相等性

要验证代码清单6.33是否正确测试

请注释掉before_save行以获得红色

然后取消注释以获得绿色

2.通过运行测试套件

验证可以使用“bang”方法email.downcase!写入before_save回调

直接修改email属性

如代码清单6.34所示。

6.3 添加安全密码

现在我们已经为名称和电子邮件字段定义了验证

我们已准备好添加最后一个基本用户属性

安全密码

该方法是要求每个用户都有密码(使用密码确认)

然后在数据库中存储密码的哈希版本

这里有一些混淆的可能性

在目前的上下文中

哈希不是指4.3.3节中的Ruby数据结构

而是指将不可逆哈希函数应用于输入数据的结果

我们还将添加一个基于给定密码对用户进行身份验证的方法

我们将在第8章中使用该方法允许用户登录该站点。

验证用户的方法是获取提交的密码

对其进行哈希

并将结果与存储在数据库中的散列值进行比较

如果两者匹配

则提交的密码正确并且用户已通过身份验证

通过比较散列值而不是原始密码

我们将能够在不存储密码的情况下对用户进行身份验证

这意味着,即使我们的数据库遭到入侵

我们用户的密码仍然是安全的

6.3.1 哈希密码

大多数安全密码机制将使用名为has_secure_password的单个Rails方法实现

我们将在User模型中包含如下:

classUser

.

.

.

has_secure_passwordend

当包含在上面的模型中时

这一方法添加了以下功能

能够将安全哈希的password_digest属性保存到数据库

一对虚拟属性password和password_confirmation

包括创建对象时的状态验证和要求它们匹配的验证

一种authenticate方法

在密码正确时返回用户(否则返回false)

has_secure_password发挥其魔力的唯一要求是

相应的模型具有名为password_digest的属性

digest来自加密哈希函数的术语

在此上下文中

哈希密码和密码摘要是同义词

对于User模型,这将导致数据模型如图6.8所示

要实现图6.8中的数据模型

我们首先为password_digest列生成适当的迁移

我们可以选择我们想要的任何迁移名称

但是使用to_users结束名称很方便

因为在这种情况下

Rails会自动构建迁移以向users表添加列

迁移名称为add_password_digest_to_users的结果如下所示

$rails generate migration add_password_digest_to_users password_digest:string

这里我们还提供了参数password_digest:string

其中包含我们要创建的属性的名称和类型

将其与清单6.1中的users表的原始版本进行比较

其中包括参数name:string和email:string

通过包含password_digest:string

我们已经为Rails提供了足够的信息来为我们构建整个迁移

如代码清单6.35所示

代码清单6.35使用add_column方法将password_digest列添加到users表中

要应用它,我们只需要迁移数据库

$ rails db:migrate

password digest

has_secure_password使用名为bcrypt的最先进的哈希函数

通过使用bcrypt对密码进行散列

我们确保攻击者即使设法获取数据库副本也无法登录该站点

要在sample_app中使用bcrypt

我们需要将bcrypt gem添加到我们的Gemfile中(代码清单6.36)

6.3.2 用户有安全密码

现在我们已经为User模型提供了所需的password_digest属性

并安装了bcrypt

我们已准备好将has_secure_password添加到User模型

如代码清单6.37所示

如清单6.37中的红色指示所示

测试现在失败

您可以在命令行确认

原因是,如6.3.1节所述

has_secure_password对virtual password和password_confirmation属性强制执行验证

但代码清单6.26中的测试创建了一个没有这些属性的@user变量

因此,为了让测试套件再次通过

我们只需要添加密码及其确认

如代码清单6.39所示

请注意

根据Ruby的哈希语法(第4.3.3节)的要求

setup方法中的第一行最后包含一个额外的逗号

将此逗号保留为会产生语法错误

您应该使用自学能力(方框1.1)来识别和解决这些错误

此时测试应为绿色

我们稍后会看到将has_secure_password添加到User模型(第6.3.4节)的好处

但首先我们将添加对密码安全性的最低要求

练习

1.确认具有有效名称和电子邮件的用户整体无效

2.没有密码的用户有什么错误消息

6.3.3 最低密码标准

一般来说,最好对密码实施一些最低标准

以使其更难猜测

在Rails中有许多强制密码强度的选项

但为简单起见

我们只强制执行最小长度并要求密码不为空

选择6的长度作为合理的最小值导致验证测试如代码清单6.41所示

注意使用紧凑的多重赋值

@user.password = @user.password_confirmation = "a" * 5

在代码清单6.41中

这安排为密码及其确认同时分配一个特定值

在这种情况下,长度为5的字符串

使用字符串乘法构造,如代码清单6.14所示

您可以通过引用用户名的相应最大验证

来猜测强制执行最小长度约束的代码(代码清单6.16)

validates :password, length: { minimum: 6 }

将其与状态验证(第6.2.2节)相结合以确保非空密码

这将导致用户模型如代码清单6.42所示

事实证明,has_secure_password方法包括状态验证

但不幸的是它只适用于具有空密码的记录

这允许用户创建无效的密码

如’六个空格

练习

1.确认具有有效名称和电子邮件但密码太短的用户无效

2.有哪些相关的错误消息?

6.3.4 创建和验证用户

现在基本的用户模型已经完成

我们将在数据库中创建一个用户

作为在第7.1节中创建页面以显示用户信息的准备

我们还将更具体地了解将has_secure_password添加到User模型的效果

包括检查重要的authenticate方法

由于用户尚未通过网络注册sample_app

这是第7章的目标

我们将使用Rails控制台手动创建新用户

为方便起见

我们将使用第6.1.3节中讨论的create方法

但在本例中,我们将注意不要在沙箱中启动

以便将生成的用户保存到数据库中

这意味着启动普通的rails控制台会话

然后创建一个具有有效名称和电子邮件地址以及有效密码和匹配确认的用户

要检查这是否有效

让我们使用DB Browser for SQLite查看开发数据库中的结果用户表

如图6.9.21所示

如果您使用的是云IDE,则应下载数据库文件,如图6.5所示

请注意,列对应于图6.8中定义的数据模型的属性

回到控制台,通过查看password_digest属性

我们可以看到代码6.42中的has_secure_password的效果

这是用于初始化用户对象的密码(“foobar”)的哈希版本

因为它是使用bcrypt构造的

所以使用摘要来发现原始密码在计算上是不切实际的

如6.3.1节所述

has_secure_password会自动将authenticate方法添加到相应的模型对象中

此方法通过计算其摘要

并将结果与数据库中的password_digest进行比较

来确定给定密码对特定用户是否有效

对于我们刚刚创建的用户

我们可以尝试使用以下几个无效密码

在第8章中

我们将使用authenticate方法将注册用户签入我们的站点

事实上,对我们来说

验证返回用户本身并不重要

所有重要的是它返回一个在布尔上下文中为true的值

回顾4.2.3节!! 将对象转换为其对应的布尔值

我们可以看到user.authenticate很好地完成了这项工作

>>!!user.authenticate("foobar")

练习

1.退出并重新启动控制台,然后找到在此部分中创建的用户

2.尝试通过分配新名称并调用save来更改名称。 它为什么不起作用?

3.更新用户名以使用您的姓名。 提示:第6.1.5节介绍了必要的技术

6.4 结论

从头开始,在本章中

我们创建了一个具有名称,电子邮件和密码属性的工作用户模型

以及对其值强制执行若干重要约束的验证

此外,我们还可以使用给定的密码安全地对用户进行身份验证

这对十二行代码来说算性价比很高了

在下一章的第7章中

我们将创建一个form注册表单来创建新用户

以及一个显示每个用户信息的页面

在第8章中,我们将使用6.3节中的身份验证机制让用户登录该站点

如果你正在使用Git

现在是时候提交了

如果你有一段时间没有这样做:

6.4.1 本章学到的内容

迁移允许我们修改应用程序的数据模型

Active Record附带了大量用于创建和操作数据模型的方法

Active Record验证允许我们对模型中的数据设置约束

常见验证包括存在,长度和格式

正则表达式虽然神秘但功能强大

定义数据库索引可提高查找效率,同时允许在数据库级别强制执行唯一性

我们可以使用内置的has_secure_password方法为模型添加安全密码

  • 发表于:
  • 原文链接https://kuaibao.qq.com/s/20181210A12T5S00?refer=cp_1026
  • 腾讯「云+社区」是腾讯内容开放平台帐号(企鹅号)传播渠道之一,根据《腾讯内容开放平台服务协议》转载发布内容。

扫码关注云+社区

领取腾讯云代金券