【Flask/跟着學習】Flask大型教程項目#09:電子郵箱

跟着學習(新版):https://blog.miguelgrinberg.com/post/the-flask-mega-tutorial-part-x-email-support
回顧上一章:https://blog.csdn.net/weixin_41263513/article/details/85057156

本章內容

  • Flask-Mail簡單介紹
  • 發送電子郵件
  • 重置密碼的準備工作
  • 密碼重置令牌
  • 發送密碼重置的郵件
  • 重置用戶密碼
  • 異步發送電子郵箱
  • 發送qq郵箱

很多類型的應用都需要在特定事件發生時通知用戶,而常用的通信方法是電子郵箱。在本章中,我將爲忘記密碼的用戶添加密碼重置功能。 當用戶請求重置密碼時,應用程序將發送包含特製鏈接的電子郵件。 然後,用戶需要單擊該鏈接以訪問用於設置新密碼的表單。

Flask-Mail簡單介紹

發送電子郵箱而言,flask有一個名爲Flask-Mail的流行擴展,可以使任務變得非常簡單,記得用pip install一下
而密碼重置鏈接中將包含安全令牌,爲了生成令牌,我將使用JSON Web令牌,他也有一個流行的python包,這個需要pip install pyjwt

Flask-Mail擴展使從app.config對象配置的
文件:/app/–init–.py

# ...
from flask_mail import Mail

app = Flask(__name__)
# ...
mail = Mail(app)

如果您希望發送真實的電子郵件,則需要使用真實的電子郵件服務器。 如果您有一個,那麼您只需要爲它設置MAIL_SERVER,MAIL_PORT,MAIL_USE_TLS,MAIL_USERNAME和MAIL_PASSWORD環境變量

發送電子郵件

我們先寫一個發送發送地安仔郵箱的輔助函數
文件:/app/email.py

from flask_mail import Message
from app import mail

def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    mail.send(msg)

sender:發送人
recipients:收件人
發送電子郵件內容只需要會用就可以,之後我會教大家怎麼設置這些參數

重置密碼的準備工作

正如我上面提到的,我希望用戶可以選擇重置密碼。 爲此,我將在登錄頁面中添加一個鏈接:
文件:/app/templates/login.html

<p>
	Forgot Your Password?
	<a href="{{ url_for('reset_password_request') }}">Click to Reset It</a>
</p>

當用戶單擊該鏈接時,將出現一個新的Web表單,該表單請求用戶的電子郵件地址作爲啓動密碼重置過程的方法。 以下是表單類:
文件:/app/forms.py

class ResetPasswordRequestForm(FlaskForm):
    email = StringField('Email', validators=[DataRequired(), Email()])
    submit = SubmitField('Request Password Reset')

以下是相應的HTML模板
文件:/app/templates/reset_password_request.html

{% extends "base.html" %}

{% block content %}
    <h1>Reset Password</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.email.label }}<br>
            {{ form.email(size=64) }}<br>
            {% for error in form.email.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

我們也需要一個視圖函數來處理這個表格:
文件:/app/routes.py

from app.forms import ResetPasswordRequestForm
from app.email import send_password_reset_email

@app.route('/reset_password_request', methods=['GET', 'POST'])
def reset_password_request():
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    form = ResetPasswordRequestForm()
    if form.validate_on_submit():
        user = User.query.filter_by(email=form.email.data).first()
        if user:
            send_password_reset_email(user)
        flash('Check your email for the instructions to reset your password')
        return redirect(url_for('login'))
    return render_template('reset_password_request.html',
                           title='Reset Password', form=form)

此視圖功能與處理表單的其他功能非常相似。 我首先確保用戶沒有登錄。如果用戶已登錄,則使用密碼重置功能沒有意義,因此我重定向到index

當表單提交併有效時,我會通過表單中用戶提供的電子郵件查找用戶。 如果我找到該用戶,我會發送密碼重置電子郵件

發送電子郵件後,我會閃爍一條消息,指示用戶查找電子郵件以獲取進一步說明,然後重定向回登錄頁面。 您可能會注意到即使用戶提供的電子郵件未知,也會顯示閃爍的消息。

密碼重置令牌

在實現send_password_reset_email()函數之前,我需要有一種方法來生成密碼請求鏈接。 這將是通過電子郵件發送給用戶的鏈接。 單擊鏈接時,將向用戶顯示可以設置新密碼的頁面

鏈接將配置一個令牌,並且在允許更改密碼之前將驗證此令牌,作爲請求該電子郵件的用戶可以訪問該帳戶上的電子郵件地址的證據。 這種類型的進程的一個非常流行的令牌標準是JSON Web令牌或JWT。 關於JWT的好處是它們是自包含的。 您可以通過電子郵件向用戶發送令牌,當用戶單擊將令牌反饋給應用程序的鏈接時,可以單獨驗證它。

由於這些令牌屬於用戶,因此我將把令牌生成和驗證功能編寫爲用戶模型中的方法
文件:/app/models.py

from time import time
import jwt
from app import app

class User(UserMixin, db.Model):
    # ...

    def get_reset_password_token(self, expires_in=600):
        return jwt.encode(
            {'reset_password': self.id, 'exp': time() + expires_in},
            app.config['SECRET_KEY'], algorithm='HS256').decode('utf-8')

    @staticmethod
    def verify_reset_password_token(token):
        try:
            id = jwt.decode(token, app.config['SECRET_KEY'],
                            algorithms=['HS256'])['reset_password']
        except:
            return
        return User.query.get(id)

algorithm:算法,用算法HS256生成令牌

我將用於密碼重置令牌的有效負載將具有格式{‘reset_password’:user_id,‘exp’:token_expiration}。 exp字段是JWT的標準字段,如果存在,則表示令牌的到期時間。如果令牌具有有效簽名,但它已超過其到期時間戳,則它也將被視爲無效。對於密碼重置功能,我將給這些令牌提供10分鐘的生命。

get_reset_password_token()函數生成一個JWT標記作爲字符串。請注意,decode(‘utf-8’)是必需的,因爲jwt.encode()函數將標記作爲字節序列返回,但在應用程序中將標記作爲字符串更方便。

verify_reset_password_token()是一個靜態方法,這意味着它可以直接從類中調用。靜態方法類似於類方法,唯一的區別是靜態方法不接收類作爲第一個參數。此方法接受一個令牌並嘗試通過調用PyJWT的jwt.decode()函數對其進行解碼。如果令牌無法驗證或過期,則會引發異常,在這種情況下,我會捕獲它以防止錯誤,然後將None返回給調用者。如果令牌有效,則令牌有效負載中reset_password鍵的值是用戶的ID,因此我可以加載用戶並將其返回。

發送密碼重置的郵件

現在有了令牌,就可以生成密碼重置電子郵件
文件:/app/email.py

from flask import render_template
from app import app

# ...

def send_password_reset_email(user):
    token = user.get_reset_password_token()
    send_email('[Microblog] Reset Your Password',
               sender=app.config['ADMINS'][0],
               recipients=[user.email],
               text_body=render_template('email/reset_password.txt',
                                         user=user, token=token),
               html_body=render_template('email/reset_password.html',
                                         user=user, token=token))

這個函數的有趣部分是使用熟悉的render_template()函數從模板生成電子郵件的文本和HTML內容。 模板接收用戶和令牌作爲參數,以便可以生成個性化電子郵件消息。 以下是重置密碼電子郵件的文本模板:
文件:/app/templates/email/reset_password.txt

Dear {{ user.username }},

To reset your password click on the following link:

{{ url_for('reset_password', token=token, _external=True) }}

If you have not requested a password reset simply ignore this message.

Sincerely,

The Microblog Team

在這兩個電子郵件模板中的url_for()調用中引用的reset_password路由目前還不存在,這將在下一節中添加。 我在兩個模板中的url_for()調用中包含的_external = True參數也是新的。 默認情況下,url_for()生成的URL是相對URL,例如,url_for(‘user’,username =‘susan’)調用將返回/ user / susan。 這通常足以用於在網頁中生成的鏈接,因爲Web瀏覽器從當前頁面獲取URL的其餘部分。 但是,當通過電子郵件發送URL時,該上下文不存在,因此需要使用完全限定的URL。 當_external = True作爲參數傳遞時,會生成完整的URL

有了txt,html也要相應的改變
文件:/app/templates/email/reset_password.html

<p>Dear {{ user.username }},</p>
<p>
    To reset your password
    <a href="{{ url_for('reset_password', token=token, _external=True) }}">
        click here
    </a>.
</p>
<p>Alternatively, you can paste the following link in your browser's address bar:</p>
<p>{{ url_for('reset_password', token=token, _external=True) }}</p>
<p>If you have not requested a password reset simply ignore this message.</p>
<p>Sincerely,</p>
<p>The Microblog Team</p>

重置用戶密碼

當用戶單擊電子郵件鏈接時,將觸發與此功能關聯的第二個路由。
文件:/app/routes.py:

from app.forms import ResetPasswordForm

@app.route('/reset_password/<token>', methods=['GET', 'POST'])
def reset_password(token):
    if current_user.is_authenticated:
        return redirect(url_for('index'))
    user = User.verify_reset_password_token(token)
    if not user:
        return redirect(url_for('index'))
    form = ResetPasswordForm()
    if form.validate_on_submit():
        user.set_password(form.password.data)
        db.session.commit()
        flash('Your password has been reset.')
        return redirect(url_for('login'))
    return render_template('reset_password.html', form=form)

在此視圖函數中,我首先確保用戶未登錄,然後通過調用User類中的令牌驗證方法來確定用戶是誰。 如果令牌有效,則此方法返回用戶,否則返回None。 如果令牌無效,我會重定向到主頁。

如果令牌有效,那麼我向用戶顯示第二個表單,其中請求新密碼。 此表單以類似於以前的表單的方式處理,並且作爲有效表單提交的結果,我調用User的set_password()方法來更改密碼,然後重定向到登錄頁面,用戶現在可以在該頁面登錄。

以下是ResetPasswordForm的類
文件:/app/forms.py

class ResetPasswordForm(FlaskForm):
    password = PasswordField('Password', validators=[DataRequired()])
    password2 = PasswordField(
        'Repeat Password', validators=[DataRequired(), EqualTo('password')])
    submit = SubmitField('Request Password Reset')

然後是相應的html模板:
文件:/app/templates/reset_password.html:

{% extends "base.html" %}

{% block content %}
    <h1>Reset Your Password</h1>
    <form action="" method="post">
        {{ form.hidden_tag() }}
        <p>
            {{ form.password.label }}<br>
            {{ form.password(size=32) }}<br>
            {% for error in form.password.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>
            {{ form.password2.label }}<br>
            {{ form.password2(size=32) }}<br>
            {% for error in form.password2.errors %}
            <span style="color: red;">[{{ error }}]</span>
            {% endfor %}
        </p>
        <p>{{ form.submit() }}</p>
    </form>
{% endblock %}

密碼重置功能現已完成

異步發送電子郵箱

發送電子郵件時需要進行的所有交互都會使任務變慢,通常需要幾秒鐘才能收到電子郵件,如果收件人的電子郵件服務器速度很慢,或者有多個收件人,可能會更多。

我真正想要的是send_email()函數是異步的。那是什麼意思?這意味着當調用此函數時,發送電子郵件的任務計劃在後臺進行,釋放send_email()以立即返回,以便應用程序可以繼續與發送的電子郵件同時運行。

Python支持以不止一種方式運行異步任務。線程和多處理模塊都可以做到這一點。爲發送電子郵件啓動後臺線程比開始一個全新的流程要少得多:
文件:/app/email.py

from threading import Thread
# ...

def send_async_email(app, msg):
    with app.app_context():
        mail.send(msg)


def send_email(subject, sender, recipients, text_body, html_body):
    msg = Message(subject, sender=sender, recipients=recipients)
    msg.body = text_body
    msg.html = html_body
    Thread(target=send_async_email, args=(app, msg)).start()

send_async_email函數現在在後臺線程中運行,通過send_email()的最後一行中的Thread()類調用。通過此更改,電子郵件的發送將在線程中運行,並且當進程完成時,線程將結束並自行清理。

您可能希望只將msg參數發送到該線程,但正如您在代碼中看到的那樣,我也發送了應用程序實例。使用線程時,需要牢記Flask的一個重要設計方面。 Flask使用上下文來避免必須跨函數傳遞參數。我不打算詳細介紹這個,但要知道有兩種類型的上下文,即應用程序上下文和請求上下文。在大多數情況下,這些上下文由框架自動管理,但是當應用程序啓動自定義線程時,可能需要手動創建這些線程的上下文。

有許多擴展需要應用程序上下文才能工作,因爲這允許他們找到Flask應用程序實例而不將其作爲參數傳遞。許多擴展需要知道應用程序實例的原因是因爲它們的配置存儲在app.config對象中。這正是Flask-Mail的情況。 mail.send()方法需要訪問電子郵件服務器的配置,而這隻能通過知道應用程序是什麼來完成。使用with app.app_context()調用創建的應用程序上下文使得可以通過Flask的current_app變量訪問應用程序實例。

發送qq郵箱

參考了另一位大大的博客:https://blog.csdn.net/qq_42239520/article/details/80368733
在下面我將簡單展示以下怎麼通過qq郵箱收發郵箱
先進去自己的qq郵箱開啓相應的服務
在這裏插入圖片描述
開啓的時候他會給你一個授權碼,記住這個授權碼
在這裏插入圖片描述
簡單測試
加入一個新的視圖函數
在這裏插入圖片描述
當然config裏面也要改
在這裏插入圖片描述
在這裏插入圖片描述
運行,輸入網址,成功!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章