# 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) 所示的數據模型。
圖 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) 中數據模型的各個屬性。
圖 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 練習