本文翻譯自The Flask Mega-Tutorial Part X: Email Supporthtml
這是Flask Mega-Tutorial系列的第十部分,在其中我將告訴你,應用如何向你的用戶發送電子郵件,以及如何在電子郵件支持之上構建密碼重置功能。python
如今,應用在數據庫方面作得至關不錯,因此在本章中,我想拋開這個主題,開始添加發送電子郵件的功能,這是大多數Web應用必需的另外一個重要部分。git
爲何應用須要發送電子郵件給用戶? 緣由不少,但其中一個常見的緣由是解決與認證相關的問題。 在本章中,我將爲忘記密碼的用戶添加密碼重置功能。 當用戶請求重置密碼時,應用將發送包含特製連接的電子郵件。 用戶而後須要點擊該連接才能訪問設置新密碼的表單。github
本章的GitHub連接爲:Browse, Zip, Diff.web
就實際的郵件發送而言,Flask有一個名爲Flask-Mail的流行插件,可使任務變得很是簡單。 和往常同樣,該插件是用pip安裝的:算法
密碼重置連接將包含有一個安全令牌。 爲了生成這些令牌,我將使用JSON Web Tokens,它也有一個流行的Python包:shell
Flask-Mail插件是經過app.config
對象來配置的。還記得在第七章中,我添加了用於在生產環境中發生錯誤時發送電子郵件的配置項? 當時我沒有告訴你,不過,我選擇的配置變量都是Flask-Mail的需求的,因此不須要任何額外的工做,配置的活已經完工。數據庫
像大多數Flask插件同樣,你須要在Flask應用建立以後建立一個郵件實例。 本處,mail
是類Mail
的一個實例:flask
第七章中我提到過,測試發送電子郵件的方式有兩種。 若是你想使用一個模擬的電子郵件服務器,Python提供了一個很是好用的方法,你可使用下面的命令在第二個終端中啓動它:api
要配置此服務器,須要設置兩個環境變量:
若是你但願真實地發送電子郵件,則須要使用真實的電子郵件服務器。 那麼你只須要爲它設置MAIL_SERVER
、MAIL_PORT
、MAIL_USE_TLS
、MAIL_USERNAME
和MAIL_PASSWORD
環境變量。 若是你想要快速解決方案,可使用Gmail賬戶發送電子郵件,並使用如下設置:
若是你使用的是Microsoft Windows,則須要在上面的每一個export
語句中將export
替換爲set
。
Gmail賬戶中的安全功能可能會阻止應用經過它發送電子郵件,除非你明確容許「安全性較低的應用程序」訪問你的Gmail賬戶。 能夠閱讀此處來了解具體狀況,若是你擔憂賬戶的安全性,能夠建立一個輔助郵箱賬戶,配置它來僅用於測試電子郵件功能,或者你能夠暫時啓用容許不太安全的應用程序來運行此測試,完成後恢復爲默認值。
爲了學習Flask-Mail如何工做,我將向你展現如何用Python shell發送電子郵件。那麼,運行flask shell
以激活Python,而後運行下面的命令:
上面的代碼片斷將發送一個電子郵件到你在recipients
參數中設置的電子郵件地址列表。發件人配置項我在第七章中已經配置過了,是ADMINS
。 該電子郵件將具備純文本和HTML版本,因此根據你的電子郵件客戶端的配置,可能會看到它們之中的其中之一。
如你所見,至關簡單。如今讓咱們將電子郵件整合到應用中。
我將從編寫一個發送電子郵件的幫助函數開始,這個函數基本上是上一節中shell函數的通用版本。 我將把這個函數放在一個名爲app/email.py
的新模塊中:
Flask-Mail支持一些我不在這裏使用的功能,如抄送和密件抄送列表。 若是你對這些選項感興趣,務必查閱Flask-Mail文檔。
我上面提到過,用戶有權利重置密碼。所以我將在登陸頁面提供一個連接:
當用戶點擊連接時,會出現一個新的Web表單,要求用戶輸入註冊的電子郵件地址,以啓動密碼重置過程。 這裏是表單類:
這裏是相應的HTML模板:
固然也須要一個視圖函數來處理表單:
該視圖函數與其餘的表單處理視圖函數很是類似。 我從確保用戶沒有登陸開始,若是用戶登陸,那麼使用密碼重置功能就沒有意義,因此我重定向到主頁。
當表格被提交併驗證經過,我使用表格中的用戶提供的電子郵件來查找用戶。 若是我找到用戶,就發送一封密碼重置電子郵件。 我執行此操做使用的send_password_reset_email()
輔助函數,將在下面向你展現。
電子郵件發送後,我會閃現一條消息,指示用戶查看電子郵件以獲取進一步說明,而後重定向回登陸頁面。 你可能會注意到,即便用戶提供的電子郵件不存在,也會顯示閃現的消息,這樣的話,客戶端就不能用這個表單來判斷一個給定的用戶是否已註冊。
在實現send_password_reset_email()
函數以前,我須要一種方法來生成密碼重置連接,它將被經過電子郵件發送給用戶。 當連接被點擊時,將爲用戶展示設置新密碼的頁面。 這個計劃中棘手的部分是確保只有有效的重置連接能夠用來重置賬戶的密碼。
生成的連接中會包含令牌,它將在容許密碼變動以前被驗證,以證實請求重置密碼的用戶是經過訪問重置密碼郵件中的連接而來的。JSON Web Token(JWT)是這類令牌處理的流行標準。 JWTs的優勢是它是自成一體的,不但能夠生成令牌,還提供對應的驗證方法。
如何運行JWTs?讓咱們經過Python shell來學習一下:
{'a':'b'}
字典是要寫入令牌的示例有效載荷。 爲了使令牌安全,須要提供一個祕密密鑰用於建立加密簽名。 在這個例子中,我使用了字符串'my-secret'
,可是在應用中,我將使用配置中的SECRET_KEY
。algorithm
參數指定使用什麼算法來生成令牌,而HS256
是應用最普遍的算法。
如你所見,獲得的令牌是一長串字符。 可是不要認爲這是一個加密的令牌。 令牌的內容,包括有效載荷,能夠被任何人輕易解碼(不相信我?複製上面的令牌,而後粘貼在JWT調試器上就能夠看到它的內容)。 使令牌安全的是,有效載荷是被簽名的。 若是有人試圖僞造或篡改令牌中的有效載荷,則簽名將會無效,而且生成新的簽名依賴祕密密鑰。 令牌驗證經過時,有效負載的內容將被解碼並返回給調用者。 若是令牌的簽名驗證經過,有效載荷才能夠被認爲是可信的。
我要用於密碼重置令牌的有效載荷格式爲{'reset_password':user_id,'exp':token_expiration}
。 exp
字段是JWTs的標準,若是它存在,則表示令牌的到期時間。 若是一個令牌有一個有效的簽名,可是它已通過期,那麼它也將被認爲是無效的。 對於密碼重置功能,我會給這些令牌10分鐘的有效期。
當用戶點擊電子郵件連接時,令牌將被做爲URL的一部分發送回應用,處理這個URL的視圖函數首先要作的就是驗證它。 若是簽名是有效的,則能夠經過存儲在有效載荷中的ID來識別用戶。 一旦得知用戶的身份,應用能夠要求一個新的密碼,並將其設置在用戶的賬戶上。
因爲這些令牌屬於用戶,所以我將在User
模型中編寫令牌生成和驗證的方法:
get_reset_password_token()
函數以字符串形式生成一個JWT令牌。 請注意,decode('utf-8')
是必須的,由於jwt.encode()
函數將令牌做爲字節序列返回,可是在應用中將令牌表示爲字符串更方便。
verify_reset_password_token()
是一個靜態方法,這意味着它能夠直接從類中調用。 靜態方法與類方法相似,惟一的區別是靜態方法不會接收類做爲第一個參數。 這個方法須要一個令牌,並嘗試經過調用PyJWT的jwt.decode()
函數來解碼它。 若是令牌不能被驗證或已過時,將會引起異常,在這種狀況下,我會捕獲它以防止出現錯誤,而後將None
返回給調用者。 若是令牌有效,那麼來自令牌有效負載的reset_password
的值就是用戶的ID,因此我能夠加載用戶並返回它。
如今我有了令牌,能夠生成密碼重置電子郵件。 send_password_reset_email()
函數依賴於上面寫的send_email()
函數。
這個函數中有趣的部分是電子郵件的文本和HTML內容是使用熟悉的render_template()
函數從模板生成的。 模板接收用戶和令牌做爲參數,以即可以生成個性化的電子郵件消息。 如下是重置密碼電子郵件的文本模板:
這是更美觀的的HTML版本:
請注意,這兩個電子郵件模板中的url_for()
調用中引用的reset_password
路由尚不存在,這將在下一節中添加。
當用戶點擊電子郵件連接時,會觸發與此功能相關的第二個路由。 這是密碼重置視圖函數:
在這個視圖函數中,我首先確保用戶沒有登陸,而後經過調用User
類的令牌驗證方法來肯定用戶是誰。 若是令牌有效,則此方法返回用戶;若是不是,則返回None
,並將重定向到主頁。
若是令牌是有效的,那麼我向用戶呈現第二個表單,須要用戶其中輸入新密碼。 這個表單的處理方式與之前的表單相似,表單提交驗證經過後,我調用User
類的set_password()
方法來更改密碼,而後重定向到登陸頁面,以便用戶登陸。
這是ResetPasswordForm
類:
這是相應的HTML模板:
密碼重置功能現已完成,必定要多嘗試幾回。
若是你正在使用Python提供的模擬電子郵件服務器,可能沒有注意到這一點,那就是發送電子郵件會大大減慢應用的速度,緣由是發送電子郵件時所發生的和電子郵件服務器的網絡交互。一般須要幾秒鐘的時間才能收到電子郵件,若是收件人的電子郵件服務器速度較慢,或者收件人有多個,則可能會更久。
我真正想要的send_email()
函數是異步的。 那是什麼意思? 這意味着當這個函數被調用時,發送郵件的任務被安排在後臺進行,釋放send_email()
函數以當即返回,以便應用能夠在發送郵件的同時繼續運行。
Python實際上有多種方式支持運行異步任務,threading
和multiprocessing
模塊均可以作到這一點。 爲發送電子郵件啓動一個後臺線程,比開始一個全新的進程須要的資源少得多,因此我打算採用這種方法:
send_async_email
函數如今運行在後臺線程中,它經過send_email()
的最後一行中的Thread()
類來調用。 有了這個改變,電子郵件的發送將在線程中運行,而且當進程完成時,線程將結束並自行清理。 若是你已經配置了一個真正的電子郵件服務器,當你按下密碼重置請求表單上的提交按鈕時,確定會注意到訪問速度的提高。
你可能預期只有msg
參數會被髮送到線程,但正如你在代碼中所看到的那樣,我也傳入了應用實例。 使用線程時,須要牢記Flask的一個重要設計方面。 Flask使用上下文來避免必須跨函數傳遞參數。 我不打算詳細討論這個問題,可是須要知道的是,有兩種類型的上下文,即應用上下文和請求上下文。 在大多數狀況下,這些上下文由框架自動管理,可是當應用啓動自定義線程時,可能須要手動建立這些線程的上下文。
許多Flask插件須要應用上下文才能工做,由於這使得他們能夠在不傳遞參數的狀況下找到Flask應用實例。這些插件須要知道應用實例的緣由是由於它們的配置存儲在app.config
對象中,這正是Flask-Mail的狀況。mail.send()
方法須要訪問電子郵件服務器的配置值,而這必須經過訪問應用屬性的方式來實現。 使用with app.app_context()
調用建立的應用上下文使得應用實例能夠經過來自Flask的current_app
變量來進行訪問。