<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>

                ??碼云GVP開源項目 12k star Uniapp+ElementUI 功能強大 支持多語言、二開方便! 廣告
                # 10.2 密碼重設 完成賬戶激活功能后(從而確認了用戶的電子郵件地址可用),我們要處理一種常見的問題:用戶忘記密碼。我們會看到,密碼重設的很多步驟和賬戶激活類似,所以這里會用到 [10.1 節](#account-activation)學到的知識。不過,開頭不一樣,和賬戶激活功能不同的是,密碼重設要修改一個視圖,還要創建兩個表單(處理電子郵件地址提交和設定新密碼)。 編寫代碼之前,我們先構思要實現的重設密碼步驟。首先,我們要在演示應用的登錄表單中添加“Forgot Password”(忘記密碼)鏈接,如[圖 10.7](#fig-login-forgot-password-mockup) 所示。 ![login forgot password mockup](https://box.kancloud.cn/2016-05-11_5733306a5efc8.png)圖 10.7:“Forgot Password”鏈接的構思圖 點擊“Forgot Password”鏈接后打開一個頁面,這個頁面中有一個表單,要求輸入電子郵件地址,提交后向這個地址發送一封包含密碼重設鏈接的郵件,如[圖 10.8](#fig-forgot-password-form-mockup) 所示。 ![forgot password form mockup](https://box.kancloud.cn/2016-05-11_5733306a74aac.png)圖 10.8:“Forgot Password”表單的構思圖 點擊密碼重設鏈接會打開一個表單,用戶在這個表單中重設密碼(還要填寫密碼確認),如[圖 10.9](#fig-reset-password-form-mockup) 所示。 ![reset password form mockup](https://box.kancloud.cn/2016-05-11_5733306a84c37.png)圖 10.9:重設密碼表單的構思圖 和賬戶激活一樣,我們要把“密碼重設”看做一個資源,每個重設密碼操作都有一個重設令牌和對應的摘要。主要的步驟如下: 1. 用戶請求重設密碼時,使用提交的電子郵件地址查找用戶; 2. 如果數據庫中有這個電子郵件地址,生成一個重設令牌和對應的摘要; 3. 把重設摘要保存在數據庫中,然后給用戶發送一封郵件,其中有一包含重設令牌和用戶電子郵件地址的鏈接; 4. 用戶點擊這個鏈接后,使用電子郵件地址查找用戶,然后對比令牌和摘要; 5. 如果匹配,顯示重設密碼的表單。 ## 10.2.1 資源 和賬戶激活一樣([10.1.1 節](#account-activations-resource)),第一步要為資源生成控制器: ``` $ rails generate controller PasswordResets new edit --no-test-framework ``` 注意,我們指定了不生成測試的參數,因為我們不需要控制器測試(和 [10.1.4 節](#activation-test-and-refactoring)一樣,要使用集成測試),所以最好不生成。 我們需要兩個表單,一個請求重設密碼([圖 10.8](#fig-forgot-password-form-mockup)),一個修改用戶模型中的密碼([圖 10.9](#fig-reset-password-form-mockup)),所以需要為 `new`、`create`、`edit` 和 `update` 四個動作制定路由——通過[代碼清單 10.37](#listing-password-resets-resource) 中高亮顯示的那行 `resources` 規則實現。 ##### 代碼清單 10.37:添加“密碼重設”資源的路由 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] resources :password_resets, only: [:new, :create, :edit, :update] end ``` 添加這個規則后,得到了[表 10.2](#table-restful-password-resets) 中的 REST 路由。 表 10.2:定義“密碼重設”資源后得到的 REST 路由 | HTTP 請求 | URL | 動作 | 具名路由 | | --- | --- | --- | --- | | `GET` | /password_resets/new | `new` | `new_password_reset_path` | | `POST` | /password_resets | `create` | `password_resets_path` | | `GET` | /password_resets/&lt;token&gt;/edit | `edit` | `edit_password_reset_path(token)` | | `PATCH` | /password_resets/&lt;token&gt; | `update` | `password_reset_path(token)` | 通過表中第一個路由可以得到指向“Forgot Password”表單的鏈接: ``` new_password_reset_path ``` 把這個鏈接添加到登錄表單,如[代碼清單 10.38](#listing-log-in-password-reset) 所示。添加后的效果如[圖 10.10](#fig-forgot-password-link) 所示。 ##### 代碼清單 10.38:添加打開忘記密碼表單的鏈接 app/views/sessions/new.html.erb ``` <% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= link_to "(forgot password)", new_password_reset_path %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>Remember me on this computer</span> <% end %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div> ``` ![forgot password link](https://box.kancloud.cn/2016-05-11_5733306aa41d7.png)圖 10.10:添加“Forgot Password”鏈接后的登錄頁面 密碼重設所需的數據模型和賬戶激活的類似([圖 10.1](#fig-user-model-account-activation))。參照“記住我”功能([8.4 節](chapter8.html#remember-me))和賬戶激活功能([10.1 節](#account-activation)),密碼重設需要一個虛擬的重設令牌屬性,在重設密碼的郵件中使用,以及一個重設摘要屬性,用來取回用戶。 如果存儲未哈希的令牌,能訪問數據庫的攻擊者就能發送一封重設密碼郵件給用戶,然后使用令牌和郵件地址訪問對應的密碼重設鏈接,從而獲得賬戶控制權。因此,必須存儲令牌的摘要。為了進一步保障安全,我們還計劃過幾個小時后讓重設鏈接失效,所以要記錄重設郵件發送的時間。據此,我們要添加兩個屬性:`reset_digest` 和 `reset_sent_at`,如[圖 10.11](#fig-user-model-password-reset) 所示。 ![user model password reset](https://box.kancloud.cn/2016-05-11_5733306abc0c6.png)圖 10.11:添加密碼重設相關屬性后的用戶模型 執行下面的命令,創建添加這兩個屬性的遷移: ``` $ rails generate migration add_reset_to_users reset_digest:string \ > reset_sent_at:datetime ``` 然后像之前一樣執行遷移: ``` $ bundle exec rake db:migrate ``` ## 10.2.2 控制器和表單 我們要參照前面為沒有模型的資源編寫表單的方法,即創建新會話的登錄表單([代碼清單 8.2](chapter8.html#listing-login-form)),編寫請求重設密碼的表單。為了便于參考,我們再把這個表單列出來,如[代碼清單 10.39](#listing-login-form-redux) 所示。 ##### 代碼清單 10.39:登錄表單的代碼 app/views/sessions/new.html.erb ``` <% provide(:title, "Log in") %> <h1>Log in</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(:session, url: login_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :remember_me, class: "checkbox inline" do %> <%= f.check_box :remember_me %> <span>Remember me on this computer</span> <% end %> <%= f.submit "Log in", class: "btn btn-primary" %> <% end %> <p>New user? <%= link_to "Sign up now!", signup_path %></p> </div> </div> ``` 請求重設密碼的表單和[代碼清單 10.39](#listing-login-form-redux) 有很多共通之處,最大的區別是,`form_for` 中的資源和地址不一樣,而且也沒有密碼字段。請求重設密碼的表單如[代碼清單 10.40](#listing-new-password-reset) 所示,渲染的結果如[圖 10.12](#fig-forgot-password-form) 所示。 ##### 代碼清單 10.40:請求重設密碼頁面的視圖 app/views/password_resets/new.html.erb ``` <% provide(:title, "Forgot password") %> <h1>Forgot password</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(:password_reset, url: password_resets_path) do |f| %> <%= f.label :email %> <%= f.email_field :email, class: 'form-control' %> <%= f.submit "Submit", class: "btn btn-primary" %> <% end %> </div> </div> ``` ![forgot password form](https://box.kancloud.cn/2016-05-11_5733306ad1c8f.png)圖 10.12:“Forgot Password”表單 提交[圖 10.12](#fig-forgot-password-form) 中的表單后,我們要通過電子郵件地址查找用戶,更新這個用戶的 `reset_token`、`reset_digest` 和 `reset_sent_at` 屬性,然后重定向到根地址,并顯示一個閃現消息。和登錄一樣([代碼清單 8.9](chapter8.html#listing-correct-login-failure)),如果提交的數據無效,我們要重新渲染這個頁面,并且顯示一個 `flash.now` 消息。據此,寫出的 `create` 動作如[代碼清單 10.41](#listing-create-password-reset) 所示。 ##### 代碼清單 10.41:`PasswordResetsController` 的 `create` 動作 app/controllers/password_resets_controller.rb ``` class PasswordResetsController < ApplicationController def new end def create @user = User.find_by(email: params[:password_reset][:email].downcase) if @user @user.create_reset_digest @user.send_password_reset_email flash[:info] = "Email sent with password reset instructions" redirect_to root_url else flash.now[:danger] = "Email address not found" render 'new' end end def edit end end ``` 然后要在用戶模型中定義 `create_reset_digest` 方法,如[代碼清單 10.42](#listing-user-model-password-reset) 所示。 ##### 代碼清單 10.42:在用戶模型中添加重設密碼所需的方法 app/models/user.rb ``` class User < ActiveRecord::Base attr_accessor :remember_token, :activation_token, :reset_token before_save :downcase_email before_create :create_activation_digest . . . # 激活賬戶 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 # 設置密碼重設相關的屬性 def create_reset_digest self.reset_token = User.new_token update_attribute(:reset_digest, User.digest(reset_token)) update_attribute(:reset_sent_at, Time.zone.now) end # 發送密碼重設郵件 def send_password_reset_email UserMailer.password_reset(self).deliver_now end 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.13](#fig-invalid-email-password-reset) 所示,提交無效電子郵件地址時,應用的表現正常。為了讓提交有效地址時應用也能正常運行,我們要定義發送密碼重設郵件的方法,這一步會在 [10.2.3 節](#password-reset-mailer-method)完成。 ![invalid email password reset](https://box.kancloud.cn/2016-05-11_5733306b0bd8a.png)圖 10.13:提交無效電子郵件地址后顯示的“Forgot Password”表單 ## 10.2.3 郵件程序 [代碼清單 10.42](#listing-user-model-password-reset) 中發送密碼重設郵件的代碼是: ``` UserMailer.password_reset(self).deliver_now ``` 讓這個郵件程序運作起來所需的代碼幾乎和 [10.1.2 節](#account-activation-mailer-method)的賬戶激活郵件程序一樣。我們首先在 `UserMailer` 中定義 `password_reset` 方法([代碼清單 10.43](#listing-mail-password-reset)),然后再編寫郵件的純文本視圖([代碼清單 10.44](#listing-password-reset-text))和 HTML 視圖([代碼清單 10.45](#listing-password-reset-html))。 ##### 代碼清單 10.43:發送密碼重設鏈接 app/mailers/user_mailer.rb ``` class UserMailer < ApplicationMailer default from: "noreply@example.com" def account_activation(user) @user = user mail to: user.email, subject: "Account activation" end def password_reset(user) @user = user mail to: user.email, subject: "Password reset" end end ``` ##### 代碼清單 10.44:密碼重設郵件的純文本視圖 app/views/user_mailer/password_reset.text.erb ``` To reset your password click the link below: <%= edit_password_reset_url(@user.reset_token, email: @user.email) %> This link will expire in two hours. If you did not request your password to be reset, please ignore this email and your password will stay as it is. ``` ##### 代碼清單 10.45:密碼重設郵件的 HTML 視圖 app/views/user_mailer/password_reset.html.erb ``` <h1>Password reset</h1> <p>To reset your password click the link below:</p> <%= link_to "Reset password", edit_password_reset_url(@user.reset_token, email: @user.email) %> <p>This link will expire in two hours.</p> <p> If you did not request your password to be reset, please ignore this email and your password will stay as it is. </p> ``` 和賬戶激活郵件一樣([10.1.2 節](#account-activation-mailer-method)),我們可以使用 Rails 提供的郵件預覽程序預覽密碼重設郵件。參照[代碼清單 10.16](#listing-account-activation-preview),密碼重設的郵件預覽程序如[代碼清單 10.46](#listing-password-reset-preview) 所示。 ##### 代碼清單 10.46:預覽密碼重設郵件所需的方法 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 user = User.first user.reset_token = User.new_token UserMailer.password_reset(user) end end ``` 然后就可以預覽密碼重設郵件了,HTML 格式和純文本格式分別如[圖 10.14](#fig-password-reset-html-preview) 和[圖 10.15](#fig-password-reset-text-preview) 所示。 ![password reset html preview](https://box.kancloud.cn/2016-05-11_5733306b4195c.png)圖 10.14:預覽 HTML 格式的密碼重設郵件![password reset text preview](https://box.kancloud.cn/2016-05-11_5733306b7b091.png)圖 10.15:預覽純文本格式的密碼重設郵件 參照賬戶激活郵件程序的測試([代碼清單 10.18](#listing-real-account-activation-test)),密碼重設郵件程序的測試如[代碼清單 10.47](#listing-password-reset-mailer-test) 所示。注意,我們要創建密碼重設令牌,以便在視圖中使用。這一點和激活令牌不一樣,激活令牌使用 `before_create` 回調創建([代碼清單 10.3](#listing-user-model-activation-code)),但是密碼重設令牌只會在用戶成功提交“Forgot Password”表單后創建。在集成測試中很容易創建密碼重設令牌(參見[代碼清單 10.54](#listing-password-reset-integration-test)),但在郵件程序的測試中必須手動創建。 ##### 代碼清單 10.47:添加密碼重設郵件程序的測試 GREEN 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 test "password_reset" do user = users(:michael) user.reset_token = User.new_token mail = UserMailer.password_reset(user) assert_equal "Password reset", mail.subject assert_equal [user.email], mail.to assert_equal ["noreply@example.com"], mail.from assert_match user.reset_token, mail.body.encoded assert_match CGI::escape(user.email), mail.body.encoded end end ``` 現在,測試組件應該能通過: ##### 代碼清單 10.48:**GREEN** ``` $ bundle exec rake test ``` 有了[代碼清單 10.43](#listing-mail-password-reset)、[代碼清單 10.44](#listing-password-reset-text) 和[代碼清單 10.45](#listing-password-reset-html) 之后,提交有效電子郵件地址后顯示的頁面如[圖 10.16](#fig-valid-email-password-reset) 所示。服務器日志中記錄的郵件類似于[代碼清單 10.49](#listing-password-reset-email)。 ![valid email password reset](https://box.kancloud.cn/2016-05-11_5733306b96cce.png)圖 10.16:提交有效電子郵件地址后顯示的頁面 ##### 代碼清單 10.49:服務器日志中記錄的一封密碼重設郵件 ``` Sent mail to michael@michaelhartl.com (66.8ms) Date: Thu, 04 Sep 2014 01:04:59 +0000 From: noreply@example.com To: michael@michaelhartl.com Message-ID: <5407babbee139_8722b257d04576a@mhartl-rails-tutorial-953753.mail> Subject: Password reset Mime-Version: 1.0 Content-Type: multipart/alternative; boundary="--==_mimepart_5407babbe3505_8722b257d045617"; charset=UTF-8 Content-Transfer-Encoding: 7bit ----==_mimepart_5407babbe3505_8722b257d045617 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 7bit To reset your password click the link below: http://rails-tutorial-c9-mhartl.c9.io/password_resets/3BdBrXeQZSWqFIDRN8cxHA/ edit?email=michael%40michaelhartl.com This link will expire in two hours. If you did not request your password to be reset, please ignore this email and your password will stay as it is. ----==_mimepart_5407babbe3505_8722b257d045617 Content-Type: text/html; charset=UTF-8 Content-Transfer-Encoding: 7bit <h1>Password reset</h1> <p>To reset your password click the link below:</p> <a href="http://rails-tutorial-c9-mhartl.c9.io/ password_resets/3BdBrXeQZSWqFIDRN8cxHA/ edit?email=michael%40michaelhartl.com">Reset password</a> <p>This link will expire in two hours.</p> <p> If you did not request your password to be reset, please ignore this email and your password will stay as it is. </p> ----==_mimepart_5407babbe3505_8722b257d045617-- ``` ## 10.2.4 重設密碼 為了讓下面這種形式的鏈接生效,我們要編寫一個表單,重設密碼。 ``` http://example.com/password_resets/3BdBrXeQZSWqFIDRN8cxHA/edit?email=foo%40bar.com ``` 這個表單的目的和編輯用戶資料的表單([代碼清單 9.2](chapter9.html#listing-user-edit-view))類似,不過現在只需更新密碼和密碼確認字段。而且處理起來有點復雜,因為我們希望通過電子郵件地址查找用戶,也就是說,在 `edit` 動作和 `update` 動作中都需要使用郵件地址。在 `edit` 動作中可以輕易的獲取郵件地址,因為鏈接中有。可是提交表單后,郵件地址就沒有了。為了解決這個問題,我們可以使用一個“隱藏字段”,把這個字段的值設為郵件地址(不會顯示),和表單中的其他數據一起提交給 `update` 動作,如[代碼清單 10.50](#listing-password-reset-form) 所示。 ##### 代碼清單 10.50:重設密碼的表單 app/views/password_resets/edit.html.erb ``` <% provide(:title, 'Reset password') %> <h1>Reset password</h1> <div class="row"> <div class="col-md-6 col-md-offset-3"> <%= form_for(@user, url: password_reset_path(params[:id])) do |f| %> <%= render 'shared/error_messages' %> <%= hidden_field_tag :email, @user.email %> <%= f.label :password %> <%= f.password_field :password, class: 'form-control' %> <%= f.label :password_confirmation, "Confirmation" %> <%= f.password_field :password_confirmation, class: 'form-control' %> <%= f.submit "Update password", class: "btn btn-primary" %> <% end %> </div> </div> ``` 注意,在[代碼清單 10.50](#listing-password-reset-form) 中,使用的表單字段輔助方法是 ``` hidden_field_tag :email, @user.email ``` 而不是 ``` f.hidden_field :email, @user.email ``` 因為在重設密碼的鏈接中,郵件地址在 `params[:email]` 中,如果使用后者,就會把郵件地址放入 `params[:user][:email]` 中。 為了正確渲染這個表單,我們要在 `PasswordResetsController` 的 `edit` 控制器中定義 `@user` 變量。和賬戶激活一樣([代碼清單 10.29](#listing-account-activation-edit-action)),我們要找到 `params[:email]` 中電子郵件地址對應的用戶,確認這個用戶已經激活,然后使用[代碼清單 10.24](#listing-generalized-authenticated-p) 中的 `authenticated?` 方法認證 `params[:id]` 中的令牌。因為在 `edit` 和 `update` 動作中都要使用 `@user`,所以我們要把查找用戶和認證令牌的代碼寫入一個事前過濾器中,如[代碼清單 10.51](#listing-password-reset-edit-action) 所示。 ##### 代碼清單 10.51:重設密碼的 `edit` 動作 app/controllers/password_resets_controller.rb ``` class PasswordResetsController < ApplicationController before_action :get_user, only: [:edit, :update] before_action :valid_user, only: [:edit, :update] . . . def edit end private def get_user @user = User.find_by(email: params[:email]) end # 確保是有效用戶 def valid_user unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id])) redirect_to root_url end end end ``` [代碼清單 10.51](#listing-password-reset-edit-action) 中的 `authenticated?(:reset, params[:id])`,[代碼清單 10.26](#listing-generalized-current-user) 中的 `authenticated?(:remember, cookies[:remember_token])`,以及[代碼清單 10.29](#listing-account-activation-edit-action) 中的 `authenticated?(:activation, params[:id])`,就是[表 10.1](#table-password-token-digest) 中 `authenticated?` 方法的三個用例。 現在,點擊[代碼清單 10.49](#listing-password-reset-email) 中的鏈接后,會顯示密碼重設表單,如[圖 10.17](#fig-password-reset-form) 所示。 ![password reset form](https://box.kancloud.cn/2016-05-11_5733306bb4777.png)圖 10.17:密碼重設表單 `edit` 動作對應的 `update` 動作要考慮四種情況:密碼重設超時失效,重設成功,密碼無效導致的重設失敗,密碼和密碼確認為空值時導致的密碼重設失敗(此時看起來像是成功了)。前三種情況對應[代碼清單 10.52](#listing-password-reset-update-action) 中外層 `if` 語句的三個分支。因為這個表單會修改 Active Record 模型(用戶模型),所以我們可以使用共用的局部視圖渲染錯誤消息。密碼為空值的情況比較特殊,因為用戶模型的驗證允許出現這種情況(參見[代碼清單 9.10](chapter9.html#listing-allow-blank-password)),所以要特別處理,直接在 `@user` 對象的錯誤消息中添加一個錯誤:[[8](#fn-8)] ``` @user.errors.add(:password, "can't be empty") ``` ##### 代碼清單 10.52:重設密碼的 `update` 動作 app/controllers/password_resets_controller.rb ``` class PasswordResetsController < ApplicationController before_action :get_user, only: [:edit, :update] before_action :valid_user, only: [:edit, :update] before_action :check_expiration, only: [:edit, :update] def new end def create @user = User.find_by(email: params[:password_reset][:email].downcase) if @user @user.create_reset_digest @user.send_password_reset_email flash[:info] = "Email sent with password reset instructions" redirect_to root_url else flash.now[:danger] = "Email address not found" render 'new' end end def edit end def update if params[:user][:password].empty? @user.errors.add(:password, "can't be empty") render 'edit' elsif @user.update_attributes(user_params) log_in @user flash[:success] = "Password has been reset." redirect_to @user else render 'edit' end end private def user_params params.require(:user).permit(:password, :password_confirmation) end # 事前過濾器 def get_user @user = User.find_by(email: params[:email]) end # 確保是有效用戶 def valid_user unless (@user && @user.activated? && @user.authenticated?(:reset, params[:id])) redirect_to root_url end end # 檢查重設令牌是否過期 def check_expiration if @user.password_reset_expired? flash[:danger] = "Password reset has expired." redirect_to new_password_reset_url end end end ``` 我們把密碼重設是否超時失效交給用戶模型判斷: ``` @user.password_reset_expired? ``` 所以,我們要定義 `password_reset_expired?` 方法。如 [10.2.3 節](#password-reset-mailer-method)的郵件模板所示,如果郵件發出后兩個小時內沒重設密碼,就認為此次請求超時失效了。這個設想可以通過下面的 Ruby 代碼實現: ``` reset_sent_at < 2.hours.ago ``` 如果你把 `&lt;` 當成小于號,讀成“密碼重設郵件發出少于兩小時”就錯了,和想表達的意思正好相反。 這里,最好把 `&lt;` 理解成“超過”,讀成“密碼重設郵件已經發出超過兩小時”,這才是我們想表達的意思。`password_reset_expired?` 方法的定義如[代碼清單 10.53](#listing-user-model-password-reset-expired) 所示。(對這個比較算式的證明參見 [10.6 節](#proof-of-expiration-comparison)。) ##### 代碼清單 10.53:在用戶模型中定義 `password_reset_expired?` 方法 app/models/user.rb ``` class User < ActiveRecord::Base . . . # 如果密碼重設超時失效了,返回 true def password_reset_expired? reset_sent_at < 2.hours.ago end private . . . end ``` 現在,[代碼清單 10.52](#listing-password-reset-update-action) 中的 `update` 動作可以使用了。密碼重設失敗和成功后顯示的頁面分別如[圖 10.18](#fig-password-reset-failure) 和[圖 10.19](#fig-password-reset-success) 所示。(稍等一會,[10.5 節](#account-activation-and-password-reset-exercises)中有一題,為第三個分支編寫測試。) ![password reset failure](https://box.kancloud.cn/2016-05-11_5733306bd376d.png)圖 10.18:密碼重設失敗![password reset success](https://box.kancloud.cn/2016-05-11_5733306bebaae.png)圖 10.19:密碼重設成功 ## 10.2.5 測試 本節,我們要編寫一個集成測試,覆蓋[代碼清單 10.52](#listing-password-reset-update-action) 中的兩個分支:重設失敗和重設成功。(前面說過,第三個分支的測試留作練習。)首先,為重設密碼生成一個測試文件: ``` $ rails generate integration_test password_resets invoke test_unit create test/integration/password_resets_test.rb ``` 這個測試的步驟大致和[代碼清單 10.31](#listing-signup-with-account-activation-test) 中的賬戶激活測試差不多,不過開頭有點不同。首先訪問“Forgot Password”表單,分別提交有效和無效的電子郵件地址,電子郵件地址有效時要創建密碼重設令牌,并且發送重設郵件。然后,訪問郵件中的鏈接,分別提交無效和有效的密碼,驗證各自的表現是否正確。最終寫出的測試如[代碼清單 10.54](#listing-password-reset-integration-test) 所示。這是一個不錯的練習,可以鍛煉閱讀代碼的能力。 ##### 代碼清單 10.54:密碼重設的集成測試 test/integration/password_resets_test.rb ``` require 'test_helper' class PasswordResetsTest < ActionDispatch::IntegrationTest def setup ActionMailer::Base.deliveries.clear @user = users(:michael) end test "password resets" do get new_password_reset_path assert_template 'password_resets/new' # 電子郵件地址無效 post password_resets_path, password_reset: { email: "" } assert_not flash.empty? assert_template 'password_resets/new' # 電子郵件地址有效 post password_resets_path, password_reset: { email: @user.email } assert_not_equal @user.reset_digest, @user.reload.reset_digest assert_equal 1, ActionMailer::Base.deliveries.size assert_not flash.empty? assert_redirected_to root_url # 密碼重設表單 user = assigns(:user) # 電子郵件地址錯誤 get edit_password_reset_path(user.reset_token, email: "") assert_redirected_to root_url # 用戶未激活 user.toggle!(:activated) get edit_password_reset_path(user.reset_token, email: user.email) assert_redirected_to root_url user.toggle!(:activated) # 電子郵件地址正確,令牌不對 get edit_password_reset_path('wrong token', email: user.email) assert_redirected_to root_url # 電子郵件地址正確,令牌也對 get edit_password_reset_path(user.reset_token, email: user.email) assert_template 'password_resets/edit' assert_select "input[name=email][type=hidden][value=?]", user.email # 密碼和密碼確認不匹配 patch password_reset_path(user.reset_token), email: user.email, user: { password: "foobaz", password_confirmation: "barquux" } assert_select 'div#error_explanation' # 密碼為空值 patch password_reset_path(user.reset_token), email: user.email, user: { password: "", password_confirmation: "" } assert_select 'div#error_explanation' # 密碼和密碼確認有效 patch password_reset_path(user.reset_token), email: user.email, user: { password: "foobaz", password_confirmation: "foobaz" } assert is_logged_in? assert_not flash.empty? assert_redirected_to user end end ``` [代碼清單 10.54](#listing-password-reset-integration-test) 中的大多數用法前面都見過,但是針對 `input` 標簽的測試有點陌生: ``` assert_select "input[name=email][type=hidden][value=?]", user.email ``` 這行代碼的意思是,頁面中有 `name` 屬性、類型(隱藏)和電子郵件地址都正確的 `input` 標簽: ``` <input id="email" name="email" type="hidden" value="michael@example.com" /> ``` 現在,測試組件應該能通過: ##### 代碼清單 10.55:**GREEN** ``` $ bundle exec rake test ```
                  <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>

                              哎呀哎呀视频在线观看