# 6.3 添加安全密码
我们已经为 `name` 和 `email` 字段添加了验证规则,现在要加入用户所需的最后一个常规属性:安全密码。每个用户都要设置一个密码(还要二次确认),数据库中则存储经过哈希加密后的密码。(你可能会困惑。这里所说的“哈希”不是 [4.3.3 节](chapter4.html#hashes-and-symbols)介绍的 Ruby 数据结构,而是经过不可逆[哈希算法](http://en.wikipedia.org/wiki/Hash_function)计算得到的结果。)我们还要加入基于密码的认证验证机制,[第 8 章](chapter8.html#log-in-log-out)会利用这个机制实现用户登录功能。
认证用户的方法是,获取用户提交的密码,哈希加密,再和数据库中存储的密码哈希值对比,如果二者一致,用户提交的就是正确的密码,用户的身份也就通过认证了。我们要对比的是密码哈希值,而不是原始密码,所以不用在数据库中存储用户的密码。因此,就算被脱库了,用户的密码仍然安全。
## 6.3.1 计算密码哈希值
我们使用的安全密码机制基本上由一个 Rails 方法即可实现,这个方法是 `has_secure_password`。我们要在用户模型中调用这个方法,如下所示:
```
class User < ActiveRecord::Base
.
.
.
has_secure_password
end
```
在模型中调用这个方法后,会自动添加如下功能:
* 在数据库中的 `password_digest` 列存储安全的密码哈希值;
* 获得一对“虚拟属性”,[[17](#fn-17)]`password` 和 `password_confirmation`,而且创建用户对象时会执行存在性验证和匹配验证;
* 获得 `authenticate` 方法,如果密码正确,返回对应的用户对象,否则返回 `false`。
`has_secure_password` 发挥功效的唯一要求是,对应的模型中有个名为 `password_digest` 的属性。(“digest”(摘要)是[哈希加密算法](http://en.wikipedia.org/wiki/Cryptographic_hash_function)中的术语。“密码哈希值”和“密码摘要”是一个意思。)[[18](#fn-18)]对用户模型来说,我们要实现如[图 6.7](#fig-user-model-password-digest) 所示的数据模型。
![user model password digest 3rd edition](https://box.kancloud.cn/2016-05-11_5732bd0d53d67.png)图 6.7:用户数据模型,多了一个 `password_digest` 属性
为了实现[图 6.7](#fig-user-model-password-digest) 中的数据模型,首先要创建一个适当的迁移文件,添加 `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](#listing-generate-user-model) 中的命令对比一下,那个命令生成创建 `users` 表的迁移,指定了 `name:string` 和 `email:string` 两个参数。)加入 `password_digest:string` 后,我们为 Rails 提供了足够的信息,它会为我们生成一个完整的迁移,如[代码清单 6.32](#listing-password-migration) 所示。
##### 代码清单 6.32:在 `users` 表中添加 `password_digest` 列的迁移
db/migrate/[timestamp]_add_password_digest_to_users.rb
```
class AddPasswordDigestToUsers < ActiveRecord::Migration
def change
add_column :users, :password_digest, :string
end
end
```
这个迁移使用 `add_column` 方法把 `password_digest` 列添加到 `users` 表中。执行下述命令在数据库中运行迁移:
```
$ bundle exec rake db:migrate
```
`has_secure_password` 方法使用先进的 [bcrypt](http://en.wikipedia.org/wiki/Bcrypt) 哈希算法计算密码摘要。使用 bcrypt 计算密码哈希值,就算攻击者设法获得了数据库副本也无法登录网站。为了在演示应用中使用 bcrypt,我们要把 `bcrypt` gem 添加到 `Gemfile` 中,如[代码清单 6.33](#listing-bcrypt-ruby) 所示。
##### 代码清单 6.33:把 `bcrypt` gem 添加到 `Gemfile` 中
```
source 'https://rubygems.org'
gem 'rails', '4.2.2'
gem 'bcrypt', '3.1.7'
.
.
.
```
然后像往常一样,执行 `bundle install` 命令:
```
$ bundle install
```
## 6.3.2 用户有安全的密码
现在我们已经在用户模型中添加了 `password_digest` 属性,也安装了 bcrypt,下面可以在用户模型中添加 `has_secure_password` 方法了,如[代码清单 6.34](#listing-has-secure-password) 所示。
##### 代码清单 6.34:在用户模型中添加 `has_secure_password` 方法 RED
app/models/user.rb
```
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password end
```
如[代码清单 6.34](#listing-has-secure-password) 中的“**RED**”所示,测试现在失败,我们可以在命令行中执行下述命令确认:
##### 代码清单 6.35:**RED**
```
$ bundle exec rake test
```
我们在 [6.3.1 节](#a-hashed-password)说过,`has_secure_password` 会在 `password` 和 `password_confirmation` 两个虚拟属性上执行验证,但是现在[代码清单 6.25](#listing-validates-uniqueness-of-email-case-insensitive-test) 中的 `@user` 变量没有这两个属性:
```
def setup
@user = User.new(name: "Example User", email: "user@example.com")
end
```
所以,为了让测试组件通过,我们要添加这两个属性,如[代码清单 6.36](#listing-test-with-password-confirmation) 所示。
##### 代码清单 6.36:添加密码和密码确认 GREEN
test/models/user_test.rb
```
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar") end
.
.
.
end
```
现在测试应该可以通过了:
##### 代码清单 6.37:**GREEN**
```
$ bundle exec rake test
```
[6.3.4 节](#creating-and-authenticating-a-user)会看到在用户模型中添加 `has_secure_password` 的作用。在此之前,为了密码的安全,先添加一个小要求。
## 6.3.3 密码的最短长度
一般来说,最好为密码做些限制,让别人更难猜测。在 Rails 中增强密码强度有很多方法,简单起见,我们只限制最短长度,而且要求密码不能为空。最短长度为 6 是个不错的选择,针对这个验证的测试如[代码清单 6.38](#listing-minimum-password-length-test) 所示。
##### 代码清单 6.38:测试密码的最短长度 RED
test/models/user_test.rb
```
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
@user = User.new(name: "Example User", email: "user@example.com",
password: "foobar", password_confirmation: "foobar")
end
.
.
.
test "password should be present (nonblank)" do @user.password = @user.password_confirmation = " " * 6 assert_not @user.valid? end
test "password should have a minimum length" do @user.password = @user.password_confirmation = "a" * 5 assert_not @user.valid? end end
```
注意这段代码中使用的双重赋值:
```
@user.password = @user.password_confirmation = "a" * 5
```
这行代码同时为 `password` 和 `password_confirmation` 赋值,值是长度为 5 的字符串,使用字符串连乘创建。
参照 `name` 属性的 `maximum` 验证([代码清单 6.16](#listing-length-validation)),你或许能猜到限制最短长度所需的代码:
```
validates :password, length: { minimum: 6 }
```
在上述代码的基础上,还要加上存在性验证,得出的用户模型如[代码清单 6.39](#listing-password-implementation) 所示。(`has_secure_password` 方法本身会验证存在性,但是可惜,只会验证有没有密码,因此用户可以创建 “ ”(6 个空格)这样的无效密码。)
##### 代码清单 6.39:实现安全密码的全部代码 GREEN
app/models/user.rb
```
class User < ActiveRecord::Base
before_save { self.email = email.downcase }
validates :name, presence: true, length: { maximum: 50 }
VALID_EMAIL_REGEX = /\A[\w+\-.]+@[a-z\d\-.]+\.[a-z]+\z/i
validates :email, presence: true, length: { maximum: 255 },
format: { with: VALID_EMAIL_REGEX },
uniqueness: { case_sensitive: false }
has_secure_password
validates :password, presence: true, length: { minimum: 6 } end
```
现在,测试应该可以通过了:
##### 代码清单 6.40:**GREEN**
```
$ bundle exec rake test:models
```
## 6.3.4 创建并认证用户
至此,基本的用户模型已经完成了。接下来,我们要在数据库中创建一个用户,为 [7.1 节](chapter7.html#showing-users)开发的用户资料页面做准备。同时也看一下在用户模型中添加 `has_secure_password` 的效果,还要用一下重要的 `authenticate` 方法。
因为现在还不能在网页中注册([第 7 章](chapter7.html#sign-up)实现),我们要在控制台中手动创建新用户。为了方便,我们会使用 [6.1.3 节](#creating-user-objects)介绍的 `create` 方法。注意,不要在沙盒模式中启用控制台,否则结果不会存入数据库。所以我们要使用 `rails console` 启动普通的控制台,然后使用有效的名字和电子邮件地址,以及密码和密码确认,创建一个用户:
```
$ rails console
>> User.create(name: "Michael Hartl", email: "mhartl@example.com",
?> password: "foobar", password_confirmation: "foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-09-11 14:26:42", updated_at: "2014-09-11 14:26:42",
password_digest: "$2a$10$sLcMI2f8VglgirzjSJOln.Fv9NdLMbqmR4rdTWIXY1G...">
```
为了确认结果,我们使用 SQLite 数据库浏览器看一下开发数据库(`db/development.sqlite3`)中的 `users` 表,如[图 6.8](#fig-sqlite-user-row) 所示。[[19](#fn-19)]留意[图 6.7](#fig-user-model-password-digest) 中数据模型的各个属性。
![sqlite user row with password 3rd edition](https://box.kancloud.cn/2016-05-11_5732bd0d62956.png)图 6.8:SQLite 数据库(`db/development.sqlite3`)中的一个用户记录
回到控制台,查看 `password_digest` 属性的值,由此可以看出[代码清单 6.39](#listing-password-implementation)中 `has_secure_password` 的作用:
```
>> user = User.find_by(email: "mhartl@example.com")
>> user.password_digest
=> "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQWITUYlG3XVy"
```
这是创建用户对象时指定的密码(`"foobar"`)的哈希值。这个值由 bcrypt 计算得出,很难反推出原始密码。[[20](#fn-20)]
[6.3.1 节](#a-hashed-password)说过,`has_secure_password` 会自动在对应的模型对象中添加 `authenticate` 方法。这个方法会计算给定密码的哈希值,然后和数据库中 `password_digest` 列中的值比较,以此判断用户提供的密码是否正确。我们可以在刚创建的用户上试几个错误密码:
```
>> user.authenticate("not_the_right_password")
false
>> user.authenticate("foobaz")
false
```
我们提供的密码都是错误的,所以 `user.authenticate` 返回 `false`。如果提供正确的密码,`authenticate` 方法会返回数据库中对应的用户:
```
>> user.authenticate("foobar")
=> #<User id: 1, name: "Michael Hartl", email: "mhartl@example.com",
created_at: "2014-07-25 02:58:28", updated_at: "2014-07-25 02:58:28",
password_digest: "$2a$10$YmQTuuDNOszvu5yi7auOC.F4G//FGhyQSWCpghqRWQW...">
```
[第 8 章](chapter8.html#log-in-log-out)会使用 `authenticate` 方法把注册的用户登入网站。其实,`authenticate` 方法返回的用户对象并不重要,关键是这个值是“真值”。因为用户对象不是 `nil`,也不是 `false`,所以能很好地完成任务:[[21](#fn-21)]
```
>> !!user.authenticate("foobar")
=> true
```
- Ruby on Rails 教程
- 致中国读者
- 序
- 致谢
- 作者译者简介
- 版权和代码授权协议
- 第 1 章 从零开始,完成一次部署
- 1.1 简介
- 1.2 搭建环境
- 1.3 第一个应用
- 1.4 使用 Git 做版本控制
- 1.5 部署
- 1.6 小结
- 1.7 练习
- 第 2 章 玩具应用
- 2.1 规划应用
- 2.2 用户资源
- 2.3 微博资源
- 2.4 小结
- 2.5 练习
- 第 3 章 基本静态的页面
- 3.1 创建演示应用
- 3.2 静态页面
- 3.3 开始测试
- 3.4 有点动态内容的页面
- 3.5 小结
- 3.6 练习
- 3.7 高级测试技术
- 第 4 章 Rails 背后的 Ruby
- 4.1 导言
- 4.2 字符串和方法
- 4.3 其他数据类型
- 4.4 Ruby 类
- 4.5 小结
- 4.6 练习
- 第 5 章 完善布局
- 5.1 添加一些结构
- 5.2 Sass 和 Asset Pipeline
- 5.3 布局中的链接
- 5.4 用户注册:第一步
- 5.5 小结
- 5.6 练习
- 第 6 章 用户模型
- 6.1 用户模型
- 6.2 用户数据验证
- 6.3 添加安全密码
- 6.4 小结
- 6.5 练习
- 第 7 章 注册
- 7.1 显示用户的信息
- 7.2 注册表单
- 7.3 注册失败
- 7.4 注册成功
- 7.5 专业部署方案
- 7.6 小结
- 7.7 练习
- 第 8 章 登录和退出
- 8.1 会话
- 8.2 登录
- 8.3 退出
- 8.4 记住我
- 8.5 小结
- 8.6 练习
- 第 9 章 更新,显示和删除用户
- 9.1 更新用户
- 9.2 权限系统
- 9.3 列出所有用户
- 9.4 删除用户
- 9.5 小结
- 9.6 练习
- 第 10 章 账户激活和密码重设
- 10.1 账户激活
- 10.2 密码重设
- 10.3 在生产环境中发送邮件
- 10.4 小结
- 10.5 练习
- 10.6 证明超时失效的比较算式
- 第 11 章 用户的微博
- 11.1 微博模型
- 11.2 显示微博
- 11.3 微博相关的操作
- 11.4 微博中的图片
- 11.5 小结
- 11.6 练习
- 第 12 章 关注用户
- 12.1 “关系”模型
- 12.2 关注用户的网页界面
- 12.3 动态流
- 12.4 小结
- 12.5 练习