<ruby id="bdb3f"></ruby>

    <p id="bdb3f"><cite id="bdb3f"></cite></p>

      <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
        <p id="bdb3f"><cite id="bdb3f"></cite></p>

          <pre id="bdb3f"></pre>
          <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

          <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
          <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

          <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                <ruby id="bdb3f"></ruby>

                ??一站式輕松地調用各大LLM模型接口,支持GPT4、智譜、豆包、星火、月之暗面及文生圖、文生視頻 廣告
                # 10.1 賬戶激活 目前,用戶注冊后立即就能完全控制自己的賬戶([第 7 章](chapter7.html#sign-up))。本節,我們要添加一步,激活用戶的賬戶,從而確認用戶擁有注冊時使用的電子郵件地址。為此,我們要為用戶創建激活令牌和摘要,然后給用戶發送一封電子郵件,提供包含令牌的鏈接。用戶點擊這個鏈接后,激活這個賬戶。 我們要采取的實現步驟與注冊用戶([8.2 節](chapter8.html#logging-in))和記住用戶([8.4 節](chapter8.html#remember-me))差不多,如下所示: 1. 用戶一開始處于“未激活”狀態; 2. 用戶注冊后,生成一個激活令牌和對應的激活摘要; 3. 把激活摘要存儲在數據庫中,然后給用戶發送一封電子郵件,提供一個包含激活令牌和用戶電子郵件地址的鏈接;[[2](#fn-2)] 4. 用戶點擊這個鏈接后,使用電子郵件地址查找用戶,并且對比令牌和摘要; 5. 如果令牌和摘要匹配,就把狀態由“未激活”改為“已激活”。 因為與密碼和記憶令牌類似,實現賬戶激活(以及密碼重設)功能時可以繼續使用前面的很多方法,包括 `User.digest`、`User.new_token` 和修改過的 `user.authenticated?`。這幾個功能(包括 [10.2 節](#password-reset)要實現的密碼重設)之間的對比,如[表 10.1](#table-password-token-digest) 所示。我們會在 [10.1.3 節](#activating-the-account)定義可用于表中所有情況的通用版 `authenticated?` 方法。 表 10.1:登錄,記住狀態,賬戶激活和密碼重設之間的對比 | 查找方式 | 字符串 | 摘要 | 認證 | | --- | --- | --- | --- | | `email` | `password` | `password_digest` | `authenticate(password)` | | `id` | `remember_token` | `remember_digest` | `authenticated?(:remember, token)` | | `email` | `activation_token` | `activation_digest` | `authenticated?(:activation, token)` | | `email` | `reset_token` | `reset_digest` | `authenticated?(:reset, token)` | 和之前一樣,我們要在主題分支中開發新功能。讀到 [10.3 節](#email-in-production)會發現,賬戶激活和密碼重設需要共用一些電子郵件設置,合并到 `master` 分支之前,要把這些設置應用到這兩個功能上,所以在一個分支中開發這兩個功能比較方便: ``` $ git checkout master $ git checkout -b account-activation-password-resets ``` ## 10.1.1 資源 和會話一樣([8.1 節](chapter8.html#sessions)),我們要把“賬戶激活”看做一個資源,不過這個資源不對應模型,相關的數據(激活令牌和激活狀態)存儲在用戶模型中。然而,我們要通過標準的 REST URL 處理賬戶激活操作。激活鏈接會改變用戶的激活狀態,所以我們計劃在 `edit` 動作中處理。[[3](#fn-3)]所需的控制器使用下面的命令生成:[[4](#fn-4)] ``` $ rails generate controller AccountActivations --no-test-framework ``` 我們需要使用下面的方法生成一個 URL,放在激活郵件中: ``` edit_account_activation_url(activation_token, ...) ``` 因此,我們需要為 `edit` 動作設定一個具名路由——通過[代碼清單 10.1](#listing-account-activations-route) 中高亮顯示的那行 `resources` 實現。 ##### 代碼清單 10.1:添加賬戶激活所需的資源路由 config/routes.rb ``` Rails.application.routes.draw do root 'static_pages#home' get 'help' => 'static_pages#help' get 'about' => 'static_pages#about' get 'contact' => 'static_pages#contact' get 'signup' => 'users#new' get 'login' => 'sessions#new' post 'login' => 'sessions#create' delete 'logout' => 'sessions#destroy' resources :users resources :account_activations, only: [:edit] end ``` 接下來,我們需要一個唯一的激活令牌,用來激活用戶。密碼、記憶令牌和密碼重設([10.2 節](#password-reset))需要考慮很多安全隱患,因為如果攻擊者獲取了這些信息就能完全控制賬戶。賬戶激活則不需要這么麻煩,但如果不哈希激活令牌,賬戶也有一定危險。[[5](#fn-5)]所以,參照記住登錄狀態的做法([8.4 節](chapter8.html#remember-me)),我們會公開令牌,而在數據庫中存儲哈希摘要。這么做,我們可以使用下面的方式獲取激活令牌: ``` user.activation_token ``` 使用下面的代碼認證用戶: ``` user.authenticated?(:activation, token) ``` (不過得先修改[代碼清單 8.33](chapter8.html#listing-authenticated-p) 中定義的 `authenticated?` 方法。)我們還要定義一個布爾值屬性 `activated`,使用自動生成的布爾值方法檢查用戶的激活狀態(類似 [9.4.1 節](chapter9.html#administrative-users)使用的方法): ``` if user.activated? ... ``` 最后,我們還要記錄激活的日期和時間,雖然本書用不到,但說不定以后需要使用。完整的數據模型如[圖 10.1](#fig-user-model-account-activation) 所示。 ![user model account activation](https://box.kancloud.cn/2016-05-11_57333066c61a2.png)圖 10.1:添加賬戶激活相關屬性后的用戶模型 下面的命令生成一個遷移,添加這些屬性。我們在命令行中指定了要添加的三個屬性: ``` $ rails generate migration add_activation_to_users \ > activation_digest:string activated:boolean activated_at:datetime ``` 和 `admin` 屬性一樣([代碼清單 9.50](chapter9.html#listing-admin-migration)),我們要把 `activated` 屬性的默認值設為 `false`,如[代碼清單 10.2](#listing-add-activation-to-users-migration) 所示。 ##### 代碼清單 10.2:添加賬戶激活所需屬性的遷移 db/migrate/[timestamp]_add_activation_to_users.rb ``` class AddActivationToUsers < ActiveRecord::Migration def change add_column :users, :activation_digest, :string add_column :users, :activated, :boolean, default: false add_column :users, :activated_at, :datetime end end ``` 然后像之前一樣,執行遷移: ``` $ bundle exec rake db:migrate ``` 因為每個新注冊的用戶都得激活,所以我們應該在創建用戶對象之前為用戶分配激活令牌和摘要。類似的操作在 [6.2.5 節](chapter6.html#uniqueness-validation)見過,那時我們要在用戶存入數據庫之前把電子郵件地址轉換成小寫形式。我們使用的是 `before_save` 回調和 `downcase` 方法([代碼清單 6.31](chapter6.html#listing-email-downcase))。`before_save` 回調在保存對象之前,包括創建對象和更新對象,自動調用。不過現在我們只想在創建用戶之前調用回調,創建激活摘要。為此,我們要使用 `before_create` 回調,按照下面的方式定義: ``` before_create :create_activation_digest ``` 這種寫法叫“方法引用”,Rails 會尋找一個名為 `create_activation_digest` 的方法,在創建用戶之前調用。(在[代碼清單 6.31](chapter6.html#listing-email-downcase) 中,我們直接把一個塊傳給 `before_save`。不過方法引用是推薦的做法。)`create_activation_digest` 方法只會在用戶模型內使用,沒必要公開。如 [7.3.2 節](chapter7.html#strong-parameters)所示,在 Ruby 中可以使用 `private` 實現這個需求: ``` private def create_activation_digest # 創建令牌和摘要 end ``` 在一個類中,`private` 之后的方法都會自動“隱藏”。我們可以在控制器會話中驗證這一點: ``` $ rails console >> User.first.create_activation_digest NoMethodError: private method `create_activation_digest' called for #<User> ``` 這個 `before_create` 回調的作用是為用戶分配令牌和對應的摘要,實現的方法如下所示: ``` self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) ``` 這里用到了實現“記住我”功能時用來生成令牌和摘要的方法。我們可以把這兩行代碼和[代碼清單 8.32](chapter8.html#listing-user-model-remember) 中的 `remember` 方法比較一下: ``` # 為了持久會話,在數據庫中記住用戶 def remember self.remember_token = User.new_token update_attribute(:remember_digest, User.digest(remember_token)) end ``` 二者之間的主要區別是,`remember` 方法中使用的是 `update_attribute`。因為,創建記憶令牌和摘要時,用戶已經存在于數據庫中了,而 `before_create` 回調在創建用戶之前執行。有了這個回調,使用 `User.new` 新建用戶后(例如用戶注冊后,參見[代碼清單 7.17](chapter7.html#listing-create-action-strong-parameters)),會自動賦值 `activation_token` 和 `activation_digest` 屬性,而且因為 `activation_digest` 對應數據庫中的一個列([圖 10.1](#fig-user-model-account-activation)),所以保存用戶時會自動把屬性的值存入數據庫。 綜上所述,用戶模型如[代碼清單 10.3](#listing-user-model-activation-code) 所示。因為激活令牌是虛擬屬性,所以我們又添加了一個 `attr_accessor`。注意,我們還把電子郵件地址轉換成小寫的回調改成了方法引用形式。 ##### 代碼清單 10.3:在用戶模型中添加賬戶激活相關的代碼 GREEN app/models/user.rb ``` class User < ActiveRecord::Base attr_accessor :remember_token, :activation_token before_save :downcase_email before_create :create_activation_digest validates :name, presence: true, length: { maximum: 50 } . . . private # 把電子郵件地址轉換成小寫 def downcase_email self.email = email.downcase end # 創建并賦值激活令牌和摘要 def create_activation_digest self.activation_token = User.new_token self.activation_digest = User.digest(activation_token) end end ``` 在繼續之前,我們還要修改種子數據,把示例用戶和測試用戶設為已激活,如[代碼清單 10.4](#listing-seed-users-activated) 和[代碼清單 10.5](#listing-fixture-users-activated) 所示。(`Time.zone.now` 是 Rails 提供的輔助方法,基于服務器使用的時區,返回當前時間戳。) ##### 代碼清單 10.4:激活種子數據中的用戶 db/seeds.rb ``` User.create!(name: "Example User", email: "example@railstutorial.org", password: "foobar", password_confirmation: "foobar", admin: true, activated: true, activated_at: Time.zone.now) 99.times do |n| name = Faker::Name.name email = "example-#{n+1}@railstutorial.org" password = "password" User.create!(name: name, email: email, password: password, password_confirmation: password, activated: true, activated_at: Time.zone.now) end ``` ##### 代碼清單 10.5:激活固件中的用戶 test/fixtures/users.yml ``` michael: name: Michael Example email: michael@example.com password_digest: <%= User.digest('password') %> admin: true activated: true activated_at: <%= Time.zone.now %> archer: name: Sterling Archer email: duchess@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> lana: name: Lana Kane email: hands@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> malory: name: Malory Archer email: boss@example.gov password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> <% 30.times do |n| %> user_<%= n %>: name: <%= "User #{n}" %> email: <%= "user-#{n}@example.com" %> password_digest: <%= User.digest('password') %> activated: true activated_at: <%= Time.zone.now %> <% end %> ``` 為了應用[代碼清單 10.4](#listing-seed-users-activated) 中的改動,我們要還原數據庫,然后像之前一樣寫入數據: ``` $ bundle exec rake db:migrate:reset $ bundle exec rake db:seed ``` ## 10.1.2 郵件程序 寫好模型后,我們要編寫發送賬戶激活郵件的代碼了。我們要使用 Action Mailer 庫創建一個郵件程序,在用戶控制器的 `create` 動作中發送一封包含激活鏈接的郵件。郵件程序的結構和控制器動作差不多,郵件模板使用視圖定義。這一節的任務是創建郵件程序,以及編寫視圖,寫入激活賬戶所需的激活令牌和電子郵件地址。 與模型和控制器一樣,我們可以使用 `rails generate` 生成郵件程序: ``` $ rails generate mailer UserMailer account_activation password_reset ``` 我們使用這個命令生成了所需的 `account_activation` 方法,以及 [10.2 節](#password-reset)要使用的 `password_reset` 方法。 生成郵件程序時,Rails 還為每個郵件程序生成了兩個視圖模板,一個用于純文本郵件,一個用于 HTML 郵件。賬戶激活郵件程序的兩個視圖如[代碼清單 10.6](#listing-generated-account-activation-view-text) 和[代碼清單 10.7](#listing-generated-account-activation-view-html) 所示。 ##### 代碼清單 10.6:生成的賬戶激活郵件視圖,純文本格式 app/views/user_mailer/account_activation.text.erb ``` UserMailer#account_activation <%= @greeting %>, find me in app/views/user_mailer/account_activation.text.erb ``` ##### 代碼清單 10.7:生成的賬戶激活郵件視圖,HTML 格式 app/views/user_mailer/account_activation.html.erb ``` <h1>UserMailer#account_activation</h1> <p> <%= @greeting %>, find me in app/views/user_mailer/account_activation.html.erb </p> ``` 我們看一下生成的郵件程序,了解它是如何工作的,如[代碼清單 10.8](#listing-generated-application-mailer) 和[代碼清單 10.9](#listing-generated-user-mailer)所示。代碼[代碼清單 10.8](#listing-generated-application-mailer) 設置了一個默認的發件人地址(`from`),整個應用中的全部郵件程序都會使用這個地址。(這個代碼清單還設置了各種郵件格式使用的布局。本書不會討論郵件的布局,生成的 HTML 和純文本格式郵件布局在 `app/views/layouts` 文件夾中。)[代碼清單 10.9](#listing-generated-user-mailer) 中的每個方法中都設置了收件人地址。在生成的代碼中還有一個實例變量 `@greeting`,這個變量可在郵件程序的視圖中使用,就像控制器中的實例變量可以在普通的視圖中使用一樣。 ##### 代碼清單 10.8:生成的 `ApplicationMailer` app/mailers/application_mailer.rb ``` class ApplicationMailer < ActionMailer::Base default from: "from@example.com" layout 'mailer' end ``` ##### 代碼清單 10.9:生成的 `UserMailer` app/mailers/user_mailer.rb ``` class UserMailer < ActionMailer::Base # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # # en.user_mailer.account_activation.subject # def account_activation @greeting = "Hi" mail to: "to@example.org" end # Subject can be set in your I18n file at config/locales/en.yml # with the following lookup: # # en.user_mailer.password_reset.subject # def password_reset @greeting = "Hi" mail to: "to@example.org" end end ``` 為了發送激活郵件,我們首先要修改生成的模板,如[代碼清單 10.10](#listing-application-mailer) 所示。然后要創建一個實例變量,其值是用戶對象,以便在視圖中使用,然后把郵件發給 `user.email`。如[代碼清單 10.11](#listing-mail-account-activation) 所示,`mail` 方法還可以接受 `subject` 參數,指定郵件的主題。 ##### 代碼清單 10.10:在 `ApplicationMailer` 中設定默認的發件人地址 ``` class ApplicationMailer < ActionMailer::Base default from: "noreply@example.com" layout 'mailer' end ``` ##### 代碼清單 10.11:發送賬戶激活鏈接 app/mailers/user_mailer.rb ``` class UserMailer < ApplicationMailer def account_activation(user) @user = user mail to: user.email, subject: "Account activation" end def password_reset @greeting = "Hi" mail to: "to@example.org" end end ``` 和普通的視圖一樣,在郵件程序的視圖中也可以使用嵌入式 Ruby。在郵件中我們要添加一個針對用戶的歡迎消息,以及一個激活鏈接。我們計劃使用電子郵件地址查找用戶,然后使用激活令牌認證用戶,所以鏈接中要包含電子郵件地址和令牌。因為我們把“賬戶激活”視作一個資源,所以可以把令牌作為參數傳給[代碼清單 10.1](#listing-account-activations-route) 中定義的具名路由: ``` edit_account_activation_url(@user.activation_token, ...) ``` 我們知道,`edit_user_url(user)` 生成的地址是下面這種形式: ``` http://www.example.com/users/1/edit ``` 那么,賬戶激活的鏈接應該是這種形式: ``` http://www.example.com/account_activations/q5lt38hQDc_959PVoo6b7A/edit ``` 其中,`q5lt38hQDc_959PVoo6b7A` 是使用 `new_token` 方法([代碼清單 8.31](chapter8.html#listing-token-method))生成的 base64 字符串,可安全地在 URL 中使用。這個值的作用和 /users/1/edit 中的用戶 ID 一樣,在 `AccountActivationsController` 的 `edit` 動作中可以通過 `params[:id]` 獲取。 為了包含電子郵件地址,我們要使用“查詢參數”(query parameter)。查詢參數放在 URL 中的問號后面,使用鍵值對形式指定:[[6](#fn-6)] ``` account_activations/q5lt38hQDc_959PVoo6b7A/edit?email=foo%40example.com ``` 注意,電子郵件地址中的“@”被替換成了 `%40`,也就是被轉義了,這樣,URL 才是有效的。在 Rails 中設定查詢參數的方法是,把一個哈希傳給具名路由: ``` edit_account_activation_url(@user.activation_token, email: @user.email) ``` 使用這種方式設定查詢參數,Rails 會自動轉義所有特殊字符。而且,在控制器中會自動反轉義電子郵件地址,通過 `params[:email]` 可以獲取電子郵件地址。 定義好實例變量 `@user` 之后([代碼清單 10.11](#listing-mail-account-activation)),我們可以使用 `edit` 動作的具名路由和嵌入式 Ruby 創建所需的鏈接了,如[代碼清單 10.12](#listing-account-activation-view-text) 和[代碼清單 10.13](#listing-account-activation-view-html) 所示。注意,在[代碼清單 10.13](#listing-account-activation-view-html) 中,我們使用 `link_to` 方法創建有效的鏈接。 ##### 代碼清單 10.12:賬戶激活郵件的純文本視圖 app/views/user_mailer/account_activation.text.erb ``` Hi <%= @user.name %>, Welcome to the Sample App! Click on the link below to activate your account: <%= edit_account_activation_url(@user.activation_token, email: @user.email) %> ``` ##### 代碼清單 10.13:賬戶激活郵件的 HTML 視圖 app/views/user_mailer/account_activation.html.erb ``` <h1>Sample App</h1> <p>Hi <%= @user.name %>,</p> <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <%= link_to "Activate", edit_account_activation_url(@user.activation_token, email: @user.email) %> ``` 若想查看這兩個郵件視圖的效果,我們可以使用郵件預覽功能。Rails 提供了一些特殊的 URL,用來預覽郵件。首先,我們要在應用的開發環境中添加一些設置,如[代碼清單 10.14](#listing-development-email-settings) 所示。 ##### 代碼清單 10.14:開發環境中的郵件設置 config/environments/development.rb ``` Rails.application.configure do . . . config.action_mailer.raise_delivery_errors = true config.action_mailer.delivery_method = :test host = 'example.com' config.action_mailer.default_url_options = { host: host } . . . end ``` [代碼清單 10.14](#listing-development-email-settings) 中設置的主機地址是 `'example.com'`,你應該使用你的開發環境的主機地址。例如,在我的系統中,可以使用下面的地址(包括云端 IDE 和本地服務器): ``` host = 'rails-tutorial-c9-mhartl.c9.io' # 云端 IDE host = 'localhost:3000' # 本地主機 ``` 然后重啟開發服務器,讓[代碼清單 10.14](#listing-development-email-settings) 中的設置生效。接下來,我們要修改郵件程序的預覽文件。生成郵件程序時已經自動生成了這個文件,如[代碼清單 10.15](#listing-generated-user-mailer-previews) 所示。 ##### 代碼清單 10.15:生成的郵件預覽程序 test/mailers/previews/user_mailer_preview.rb ``` # Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/account_activation def account_activation UserMailer.account_activation end # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset UserMailer.password_reset end end ``` 因為[代碼清單 10.11](#listing-mail-account-activation) 中定義的 `account_activation` 方法需要一個有效的用戶作為參數,所以[代碼清單 10.15](#listing-generated-user-mailer-previews) 中的代碼現在還不能使用。為了解決這個問題,我們要定義 `user` 變量,把開發數據庫中的第一個用戶賦值給它,然后作為參數傳給 `UserMailer.account_activation`,如[代碼清單 10.16](#listing-account-activation-preview) 所示。注意,在這段代碼中,我們還給 `user.activation_token` 賦了值,因為[代碼清單 10.12](#listing-account-activation-view-text) 和[代碼清單 10.13](#listing-account-activation-view-html) 中的模板要使用賬戶激活令牌。(`activation_token` 是虛擬屬性,所以數據庫中的用戶并沒有激活令牌。) ##### 代碼清單 10.16:預覽賬戶激活郵件所需的方法 test/mailers/previews/user_mailer_preview.rb ``` # Preview all emails at http://localhost:3000/rails/mailers/user_mailer class UserMailerPreview < ActionMailer::Preview # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/account_activation def account_activation user = User.first user.activation_token = User.new_token UserMailer.account_activation(user) end # Preview this email at # http://localhost:3000/rails/mailers/user_mailer/password_reset def password_reset UserMailer.password_reset end end ``` 這樣修改之后,我們就可以訪問注釋中提示的 URL 預覽賬戶激活郵件了。(如果使用云端 IDE,要把 `localhost:3000` 換成相應的 URL。)HTML 和純文本郵件分別如[圖 10.2](#fig-account-activation-html-preview) 和[圖 10.3](#fig-account-activation-text-preview) 所示。 ![account activation html preview](https://box.kancloud.cn/2016-05-11_57333066da535.png)圖 10.2:預覽 HTML 格式的賬戶激活郵件![account activation text preview](https://box.kancloud.cn/2016-05-11_57333066f37bf.png)圖 10.3:預覽純文本格式的賬戶激活郵件 最后,我們要編寫一些測試,再次確認郵件的內容。這并不難,因為 Rails 生成了一些有用的測試示例,如[代碼清單 10.17](#listing-generated-user-mailer-test) 所示。 ##### 代碼清單 10.17:Rails 生成的 `UserMailer` 測試 test/mailers/user_mailer_test.rb ``` require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do mail = UserMailer.account_activation assert_equal "Account activation", mail.subject assert_equal ["to@example.org"], mail.to assert_equal ["from@example.com"], mail.from assert_match "Hi", mail.body.encoded end test "password_reset" do mail = UserMailer.password_reset assert_equal "Password reset", mail.subject assert_equal ["to@example.org"], mail.to assert_equal ["from@example.com"], mail.from assert_match "Hi", mail.body.encoded end end ``` [代碼清單 10.17](#listing-generated-user-mailer-test) 中使用了強大的 `assert_match` 方法。這個方法既可以匹配字符串,也可以匹配正則表達式: ``` assert_match 'foo', 'foobar' # true assert_match 'baz', 'foobar' # false assert_match /\w+/, 'foobar' # true assert_match /\w+/, '$#!*+@' # false ``` [代碼清單 10.18](#listing-real-account-activation-test) 使用 `assert_match` 檢查郵件正文中是否有用戶的名字、激活令牌和轉義后的電子郵件地址。注意,轉義用戶電子郵件地址使用的方法是 `CGI::escape(user.email)`。[[7](#fn-7)](其實還有第三種方法,`ERB::Util` 中的 [`url_encode` 方法](http://apidock.com/ruby/ERB/Util/url_encode)有同樣的效果。) ##### 代碼清單 10.18:測試現在這個郵件程序 RED test/mailers/user_mailer_test.rb ``` require 'test_helper' class UserMailerTest < ActionMailer::TestCase test "account_activation" do user = users(:michael) user.activation_token = User.new_token mail = UserMailer.account_activation(user) assert_equal "Account activation", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.name, mail.body.encoded assert_match user.activation_token, mail.body.encoded assert_match CGI::escape(user.email), mail.body.encoded end end ``` 注意,我們在[代碼清單 10.18](#listing-real-account-activation-test) 中為用戶固件指定了激活令牌,因為固件中沒有虛擬屬性。 為了讓這個測試通過,我們要修改測試環境的配置,設定正確的主機地址,如[代碼清單 10.19](#listing-test-domain-host) 所示。 ##### 代碼清單 10.19:設定測試環境的主機地址 config/environments/test.rb ``` Rails.application.configure do . . . config.action_mailer.delivery_method = :test config.action_mailer.default_url_options = { host: 'example.com' } . . . end ``` 現在,郵件程序的測試應該可以通過了: ##### 代碼清單 10.20:**GREEN** ``` $ bundle exec rake test:mailers ``` 若要在我們的應用中使用這個郵件程序,只需在處理用戶注冊的 `create` 動作中添加幾行代碼,如[代碼清單 10.21](#listing-user-signup-with-account-activation) 所示。注意,[代碼清單 10.21](#listing-user-signup-with-account-activation) 修改了注冊后的重定向地址。之前,我們把用戶重定向到資料頁面([7.4 節](chapter7.html#successful-signups)),可是現在需要先激活,再轉向這個頁面就不合理了,所以把重定向地址改成了根地址。 ##### 代碼清單 10.21:在注冊過程中添加賬戶激活 RED app/controllers/users_controller.rb ``` class UsersController < ApplicationController . . . def create @user = User.new(user_params) if @user.save UserMailer.account_activation(@user).deliver_now flash[:info] = "Please check your email to activate your account." redirect_to root_url else render 'new' end end . . . end ``` 因為現在重定向到根地址而不是資料頁面,而且不會像之前那樣自動登入用戶,所以測試組件無法通過,不過應用能按照我們設計的方式運行。我們暫時把導致失敗的測試注釋掉,如[代碼清單 10.22](#listing-comment-out-failing-tests) 所示。我們會在 [10.1.4 節](#activation-test-and-refactoring)去掉注釋,并且為賬戶激活編寫能通過的測試。 ##### 代碼清單 10.22:臨時注釋掉失敗的測試 GREEN test/integration/users_signup_test.rb ``` require 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest test "invalid signup information" do get signup_path assert_no_difference 'User.count' do post users_path, user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } end assert_template 'users/new' assert_select 'div#error_explanation' assert_select 'div.field_with_errors' end test "valid signup information" do get signup_path assert_difference 'User.count', 1 do post_via_redirect users_path, user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } end # assert_template 'users/show' # assert is_logged_in? end end ``` 如果現在注冊,重定向后顯示的頁面如[圖 10.4](#fig-redirected-not-activated) 所示,而且會生成一封郵件,如[代碼清單 10.23](#listing-account-activation-email) 所示。注意,在開發環境中并不會真發送郵件,不過能在服務器的日志中看到(可能要往上滾動才能看到)。[10.3 節](#email-in-production)會介紹如何在生產環境中發送郵件。 ##### 代碼清單 10.23:在服務器日志中看到的賬戶激活郵件 ``` Sent mail to michael@michaelhartl.com (931.6ms) Date: Wed, 03 Sep 2014 19:47:18 +0000 From: noreply@example.com To: michael@michaelhartl.com Message-ID: <540770474e16_61d3fd1914f4cd0300a0@mhartl-rails-tutorial-953753.mail> Subject: Account activation Mime-Version: 1.0 Content-Type: multipart/alternative; boundary="--==_mimepart_5407704656b50_61d3fd1914f4cd02996a"; charset=UTF-8 Content-Transfer-Encoding: 7bit ----==_mimepart_5407704656b50_61d3fd1914f4cd02996a Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit Hi Michael Hartl, Welcome to the Sample App! Click on the link below to activate your account: http://rails-tutorial-c9-mhartl.c9.io/account_activations/ fFb_F94mgQtmlSvRFGsITw/edit?email=michael%40michaelhartl.com ----==_mimepart_5407704656b50_61d3fd1914f4cd02996a Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 7bit <h1>Sample App</h1> <p>Hi Michael Hartl,</p> <p> Welcome to the Sample App! Click on the link below to activate your account: </p> <a href="http://rails-tutorial-c9-mhartl.c9.io/account_activations/ fFb_F94mgQtmlSvRFGsITw/edit?email=michael%40michaelhartl.com">Activate</a> ----==_mimepart_5407704656b50_61d3fd1914f4cd02996a-- ``` ![redirected not activated](https://box.kancloud.cn/2016-05-11_5733306729172.png)圖 10.4:注冊后顯示的首頁,有一個提醒激活的消息 ## 10.1.3 激活賬戶 現在可以正確生成電子郵件了([代碼清單 10.23](#listing-account-activation-email)),接下來我們要編寫 `AccountActivationsController` 中的 `edit` 動作,激活用戶。[10.1.2 節](#account-activation-mailer-method)說過,激活令牌和電子郵件地址可以分別通過 `params[:id]` 和 `params[:email]` 獲取。參照密碼([代碼清單 8.5](chapter8.html#listing-find-authenticate-user))和記憶令牌([代碼清單 8.36](chapter8.html#listing-persistent-current-user))的實現方式,我們計劃使用下面的代碼查找和認證用戶: ``` user = User.find_by(email: params[:email]) if user && user.authenticated?(:activation, params[:id]) ``` (稍后會看到,上述代碼還缺一個判斷條件。看看你能否猜到缺了什么。) 上述代碼使用 `authenticated?` 方法檢查賬戶激活的摘要和指定的令牌是否匹配,但是現在不起作用,因為 `authenticated?` 方法是專門用來認證記憶令牌的([代碼清單 8.33](chapter8.html#listing-authenticated-p)): ``` # 如果指定的令牌和摘要匹配,返回 true def authenticated?(remember_token) return false if remember_digest.nil? BCrypt::Password.new(remember_digest).is_password?(remember_token) end ``` 其中,`remember_digest` 是用戶模型的屬性,在模型內,我們可以將其改寫成: ``` self.remember_digest ``` 我們希望以某種方式把這個值變成“變量”,這樣才能調用 `self.activation_token`,而不是把合適的參數傳給 `authenticated?` 方法。 我們要使用的解決方法涉及到“元編程”(metaprogramming),意思是用程序編寫程序。(元編程是 Ruby 最強大的功能,Rails 中很多“神奇”的功能都是通過元編程實現的。)這里的關鍵是強大的 `send` 方法。這個方法的作用是在指定的對象上調用指定的方法。例如,在下面的控制臺會話中,我們在一個 Ruby 原生對象上調用 `send` 方法,獲取數組的長度: ``` $ rails console >> a = [1, 2, 3] >> a.length => 3 >> a.send(:length) => 3 >> a.send('length') => 3 ``` 可以看出,把 `:length` 符號或者 `'length'` 字符串傳給 `send` 方法的作用和在對象上直接調用 `length` 方法的作用一樣。再看一個例子,獲取數據庫中第一個用戶的 `activation_digest` 屬性: ``` >> user = User.first >> user.activation_digest => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" >> user.send(:activation_digest) => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" >> user.send('activation_digest') => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" >> attribute = :activation >> user.send("#{attribute}_digest") => "$2a$10$4e6TFzEJAVNyjLv8Q5u22ensMt28qEkx0roaZvtRcp6UZKRM6N9Ae" ``` 注意最后一種調用方式,我們定義了一個 `attribute` 變量,其值為符號 `:activation`,然后使用字符串插值構建傳給 `send` 方法的參數。`attribute` 變量的值使用字符串 `'activation'` 也行,不過符號更便利。不管使用什么,插值后,`"#{attribute}_digest"` 的結果都是 `"activation_digest"`。([7.4.2 節](chapter7.html#the-flash)介紹過,插值時會把符號轉換成字符串。) 基于上述對 `send` 方法的介紹,我們可以把 `authenticated?` 方法改寫成: ``` def authenticated?(remember_token) digest = self.send('remember_digest') return false if digest.nil? BCrypt::Password.new(digest).is_password?(remember_token) end ``` 以此為模板,我們可以為這個方法增加一個參數,代表摘要的名字,然后再使用字符串插值,擴大這個方法的用途: ``` def authenticated?(attribute, token) digest = self.send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) end ``` (我們把第二個參數的名字改成了 `token`,以此強調這個方法的用途更廣。)因為這個方法在用戶模型內,所以可以省略 `self`,得到更符合習慣寫法的版本: ``` def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) end ``` 現在我們可以像下面這樣調用 `authenticated?` 方法實現以前的效果: ``` user.authenticated?(:remember, remember_token) ``` 把修改后的 `authenticated?` 方法寫入用戶模型,如[代碼清單 10.24](#listing-generalized-authenticated-p) 所示。 ##### 代碼清單 10.24:用途更廣的 `authenticated?` 方法 RED app/models/user.rb ``` class User < ActiveRecord::Base . . . # 如果指定的令牌和摘要匹配,返回 true def authenticated?(attribute, token) digest = send("#{attribute}_digest") return false if digest.nil? BCrypt::Password.new(digest).is_password?(token) end . . . end ``` 如[代碼清單 10.24](#listing-generalized-authenticated-p) 的標題所示,測試組件無法通過: ##### 代碼清單 10.25:**RED** ``` $ bundle exec rake test ``` 失敗的原因是,`current_user` 方法([代碼清單 8.36](chapter8.html#listing-persistent-current-user))和摘要為 `nil` 的測試([代碼清單 8.43](chapter8.html#listing-test-authenticated-invalid-token))使用的都是舊版 `authenticated?`,期望傳入的是一個參數而不是兩個。因此,我們只需修改這兩個地方,換用修改后的 `authenticated?` 方法就能解決這個問題,如[代碼清單 10.26](#listing-generalized-current-user) 和[代碼清單 10.27](#listing-test-authenticated-invalid-token-updated) 所示。 ##### 代碼清單 10.26:在 `current_user` 中使用修改后的 `authenticated?` 方法 GREEN app/helpers/sessions_helper.rb ``` module SessionsHelper . . . # 返回當前登錄的用戶(如果有的話) def current_user if (user_id = session[:user_id]) @current_user ||= User.find_by(id: user_id) elsif (user_id = cookies.signed[:user_id]) user = User.find_by(id: user_id) if user && user.authenticated?(:remember, cookies[:remember_token]) log_in user @current_user = user end end end . . . end ``` ##### 代碼清單 10.27:在 `UserTest` 中使用修改后的 `authenticated?` 方法 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 . . . test "authenticated? should return false for a user with nil digest" do assert_not @user.authenticated?(:remember, '') end end ``` 修改后,測試應該可以通過了: ##### 代碼清單 10.28:**GREEN** ``` $ bundle exec rake test ``` 沒有堅實的測試組件做后盾,像這樣的重構很容易出錯,所以我們才要在 [8.4.2 節](chapter8.html#login-with-remembering)和 [8.4.6 節](chapter8.html#remember-tests)排除萬難編寫測試。 有了[代碼清單 10.24](#listing-generalized-authenticated-p) 中定義的 `authenticated?` 方法,現在我們可以編寫 `edit` 動作,認證 `params` 哈希中電子郵件地址對應的用戶了。我們要使用的判斷條件如下所示: ``` if user && !user.activated? && user.authenticated?(:activation, params[:id]) ``` 注意,這里加入了 `!user.activated?`,就是前面提到的那個缺失的條件,作用是避免激活已經激活的用戶。這個條件很重要,因為激活后我們要登入用戶,但是不能讓獲得激活鏈接的攻擊者以這個用戶的身份登錄。 如果通過了上述判斷條件,我們要激活這個用戶,并且更新 `activated_at` 中的時間戳: ``` user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone.now) ``` 據此,寫出的 `edit` 動作如[代碼清單 10.29](#listing-account-activation-edit-action) 所示。注意,在[代碼清單 10.29](#listing-account-activation-edit-action) 中我們還處理了激活令牌無效的情況。這種情況很少發生,但處理起來也很容易,直接重定向到根地址即可。 ##### 代碼清單 10.29:在 `edit` 動作中激活賬戶 app/controllers/account_activations_controller.rb ``` class AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.update_attribute(:activated, true) user.update_attribute(:activated_at, Time.zone.now) log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end end ``` 然后,復制粘貼[代碼清單 10.23](#listing-account-activation-email) 中的地址,應該就可以激活對應的用戶了。例如,在我的系統中,我訪問的地址是: ``` http://rails-tutorial-c9-mhartl.c9.io/account_activations/ fFb_F94mgQtmlSvRFGsITw/edit?email=michael%40michaelhartl.com ``` 然后會看到如[圖 10.5](#fig-activated-user) 所示的頁面。 ![activated user](https://box.kancloud.cn/2016-05-11_5733306746907.png)圖 10.5:成功激活后顯示的資料頁面 當然,現在激活用戶后沒有什么實際效果,因為我們還沒修改用戶登錄的方式。為了讓賬戶激活有實際意義,只能允許已經激活的用戶登錄,即 `user.activated?` 返回 `true` 時才能像之前那樣登錄,否則重定向到根地址,并且顯示一個提醒消息([圖 10.6](#fig-not-activated-warning)),如[代碼清單 10.30](#listing-preventing-unactivated-logins) 所示。 ##### 代碼清單 10.30:禁止未激活的用戶登錄 app/controllers/sessions_controller.rb ``` class SessionsController < ApplicationController def new end def create user = User.find_by(email: params[:session][:email].downcase) if user && user.authenticate(params[:session][:password]) if user.activated? log_in user params[:session][:remember_me] == '1' ? remember(user) : forget(user) redirect_back_or user else message = "Account not activated. " message += "Check your email for the activation link." flash[:warning] = message redirect_to root_url end else flash.now[:danger] = 'Invalid email/password combination' render 'new' end end def destroy log_out if logged_in? redirect_to root_url end end ``` ![not activated warning](https://box.kancloud.cn/2016-05-11_573330675d32c.png)圖 10.6:未激活用戶試圖登錄后看到的提醒消息 至此,激活用戶的功能基本完成了,不過還有個地方可以改進。(可以改進的是,不顯示未激活的用戶。這個改進留作[練習](#account-activation-and-password-reset-exercises)。)[10.1.4 節](#activation-test-and-refactoring)會編寫一些測試,再做一些重構,完成整個功能。 ## 10.1.4 測試和重構 本節,我們要為賬戶激活功能添加一些集成測試。我們已經為提交有效信息的注冊過程編寫了測試,所以我們要把這個測試添加到 [7.4.4 節](chapter7.html#a-test-for-valid-submission)編寫的測試中([代碼清單 7.26](chapter7.html#listing-a-test-for-valid-submission))。在測試中,我們要添加好多步,不過意圖都很明確,看看你是否能理解[代碼清單 10.31](#listing-signup-with-account-activation-test) 中的測試。 ##### 代碼清單 10.31:在用戶注冊的測試文件中添加賬戶激活的測試 GREEN test/integration/users_signup_test.rb ``` require 'test_helper' class UsersSignupTest < ActionDispatch::IntegrationTest def setup ActionMailer::Base.deliveries.clear end test "invalid signup information" do get signup_path assert_no_difference 'User.count' do post users_path, user: { name: "", email: "user@invalid", password: "foo", password_confirmation: "bar" } end assert_template 'users/new' assert_select 'div#error_explanation' assert_select 'div.field_with_errors' end test "valid signup information with account activation" do get signup_path assert_difference 'User.count', 1 do post users_path, user: { name: "Example User", email: "user@example.com", password: "password", password_confirmation: "password" } end assert_equal 1, ActionMailer::Base.deliveries.size user = assigns(:user) assert_not user.activated? # 嘗試在激活之前登錄 log_in_as(user) assert_not is_logged_in? # 激活令牌無效 get edit_account_activation_path("invalid token") assert_not is_logged_in? # 令牌有效,電子郵件地址不對 get edit_account_activation_path(user.activation_token, email: 'wrong') assert_not is_logged_in? # 激活令牌有效 get edit_account_activation_path(user.activation_token, email: user.email) assert user.reload.activated? follow_redirect! assert_template 'users/show' assert is_logged_in? end end ``` 代碼很多,不過有一行完全沒見過: ``` assert_equal 1, ActionMailer::Base.deliveries.size ``` 這行代碼確認只發送了一封郵件。`deliveries` 是一個數組,會統計所有發出的郵件,所以我們要在 `setup` 方法中把它清空,以防其他測試發送了郵件([10.2.5 節](#password-reset-test)就會這么做)。[代碼清單 10.31](#listing-signup-with-account-activation-test) 還第一次在本書正文中使用了 `assigns` 方法。[8.6 節](chapter8.html#log-in-log-out-exercises)說過,`assigns` 的作用是獲取相應動作中的實例變量。例如,用戶控制器的 `create` 動作中定義了一個 `@user` 變量,那么我們可以在測試中使用 `assigns(:user)` 獲取這個變量的值。最后,注意,[代碼清單 10.31](#listing-signup-with-account-activation-test) 把[代碼清單 10.22](#listing-comment-out-failing-tests) 中的注釋去掉了。 現在,測試組件應該可以通過: ##### 代碼清單 10.32:**GREEN** ``` $ bundle exec rake test ``` 有了[代碼清單 10.31](#listing-signup-with-account-activation-test) 中的測試做后盾,接下來我們可以稍微重構一下了:把處理用戶的代碼從控制器中移出,放入模型。我們會定義一個 `activate` 方法,用來更新用戶激活相關的屬性;還要定義一個 `send_activation_email` 方法,發送激活郵件。這兩個方法的定義如[代碼清單 10.33](#listing-user-activation-methods) 所示,重構后的應用代碼如[代碼清單 10.34](#listing-user-signup-refactored) 和[代碼清單 10.35](#listing-account-activation-refactored) 所示。 ##### 代碼清單 10.33:在用戶模型中添加賬戶激活相關的方法 app/models/user.rb ``` class User < ActiveRecord::Base . . . # 激活賬戶 def activate update_attribute(:activated, true) update_attribute(:activated_at, Time.zone.now) end # 發送激活郵件 def send_activation_email UserMailer.account_activation(self).deliver_now end private . . . end ``` ##### 代碼清單 10.34:通過用戶模型對象發送郵件 app/controllers/users_controller.rb ``` class UsersController < ApplicationController . . . def create @user = User.new(user_params) if @user.save @user.send_activation_email flash[:info] = "Please check your email to activate your account." redirect_to root_url else render 'new' end end . . . end ``` ##### 代碼清單 10.35:通過用戶模型對象激活賬戶 app/controllers/account_activations_controller.rb ``` class AccountActivationsController < ApplicationController def edit user = User.find_by(email: params[:email]) if user && !user.activated? && user.authenticated?(:activation, params[:id]) user.activate log_in user flash[:success] = "Account activated!" redirect_to user else flash[:danger] = "Invalid activation link" redirect_to root_url end end end ``` 注意,在[代碼清單 10.33](#listing-user-activation-methods) 中沒有使用 `user`。如果還像之前那樣寫就會出錯,因為用戶模型中沒有這個變量: ``` -user.update_attribute(:activated, true) -user.update_attribute(:activated_at, Time.zone.now) +update_attribute(:activated, true) +update_attribute(:activated_at, Time.zone.now) ``` (也可以把 `user` 換成 `self`,但 [6.2.5 節](chapter6.html#uniqueness-validation)說過,在模型內可以不加 `self`。)調用 `UserMailer` 時,還把 `@user` 改成了 `self`: ``` -UserMailer.account_activation(@user).deliver_now +UserMailer.account_activation(self).deliver_now ``` 就算是簡單的重構,也可能忽略這些細節,不過好的測試組件能捕獲這些問題。現在,測試組件應該仍能通過: ##### 代碼清單 10.36:**GREEN** ``` $ bundle exec rake test ``` 賬戶激活功能完成了,我們取得了一定進展,可以提交了: ``` $ git add -A $ git commit -m "Add account activations" ```
                  <ruby id="bdb3f"></ruby>

                  <p id="bdb3f"><cite id="bdb3f"></cite></p>

                    <p id="bdb3f"><cite id="bdb3f"><th id="bdb3f"></th></cite></p><p id="bdb3f"></p>
                      <p id="bdb3f"><cite id="bdb3f"></cite></p>

                        <pre id="bdb3f"></pre>
                        <pre id="bdb3f"><del id="bdb3f"><thead id="bdb3f"></thead></del></pre>

                        <ruby id="bdb3f"><mark id="bdb3f"></mark></ruby><ruby id="bdb3f"></ruby>
                        <pre id="bdb3f"><pre id="bdb3f"><mark id="bdb3f"></mark></pre></pre><output id="bdb3f"></output><p id="bdb3f"></p><p id="bdb3f"></p>

                        <pre id="bdb3f"><del id="bdb3f"><progress id="bdb3f"></progress></del></pre>

                              <ruby id="bdb3f"></ruby>

                              哎呀哎呀视频在线观看